examples/core: building without ECS
Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
This commit is contained in:
parent
2a13c07d9e
commit
0e12857154
35 changed files with 1365 additions and 4176 deletions
10
build.zig
10
build.zig
|
|
@ -73,13 +73,13 @@ pub fn build(b: *std.Build) !void {
|
|||
var examples = [_]Example{
|
||||
.{ .name = "core-custom-entrypoint", .deps = &.{} },
|
||||
.{ .name = "core-triangle", .deps = &.{} },
|
||||
.{ .name = "custom-renderer", .deps = &.{} },
|
||||
.{ .name = "glyphs", .deps = &.{ .assets, .freetype } },
|
||||
// .{ .name = "custom-renderer", .deps = &.{} },
|
||||
// .{ .name = "glyphs", .deps = &.{ .assets, .freetype } },
|
||||
// .{ .name = "hardware-check", .deps = &.{ .assets, .zigimg } },
|
||||
.{ .name = "piano", .deps = &.{} },
|
||||
.{ .name = "play-opus", .deps = &.{.assets} },
|
||||
// .{ .name = "piano", .deps = &.{} },
|
||||
// .{ .name = "play-opus", .deps = &.{.assets} },
|
||||
// .{ .name = "sprite", .deps = &.{ .zigimg, .assets } },
|
||||
.{ .name = "text", .deps = &.{.assets} },
|
||||
// .{ .name = "text", .deps = &.{.assets} },
|
||||
};
|
||||
|
||||
var sysaudio_tests = [_]SysAudioTest{
|
||||
|
|
|
|||
|
|
@ -1,35 +1,36 @@
|
|||
const mach = @import("mach");
|
||||
const gpu = mach.gpu;
|
||||
|
||||
pub const name = .app;
|
||||
pub const Mod = mach.Mod(@This());
|
||||
const App = @This();
|
||||
|
||||
pub const systems = .{
|
||||
.start = .{ .handler = start },
|
||||
.init = .{ .handler = init },
|
||||
.deinit = .{ .handler = deinit },
|
||||
.tick = .{ .handler = tick },
|
||||
};
|
||||
pub const mach_module = .app;
|
||||
|
||||
pub const mach_systems = .{ .main, .init, .deinit, .tick };
|
||||
|
||||
title_timer: mach.time.Timer,
|
||||
pipeline: *gpu.RenderPipeline,
|
||||
|
||||
pub fn deinit(core: *mach.Core.Mod, app: *Mod) void {
|
||||
app.state().pipeline.release();
|
||||
core.schedule(.deinit);
|
||||
pub const main = mach.schedule(.{
|
||||
.{ mach.Core, .init },
|
||||
.{ App, .init },
|
||||
.{ mach.Core, .main },
|
||||
});
|
||||
|
||||
pub fn deinit(app: *App) void {
|
||||
app.pipeline.release();
|
||||
}
|
||||
|
||||
fn start(app: *Mod, core: *mach.Core.Mod) !void {
|
||||
core.schedule(.init);
|
||||
app.schedule(.init);
|
||||
}
|
||||
|
||||
fn init(app: *Mod, core: *mach.Core.Mod) !void {
|
||||
core.state().on_tick = app.system(.tick);
|
||||
core.state().on_exit = app.system(.deinit);
|
||||
pub fn init(
|
||||
app: *App,
|
||||
core: *mach.Core,
|
||||
app_tick: mach.Call(App, .tick),
|
||||
app_deinit: mach.Call(App, .deinit),
|
||||
) !void {
|
||||
core.on_tick = app_tick.id;
|
||||
core.on_exit = app_deinit.id;
|
||||
|
||||
// Create our shader module
|
||||
const shader_module = core.state().device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl"));
|
||||
const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl"));
|
||||
defer shader_module.release();
|
||||
|
||||
// Blend state describes how rendered colors get blended
|
||||
|
|
@ -37,7 +38,7 @@ fn init(app: *Mod, core: *mach.Core.Mod) !void {
|
|||
|
||||
// Color target describes e.g. the pixel format of the window we are rendering to.
|
||||
const color_target = gpu.ColorTargetState{
|
||||
.format = core.get(core.state().main_window, .framebuffer_format).?,
|
||||
.format = core.windows.get(core.main_window).?.framebuffer_format,
|
||||
.blend = &blend,
|
||||
};
|
||||
|
||||
|
|
@ -49,7 +50,7 @@ fn init(app: *Mod, core: *mach.Core.Mod) !void {
|
|||
});
|
||||
|
||||
// Create our render pipeline that will ultimately get pixels onto the screen.
|
||||
const label = @tagName(name) ++ ".init";
|
||||
const label = @tagName(mach_module) ++ ".init";
|
||||
const pipeline_descriptor = gpu.RenderPipeline.Descriptor{
|
||||
.label = label,
|
||||
.fragment = &fragment,
|
||||
|
|
@ -58,34 +59,33 @@ fn init(app: *Mod, core: *mach.Core.Mod) !void {
|
|||
.entry_point = "vertex_main",
|
||||
},
|
||||
};
|
||||
const pipeline = core.state().device.createRenderPipeline(&pipeline_descriptor);
|
||||
const pipeline = core.device.createRenderPipeline(&pipeline_descriptor);
|
||||
|
||||
// Store our render pipeline in our module's state, so we can access it later on.
|
||||
app.init(.{
|
||||
.title_timer = try mach.time.Timer.start(),
|
||||
.pipeline = pipeline,
|
||||
});
|
||||
try updateWindowTitle(core);
|
||||
// TODO(object): module-state-init
|
||||
app.title_timer = try mach.time.Timer.start();
|
||||
app.pipeline = pipeline;
|
||||
|
||||
core.schedule(.start);
|
||||
// TODO(object): window-title
|
||||
// try updateWindowTitle(core);
|
||||
}
|
||||
|
||||
fn tick(core: *mach.Core.Mod, app: *Mod) !void {
|
||||
while (core.state().nextEvent()) |event| {
|
||||
pub fn tick(core: *mach.Core, app: *App) !void {
|
||||
while (core.nextEvent()) |event| {
|
||||
switch (event) {
|
||||
.close => core.schedule(.exit), // Tell mach.Core to exit the app
|
||||
.close => core.exit(),
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
// Grab the back buffer of the swapchain
|
||||
// TODO(Core)
|
||||
const back_buffer_view = core.state().swap_chain.getCurrentTextureView().?;
|
||||
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
|
||||
defer back_buffer_view.release();
|
||||
|
||||
// Create a command encoder
|
||||
const label = @tagName(name) ++ ".tick";
|
||||
const encoder = core.state().device.createCommandEncoder(&.{ .label = label });
|
||||
const label = @tagName(mach_module) ++ ".tick";
|
||||
const encoder = core.device.createCommandEncoder(&.{ .label = label });
|
||||
defer encoder.release();
|
||||
|
||||
// Begin render pass
|
||||
|
|
@ -103,7 +103,7 @@ fn tick(core: *mach.Core.Mod, app: *Mod) !void {
|
|||
defer render_pass.release();
|
||||
|
||||
// Draw
|
||||
render_pass.setPipeline(app.state().pipeline);
|
||||
render_pass.setPipeline(app.pipeline);
|
||||
render_pass.draw(3, 1, 0, 0);
|
||||
|
||||
// Finish render pass
|
||||
|
|
@ -112,27 +112,26 @@ fn tick(core: *mach.Core.Mod, app: *Mod) !void {
|
|||
// Submit our commands to the queue
|
||||
var command = encoder.finish(&.{ .label = label });
|
||||
defer command.release();
|
||||
core.state().queue.submit(&[_]*gpu.CommandBuffer{command});
|
||||
|
||||
// Present the frame
|
||||
core.schedule(.present_frame);
|
||||
core.queue.submit(&[_]*gpu.CommandBuffer{command});
|
||||
|
||||
// update the window title every second
|
||||
if (app.state().title_timer.read() >= 1.0) {
|
||||
app.state().title_timer.reset();
|
||||
try updateWindowTitle(core);
|
||||
if (app.title_timer.read() >= 1.0) {
|
||||
app.title_timer.reset();
|
||||
// TODO(object): window-title
|
||||
// try updateWindowTitle(core);
|
||||
}
|
||||
}
|
||||
|
||||
fn updateWindowTitle(core: *mach.Core.Mod) !void {
|
||||
try core.state().printTitle(
|
||||
core.state().main_window,
|
||||
"core-custom-entrypoint [ {d}fps ] [ Input {d}hz ]",
|
||||
.{
|
||||
// TODO(Core)
|
||||
core.state().frameRate(),
|
||||
core.state().inputRate(),
|
||||
},
|
||||
);
|
||||
core.schedule(.update);
|
||||
}
|
||||
// TODO(object): window-title
|
||||
// fn updateWindowTitle(core: *mach.Core) !void {
|
||||
// try core.printTitle(
|
||||
// core.main_window,
|
||||
// "core-custom-entrypoint [ {d}fps ] [ Input {d}hz ]",
|
||||
// .{
|
||||
// // TODO(Core)
|
||||
// core.frameRate(),
|
||||
// core.inputRate(),
|
||||
// },
|
||||
// );
|
||||
// core.schedule(.update);
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -1,40 +1,37 @@
|
|||
const std = @import("std");
|
||||
const mach = @import("mach");
|
||||
|
||||
// The global list of Mach modules our application may use.
|
||||
pub const modules = .{
|
||||
// The set of Mach modules our application may use.
|
||||
const Modules = mach.Modules(.{
|
||||
mach.Core,
|
||||
@import("App.zig"),
|
||||
};
|
||||
});
|
||||
|
||||
pub fn main() !void {
|
||||
const allocator = std.heap.c_allocator;
|
||||
|
||||
// Initialize module system
|
||||
try mach.mods.init(allocator);
|
||||
// The set of Mach modules our application may use.
|
||||
var mods = Modules.init(allocator);
|
||||
|
||||
// Schedule .app.start to run.
|
||||
mach.mods.schedule(.app, .start);
|
||||
|
||||
// If desired, it is possible to observe when the app has finished starting by dispatching
|
||||
// systems until the app has started:
|
||||
try mach.mods.dispatchUntil(.mach_core, .started);
|
||||
|
||||
// On some platforms, you can drive the mach.Core main loop yourself - but this isn't
|
||||
// possible on all platforms.
|
||||
// On some platforms, you can drive the mach.Core main loop yourself - but this isn't possible
|
||||
// on all platforms. If mach.Core.non_blocking is set to true, and the platform supports
|
||||
// non-blocking mode, then .mach_core.main will return without blocking. Otherwise it will block
|
||||
// forever and app.run(.main) will never return.
|
||||
if (mach.Core.supports_non_blocking) {
|
||||
defer mods.deinit(allocator);
|
||||
|
||||
mach.Core.non_blocking = true;
|
||||
while (mach.mods.mod.mach_core.state().state != .exited) {
|
||||
// Execute systems until a frame has been finished.
|
||||
try mach.mods.dispatchUntil(.mach_core, .frame_finished);
|
||||
|
||||
const app = mods.get(.app);
|
||||
app.run(.main);
|
||||
|
||||
// If you are driving the main loop yourself, you should call tick until exit.
|
||||
const core = mods.get(.mach_core);
|
||||
while (mods.mods.mach_core.state != .exited) {
|
||||
core.run(.tick);
|
||||
}
|
||||
} else {
|
||||
// On platforms where you cannot control the mach.Core main loop, the .mach_core.start
|
||||
// system your app schedules will block forever and the function call below will NEVER
|
||||
// return (std.process.exit will occur first.)
|
||||
//
|
||||
// In this case we can just dispatch systems until there are no more left to execute, which
|
||||
// conviently works even if you aren't using mach.Core in your program.
|
||||
try mach.mods.dispatch(.{});
|
||||
const app = mods.get(.app);
|
||||
app.run(.main);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,35 +1,32 @@
|
|||
const mach = @import("mach");
|
||||
const gpu = mach.gpu;
|
||||
|
||||
pub const name = .app;
|
||||
pub const Mod = mach.Mod(@This());
|
||||
const App = @This();
|
||||
|
||||
pub const systems = .{
|
||||
.start = .{ .handler = start },
|
||||
.init = .{ .handler = init },
|
||||
.deinit = .{ .handler = deinit },
|
||||
.tick = .{ .handler = tick },
|
||||
};
|
||||
pub const mach_module = .app;
|
||||
|
||||
pub const mach_systems = .{ .main, .init, .tick, .deinit };
|
||||
|
||||
pub const main = mach.schedule(.{
|
||||
.{ mach.Core, .init },
|
||||
.{ App, .init },
|
||||
.{ mach.Core, .main },
|
||||
});
|
||||
|
||||
title_timer: mach.time.Timer,
|
||||
pipeline: *gpu.RenderPipeline,
|
||||
|
||||
pub fn deinit(core: *mach.Core.Mod, app: *Mod) void {
|
||||
app.state().pipeline.release();
|
||||
core.schedule(.deinit);
|
||||
}
|
||||
|
||||
fn start(app: *Mod, core: *mach.Core.Mod) !void {
|
||||
core.schedule(.init);
|
||||
app.schedule(.init);
|
||||
}
|
||||
|
||||
fn init(app: *Mod, core: *mach.Core.Mod) !void {
|
||||
core.state().on_tick = app.system(.tick);
|
||||
core.state().on_exit = app.system(.deinit);
|
||||
pub fn init(
|
||||
core: *mach.Core,
|
||||
app: *App,
|
||||
app_tick: mach.Call(App, .tick),
|
||||
app_deinit: mach.Call(App, .deinit),
|
||||
) !void {
|
||||
core.on_tick = app_tick.id;
|
||||
core.on_exit = app_deinit.id;
|
||||
|
||||
// Create our shader module
|
||||
const shader_module = core.state().device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl"));
|
||||
const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl"));
|
||||
defer shader_module.release();
|
||||
|
||||
// Blend state describes how rendered colors get blended
|
||||
|
|
@ -37,7 +34,7 @@ fn init(app: *Mod, core: *mach.Core.Mod) !void {
|
|||
|
||||
// Color target describes e.g. the pixel format of the window we are rendering to.
|
||||
const color_target = gpu.ColorTargetState{
|
||||
.format = core.get(core.state().main_window, .framebuffer_format).?,
|
||||
.format = core.windows.get(core.main_window).?.framebuffer_format,
|
||||
.blend = &blend,
|
||||
};
|
||||
|
||||
|
|
@ -49,7 +46,7 @@ fn init(app: *Mod, core: *mach.Core.Mod) !void {
|
|||
});
|
||||
|
||||
// Create our render pipeline that will ultimately get pixels onto the screen.
|
||||
const label = @tagName(name) ++ ".init";
|
||||
const label = @tagName(mach_module) ++ ".init";
|
||||
const pipeline_descriptor = gpu.RenderPipeline.Descriptor{
|
||||
.label = label,
|
||||
.fragment = &fragment,
|
||||
|
|
@ -58,34 +55,33 @@ fn init(app: *Mod, core: *mach.Core.Mod) !void {
|
|||
.entry_point = "vertex_main",
|
||||
},
|
||||
};
|
||||
const pipeline = core.state().device.createRenderPipeline(&pipeline_descriptor);
|
||||
const pipeline = core.device.createRenderPipeline(&pipeline_descriptor);
|
||||
|
||||
// Store our render pipeline in our module's state, so we can access it later on.
|
||||
app.init(.{
|
||||
.title_timer = try mach.time.Timer.start(),
|
||||
.pipeline = pipeline,
|
||||
});
|
||||
try updateWindowTitle(core);
|
||||
// TODO(object): module-state-init
|
||||
app.title_timer = try mach.time.Timer.start();
|
||||
app.pipeline = pipeline;
|
||||
|
||||
core.schedule(.start);
|
||||
// TODO(object): window-title
|
||||
// try updateWindowTitle(core);
|
||||
}
|
||||
|
||||
fn tick(core: *mach.Core.Mod, app: *Mod) !void {
|
||||
while (core.state().nextEvent()) |event| {
|
||||
pub fn tick(app: *App, core: *mach.Core) void {
|
||||
while (core.nextEvent()) |event| {
|
||||
switch (event) {
|
||||
.close => core.schedule(.exit), // Tell mach.Core to exit the app
|
||||
.close => core.exit(),
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
// Grab the back buffer of the swapchain
|
||||
// TODO(Core)
|
||||
const back_buffer_view = core.state().swap_chain.getCurrentTextureView().?;
|
||||
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
|
||||
defer back_buffer_view.release();
|
||||
|
||||
// Create a command encoder
|
||||
const label = @tagName(name) ++ ".tick";
|
||||
const encoder = core.state().device.createCommandEncoder(&.{ .label = label });
|
||||
const label = @tagName(mach_module) ++ ".tick";
|
||||
const encoder = core.device.createCommandEncoder(&.{ .label = label });
|
||||
defer encoder.release();
|
||||
|
||||
// Begin render pass
|
||||
|
|
@ -103,7 +99,7 @@ fn tick(core: *mach.Core.Mod, app: *Mod) !void {
|
|||
defer render_pass.release();
|
||||
|
||||
// Draw
|
||||
render_pass.setPipeline(app.state().pipeline);
|
||||
render_pass.setPipeline(app.pipeline);
|
||||
render_pass.draw(3, 1, 0, 0);
|
||||
|
||||
// Finish render pass
|
||||
|
|
@ -112,27 +108,30 @@ fn tick(core: *mach.Core.Mod, app: *Mod) !void {
|
|||
// Submit our commands to the queue
|
||||
var command = encoder.finish(&.{ .label = label });
|
||||
defer command.release();
|
||||
core.state().queue.submit(&[_]*gpu.CommandBuffer{command});
|
||||
|
||||
// Present the frame
|
||||
core.schedule(.present_frame);
|
||||
core.queue.submit(&[_]*gpu.CommandBuffer{command});
|
||||
|
||||
// update the window title every second
|
||||
if (app.state().title_timer.read() >= 1.0) {
|
||||
app.state().title_timer.reset();
|
||||
try updateWindowTitle(core);
|
||||
if (app.title_timer.read() >= 1.0) {
|
||||
app.title_timer.reset();
|
||||
// TODO(object): window-title
|
||||
// try updateWindowTitle(core);
|
||||
}
|
||||
}
|
||||
|
||||
fn updateWindowTitle(core: *mach.Core.Mod) !void {
|
||||
try core.state().printTitle(
|
||||
core.state().main_window,
|
||||
"core-custom-entrypoint [ {d}fps ] [ Input {d}hz ]",
|
||||
.{
|
||||
// TODO(Core)
|
||||
core.state().frameRate(),
|
||||
core.state().inputRate(),
|
||||
},
|
||||
);
|
||||
core.schedule(.update);
|
||||
pub fn deinit(app: *App) void {
|
||||
app.pipeline.release();
|
||||
}
|
||||
|
||||
// TODO(object): window-title
|
||||
// fn updateWindowTitle(core: *mach.Core) !void {
|
||||
// try core.printTitle(
|
||||
// core.main_window,
|
||||
// "core-custom-entrypoint [ {d}fps ] [ Input {d}hz ]",
|
||||
// .{
|
||||
// // TODO(Core)
|
||||
// core.frameRate(),
|
||||
// core.inputRate(),
|
||||
// },
|
||||
// );
|
||||
// core.schedule(.update);
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -1,23 +1,21 @@
|
|||
const std = @import("std");
|
||||
const mach = @import("mach");
|
||||
|
||||
// The global list of Mach modules our application may use.
|
||||
pub const modules = .{
|
||||
// The set of Mach modules our application may use.
|
||||
const Modules = mach.Modules(.{
|
||||
mach.Core,
|
||||
@import("App.zig"),
|
||||
};
|
||||
});
|
||||
|
||||
// TODO: move this to a mach "entrypoint" zig module which handles nuances like WASM requires.
|
||||
pub fn main() !void {
|
||||
const allocator = std.heap.c_allocator;
|
||||
|
||||
// Initialize module system
|
||||
try mach.mods.init(allocator);
|
||||
// The set of Mach modules our application may use.
|
||||
var mods = Modules.init(allocator);
|
||||
// TODO: enable mods.deinit(allocator); for allocator leak detection
|
||||
// defer mods.deinit(allocator);
|
||||
|
||||
// Schedule .app.start to run.
|
||||
mach.mods.schedule(.app, .start);
|
||||
|
||||
// Dispatch systems forever or until there are none left to dispatch. If your app uses mach.Core
|
||||
// then this will block forever and never return.
|
||||
try mach.mods.dispatch(.{});
|
||||
const app = mods.get(.app);
|
||||
app.run(.main);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,12 @@ const vec2 = math.vec2;
|
|||
const Vec2 = math.Vec2;
|
||||
const Vec3 = math.Vec3;
|
||||
|
||||
const App = @This();
|
||||
|
||||
pub const mach_module = .app;
|
||||
|
||||
pub const mach_systems = .{ .start, .init, .deinit, .tick };
|
||||
|
||||
// Global state for our game module.
|
||||
timer: mach.time.Timer,
|
||||
player: mach.EntityID,
|
||||
|
|
@ -21,32 +27,14 @@ pub const components = .{
|
|||
.follower = .{ .type = void },
|
||||
};
|
||||
|
||||
pub const systems = .{
|
||||
.start = .{ .handler = start },
|
||||
.init = .{ .handler = init },
|
||||
.deinit = .{ .handler = deinit },
|
||||
.tick = .{ .handler = tick },
|
||||
};
|
||||
|
||||
// Define the globally unique name of our module. You can use any name here, but keep in mind no
|
||||
// two modules in the program can have the same name.
|
||||
pub const name = .app;
|
||||
|
||||
// The mach.Mod type corresponding to our module struct (this file.) This provides methods for
|
||||
// working with this module (e.g. sending events, working with its components, etc.)
|
||||
//
|
||||
// Note that Mod.state() returns an instance of our module struct.
|
||||
pub const Mod = mach.Mod(@This());
|
||||
|
||||
pub fn deinit(core: *mach.Core.Mod, renderer: *Renderer.Mod) void {
|
||||
pub fn deinit(renderer: *Renderer) void {
|
||||
renderer.schedule(.deinit);
|
||||
core.schedule(.deinit);
|
||||
}
|
||||
|
||||
fn start(
|
||||
core: *mach.Core.Mod,
|
||||
renderer: *Renderer.Mod,
|
||||
app: *Mod,
|
||||
core: *mach.Core,
|
||||
renderer: *Renderer,
|
||||
app: *App,
|
||||
) !void {
|
||||
core.schedule(.init);
|
||||
renderer.schedule(.init);
|
||||
|
|
@ -58,18 +46,20 @@ fn init(
|
|||
// of the program we can have these types injected here, letting us work with other modules in
|
||||
// our program seamlessly and with a type-safe API:
|
||||
entities: *mach.Entities.Mod,
|
||||
core: *mach.Core.Mod,
|
||||
renderer: *Renderer.Mod,
|
||||
app: *Mod,
|
||||
core: *mach.Core,
|
||||
renderer: *Renderer,
|
||||
app: *App,
|
||||
app_tick: mach.Call(App, .tick),
|
||||
app_deinit: mach.Call(App, .deinit),
|
||||
) !void {
|
||||
core.state().on_tick = app.system(.tick);
|
||||
core.state().on_exit = app.system(.deinit);
|
||||
core.on_tick = app_tick.id;
|
||||
core.on_exit = app_deinit.id;
|
||||
|
||||
// Create our player entity.
|
||||
const player = try entities.new();
|
||||
|
||||
// Give our player entity a .renderer.position and .renderer.scale component. Note that these
|
||||
// are defined by the Renderer module, so we use `renderer: *Renderer.Mod` to interact with
|
||||
// are defined by the Renderer module, so we use `renderer: *Renderer` to interact with
|
||||
// them.
|
||||
//
|
||||
// Components live in a module's namespace, so e.g. a physics2d module and renderer3d module could
|
||||
|
|
@ -79,26 +69,24 @@ fn init(
|
|||
try renderer.set(player, .scale, 1.0);
|
||||
|
||||
// Initialize our game module's state - these are the struct fields defined at the top of this
|
||||
// file. If this is not done, then app.state() will panic indicating the state was never
|
||||
// file. If this is not done, then app. will panic indicating the state was never
|
||||
// initialized.
|
||||
app.init(.{
|
||||
.timer = try mach.time.Timer.start(),
|
||||
.spawn_timer = try mach.time.Timer.start(),
|
||||
.player = player,
|
||||
});
|
||||
|
||||
core.schedule(.start);
|
||||
}
|
||||
|
||||
fn tick(
|
||||
entities: *mach.Entities.Mod,
|
||||
core: *mach.Core.Mod,
|
||||
renderer: *Renderer.Mod,
|
||||
app: *Mod,
|
||||
core: *mach.Core,
|
||||
renderer: *Renderer,
|
||||
app: *App,
|
||||
) !void {
|
||||
var direction = app.state().direction;
|
||||
var spawning = app.state().spawning;
|
||||
while (core.state().nextEvent()) |event| {
|
||||
var direction = app.direction;
|
||||
var spawning = app.spawning;
|
||||
while (core.nextEvent()) |event| {
|
||||
switch (event) {
|
||||
.key_press => |ev| {
|
||||
switch (ev.key) {
|
||||
|
|
@ -120,7 +108,7 @@ fn tick(
|
|||
else => {},
|
||||
}
|
||||
},
|
||||
.close => core.schedule(.exit), // Send an event telling mach to exit the app
|
||||
.close => core.exit(),
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
|
@ -128,18 +116,18 @@ fn tick(
|
|||
// Keep track of which direction we want the player to move based on input, and whether we want
|
||||
// to be spawning entities.
|
||||
//
|
||||
// Note that app.state() simply returns a pointer to a global singleton of the struct defined
|
||||
// Note that app. simply returns a pointer to a global singleton of the struct defined
|
||||
// by this file, so we can access fields defined at the top of this file.
|
||||
app.state().direction = direction;
|
||||
app.state().spawning = spawning;
|
||||
app.direction = direction;
|
||||
app.spawning = spawning;
|
||||
|
||||
// Get the current player position
|
||||
var player_pos = renderer.get(app.state().player, .position).?;
|
||||
var player_pos = renderer.get(app.player, .position).?;
|
||||
|
||||
// If we want to spawn new entities, then spawn them now. The timer just makes spawning rate
|
||||
// independent of frame rate.
|
||||
if (spawning and app.state().spawn_timer.read() > 1.0 / 60.0) {
|
||||
_ = app.state().spawn_timer.lap(); // Reset the timer
|
||||
if (spawning and app.spawn_timer.read() > 1.0 / 60.0) {
|
||||
_ = app.spawn_timer.lap(); // Reset the timer
|
||||
for (0..5) |_| {
|
||||
// Spawn a new entity at the same position as the player, but smaller in scale.
|
||||
const new_entity = try entities.new();
|
||||
|
|
@ -152,14 +140,14 @@ fn tick(
|
|||
}
|
||||
|
||||
// Multiply by delta_time to ensure that movement is the same speed regardless of the frame rate.
|
||||
const delta_time = app.state().timer.lap();
|
||||
const delta_time = app.timer.lap();
|
||||
|
||||
// Calculate the player position, by moving in the direction the player wants to go
|
||||
// by the speed amount.
|
||||
const speed = 1.0;
|
||||
player_pos.v[0] += direction.x() * speed * delta_time;
|
||||
player_pos.v[1] += direction.y() * speed * delta_time;
|
||||
try renderer.set(app.state().player, .position, player_pos);
|
||||
try renderer.set(app.player, .position, player_pos);
|
||||
|
||||
// Query all the entities that have the .follower tag indicating they should follow the player.
|
||||
// TODO(important): better querying API
|
||||
|
|
@ -168,7 +156,7 @@ fn tick(
|
|||
var q = try entities.query(.{
|
||||
.ids = mach.Entities.Mod.read(.id),
|
||||
.followers = Mod.read(.follower),
|
||||
.positions = Renderer.Mod.write(.position),
|
||||
.positions = Renderer.write(.position),
|
||||
});
|
||||
while (q.next()) |v| {
|
||||
for (v.ids, v.positions) |id, *position| {
|
||||
|
|
@ -182,7 +170,7 @@ fn tick(
|
|||
var q2 = try entities.query(.{
|
||||
.ids = mach.Entities.Mod.read(.id),
|
||||
.followers = Mod.read(.follower),
|
||||
.positions = Renderer.Mod.read(.position),
|
||||
.positions = Renderer.read(.position),
|
||||
});
|
||||
while (q2.next()) |v2| {
|
||||
for (v2.ids, v2.positions) |other_id, other_position| {
|
||||
|
|
|
|||
|
|
@ -13,8 +13,7 @@ pipeline: *gpu.RenderPipeline,
|
|||
bind_groups: [num_bind_groups]*gpu.BindGroup,
|
||||
uniform_buffer: *gpu.Buffer,
|
||||
|
||||
pub const name = .renderer;
|
||||
pub const Mod = mach.Mod(@This());
|
||||
pub const mach_module = .renderer;
|
||||
|
||||
pub const components = .{
|
||||
.position = .{ .type = Vec3 },
|
||||
|
|
@ -22,11 +21,7 @@ pub const components = .{
|
|||
.scale = .{ .type = f32 },
|
||||
};
|
||||
|
||||
pub const systems = .{
|
||||
.init = .{ .handler = init },
|
||||
.deinit = .{ .handler = deinit },
|
||||
.render_frame = .{ .handler = renderFrame },
|
||||
};
|
||||
pub const mach_systems = .{ .init, .deinit, .render_frame };
|
||||
|
||||
const UniformBufferObject = extern struct {
|
||||
offset: Vec3,
|
||||
|
|
@ -34,17 +29,17 @@ const UniformBufferObject = extern struct {
|
|||
};
|
||||
|
||||
fn init(
|
||||
core: *mach.Core.Mod,
|
||||
core: *mach.Core,
|
||||
renderer: *Mod,
|
||||
) !void {
|
||||
const device = core.state().device;
|
||||
const device = core.device;
|
||||
const shader_module = device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl"));
|
||||
defer shader_module.release();
|
||||
|
||||
// Fragment state
|
||||
const blend = gpu.BlendState{};
|
||||
const color_target = gpu.ColorTargetState{
|
||||
.format = core.get(core.state().main_window, .framebuffer_format).?,
|
||||
.format = core.windows.get(core.main_window).?.framebuffer_format,
|
||||
.blend = &blend,
|
||||
.write_mask = gpu.ColorWriteMaskFlags.all,
|
||||
};
|
||||
|
|
@ -54,7 +49,7 @@ fn init(
|
|||
.targets = &.{color_target},
|
||||
});
|
||||
|
||||
const label = @tagName(name) ++ ".init";
|
||||
const label = @tagName(mach_module) ++ ".init";
|
||||
const uniform_buffer = device.createBuffer(&.{
|
||||
.label = label ++ " uniform buffer",
|
||||
.usage = .{ .copy_dst = true, .uniform = true },
|
||||
|
|
@ -116,17 +111,17 @@ fn deinit(
|
|||
|
||||
fn renderFrame(
|
||||
entities: *mach.Entities.Mod,
|
||||
core: *mach.Core.Mod,
|
||||
core: *mach.Core,
|
||||
renderer: *Mod,
|
||||
) !void {
|
||||
// Grab the back buffer of the swapchain
|
||||
// TODO(Core)
|
||||
const back_buffer_view = core.state().swap_chain.getCurrentTextureView().?;
|
||||
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
|
||||
defer back_buffer_view.release();
|
||||
|
||||
// Create a command encoder
|
||||
const label = @tagName(name) ++ ".tick";
|
||||
const encoder = core.state().device.createCommandEncoder(&.{ .label = label });
|
||||
const label = @tagName(mach_module) ++ ".tick";
|
||||
const encoder = core.device.createCommandEncoder(&.{ .label = label });
|
||||
defer encoder.release();
|
||||
|
||||
// Update uniform buffer
|
||||
|
|
@ -173,8 +168,5 @@ fn renderFrame(
|
|||
// Submit our commands to the queue
|
||||
var command = encoder.finish(&.{ .label = label });
|
||||
defer command.release();
|
||||
core.state().queue.submit(&[_]*gpu.CommandBuffer{command});
|
||||
|
||||
// Present the frame
|
||||
core.schedule(.present_frame);
|
||||
core.queue.submit(&[_]*gpu.CommandBuffer{command});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,22 @@
|
|||
const std = @import("std");
|
||||
const mach = @import("mach");
|
||||
|
||||
// The global list of Mach modules registered for use in our application.
|
||||
pub const modules = .{
|
||||
// The set of Mach modules our application may use.
|
||||
const Modules = mach.Modules(.{
|
||||
mach.Core,
|
||||
@import("App.zig"),
|
||||
@import("Renderer.zig"),
|
||||
};
|
||||
});
|
||||
|
||||
// TODO: move this to a mach "entrypoint" zig module which handles nuances like WASM requires.
|
||||
pub fn main() !void {
|
||||
const allocator = std.heap.c_allocator;
|
||||
|
||||
// Initialize module system
|
||||
try mach.mods.init(allocator);
|
||||
// The set of Mach modules our application may use.
|
||||
var mods = Modules.init(allocator);
|
||||
// TODO: enable mods.deinit(allocator); for allocator leak detection
|
||||
// defer mods.deinit(allocator);
|
||||
|
||||
// Schedule .app.start to run.
|
||||
mach.mods.schedule(.app, .start);
|
||||
|
||||
// Dispatch systems forever or until there are none left to dispatch. If your app uses mach.Core
|
||||
// then this will block forever and never return.
|
||||
try mach.mods.dispatch(.{});
|
||||
const app = mods.get(.app);
|
||||
app.run(.main);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,12 @@ const Mat4x4 = math.Mat4x4;
|
|||
|
||||
const Glyphs = @import("Glyphs.zig");
|
||||
|
||||
const App = @This();
|
||||
|
||||
pub const mach_module = .app;
|
||||
|
||||
pub const mach_systems = .{ .start, .init, .deinit, .tick, .end_frame };
|
||||
|
||||
timer: mach.time.Timer,
|
||||
player: mach.EntityID,
|
||||
direction: Vec2 = vec2(0, 0),
|
||||
|
|
@ -26,26 +32,12 @@ pipeline: mach.EntityID,
|
|||
frame_encoder: *gpu.CommandEncoder = undefined,
|
||||
frame_render_pass: *gpu.RenderPassEncoder = undefined,
|
||||
|
||||
// Define the globally unique name of our module. You can use any name here, but keep in mind no
|
||||
// two modules in the program can have the same name.
|
||||
pub const name = .app;
|
||||
pub const Mod = mach.Mod(@This());
|
||||
|
||||
pub const systems = .{
|
||||
.start = .{ .handler = start },
|
||||
.init = .{ .handler = init },
|
||||
.deinit = .{ .handler = deinit },
|
||||
.tick = .{ .handler = tick },
|
||||
.end_frame = .{ .handler = endFrame },
|
||||
};
|
||||
|
||||
fn deinit(core: *mach.Core.Mod, sprite_pipeline: *gfx.SpritePipeline.Mod, glyphs: *Glyphs.Mod) !void {
|
||||
fn deinit(sprite_pipeline: *gfx.SpritePipeline.Mod, glyphs: *Glyphs.Mod) !void {
|
||||
sprite_pipeline.schedule(.deinit);
|
||||
glyphs.schedule(.deinit);
|
||||
core.schedule(.deinit);
|
||||
}
|
||||
|
||||
fn start(core: *mach.Core.Mod, sprite_pipeline: *gfx.SpritePipeline.Mod, glyphs: *Glyphs.Mod, app: *Mod) !void {
|
||||
fn start(core: *mach.Core, sprite_pipeline: *gfx.SpritePipeline.Mod, glyphs: *Glyphs.Mod, app: *App) !void {
|
||||
core.schedule(.init);
|
||||
sprite_pipeline.schedule(.init);
|
||||
glyphs.schedule(.init);
|
||||
|
|
@ -62,11 +54,13 @@ fn init(
|
|||
sprite: *gfx.Sprite.Mod,
|
||||
sprite_pipeline: *gfx.SpritePipeline.Mod,
|
||||
glyphs: *Glyphs.Mod,
|
||||
app: *Mod,
|
||||
core: *mach.Core.Mod,
|
||||
app: *App,
|
||||
core: *mach.Core,
|
||||
app_tick: mach.Call(App, .tick),
|
||||
app_deinit: mach.Call(App, .deinit),
|
||||
) !void {
|
||||
core.state().on_tick = app.system(.tick);
|
||||
core.state().on_exit = app.system(.deinit);
|
||||
core.on_tick = app_tick.id;
|
||||
core.on_exit = app_deinit.id;
|
||||
|
||||
// Create a sprite rendering pipeline
|
||||
const texture = glyphs.state().texture;
|
||||
|
|
@ -98,21 +92,19 @@ fn init(
|
|||
.time = 0,
|
||||
.pipeline = pipeline,
|
||||
});
|
||||
|
||||
core.schedule(.start);
|
||||
}
|
||||
|
||||
fn tick(
|
||||
entities: *mach.Entities.Mod,
|
||||
core: *mach.Core.Mod,
|
||||
core: *mach.Core,
|
||||
sprite: *gfx.Sprite.Mod,
|
||||
sprite_pipeline: *gfx.SpritePipeline.Mod,
|
||||
glyphs: *Glyphs.Mod,
|
||||
app: *Mod,
|
||||
app: *App,
|
||||
) !void {
|
||||
var direction = app.state().direction;
|
||||
var spawning = app.state().spawning;
|
||||
while (core.state().nextEvent()) |event| {
|
||||
var direction = app.direction;
|
||||
var spawning = app.spawning;
|
||||
while (core.nextEvent()) |event| {
|
||||
switch (event) {
|
||||
.key_press => |ev| {
|
||||
switch (ev.key) {
|
||||
|
|
@ -134,37 +126,37 @@ fn tick(
|
|||
else => {},
|
||||
}
|
||||
},
|
||||
.close => core.schedule(.exit),
|
||||
.close => core.exit(),
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
app.state().direction = direction;
|
||||
app.state().spawning = spawning;
|
||||
app.direction = direction;
|
||||
app.spawning = spawning;
|
||||
|
||||
var player_transform = sprite.get(app.state().player, .transform).?;
|
||||
var player_transform = sprite.get(app.player, .transform).?;
|
||||
var player_pos = player_transform.translation();
|
||||
if (!spawning and app.state().spawn_timer.read() > 1.0 / 60.0) {
|
||||
if (!spawning and app.spawn_timer.read() > 1.0 / 60.0) {
|
||||
// Spawn new entities
|
||||
_ = app.state().spawn_timer.lap();
|
||||
_ = app.spawn_timer.lap();
|
||||
for (0..50) |_| {
|
||||
var new_pos = player_pos;
|
||||
new_pos.v[0] += app.state().rand.random().floatNorm(f32) * 25;
|
||||
new_pos.v[1] += app.state().rand.random().floatNorm(f32) * 25;
|
||||
new_pos.v[0] += app.rand.random().floatNorm(f32) * 25;
|
||||
new_pos.v[1] += app.rand.random().floatNorm(f32) * 25;
|
||||
|
||||
const rand_index = app.state().rand.random().intRangeAtMost(usize, 0, glyphs.state().regions.count() - 1);
|
||||
const rand_index = app.rand.random().intRangeAtMost(usize, 0, glyphs.state().regions.count() - 1);
|
||||
const r = glyphs.state().regions.entries.get(rand_index).value;
|
||||
|
||||
const new_entity = try entities.new();
|
||||
try sprite.set(new_entity, .transform, Mat4x4.translate(new_pos).mul(&Mat4x4.scaleScalar(0.3)));
|
||||
try sprite.set(new_entity, .size, vec2(@floatFromInt(r.width), @floatFromInt(r.height)));
|
||||
try sprite.set(new_entity, .uv_transform, Mat3x3.translate(vec2(@floatFromInt(r.x), @floatFromInt(r.y))));
|
||||
try sprite.set(new_entity, .pipeline, app.state().pipeline);
|
||||
app.state().sprites += 1;
|
||||
try sprite.set(new_entity, .pipeline, app.pipeline);
|
||||
app.sprites += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Multiply by delta_time to ensure that movement is the same speed regardless of the frame rate.
|
||||
const delta_time = app.state().timer.lap();
|
||||
const delta_time = app.timer.lap();
|
||||
|
||||
// Animate entities
|
||||
var q = try entities.query(.{
|
||||
|
|
@ -176,17 +168,17 @@ fn tick(
|
|||
var location = entity_transform.translation();
|
||||
// TODO: formatting
|
||||
// TODO(Core)
|
||||
if (location.x() < -@as(f32, @floatFromInt(core.state().size().width)) / 1.5 or location.x() > @as(f32, @floatFromInt(core.state().size().width)) / 1.5 or location.y() < -@as(f32, @floatFromInt(core.state().size().height)) / 1.5 or location.y() > @as(f32, @floatFromInt(core.state().size().height)) / 1.5) {
|
||||
if (location.x() < -@as(f32, @floatFromInt(core.size().width)) / 1.5 or location.x() > @as(f32, @floatFromInt(core.size().width)) / 1.5 or location.y() < -@as(f32, @floatFromInt(core.size().height)) / 1.5 or location.y() > @as(f32, @floatFromInt(core.size().height)) / 1.5) {
|
||||
try entities.remove(id);
|
||||
app.state().sprites -= 1;
|
||||
app.sprites -= 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
var transform = Mat4x4.ident;
|
||||
transform = transform.mul(&Mat4x4.scale(Vec3.splat(1.0 + (0.2 * delta_time))));
|
||||
transform = transform.mul(&Mat4x4.translate(location));
|
||||
transform = transform.mul(&Mat4x4.rotateZ(2 * math.pi * app.state().time));
|
||||
transform = transform.mul(&Mat4x4.scale(Vec3.splat(@max(math.cos(app.state().time / 2.0), 0.2))));
|
||||
transform = transform.mul(&Mat4x4.rotateZ(2 * math.pi * app.time));
|
||||
transform = transform.mul(&Mat4x4.scale(Vec3.splat(@max(math.cos(app.time / 2.0), 0.2))));
|
||||
entity_transform.* = transform;
|
||||
}
|
||||
}
|
||||
|
|
@ -199,19 +191,19 @@ fn tick(
|
|||
player_transform = Mat4x4.translate(player_pos).mul(
|
||||
&Mat4x4.scale(Vec3.splat(1.0)),
|
||||
);
|
||||
try sprite.set(app.state().player, .transform, player_transform);
|
||||
try sprite.set(app.player, .transform, player_transform);
|
||||
sprite.schedule(.update);
|
||||
|
||||
// Perform pre-render work
|
||||
sprite_pipeline.schedule(.pre_render);
|
||||
|
||||
// Create a command encoder for this frame
|
||||
const label = @tagName(name) ++ ".tick";
|
||||
app.state().frame_encoder = core.state().device.createCommandEncoder(&.{ .label = label });
|
||||
const label = @tagName(mach_module) ++ ".tick";
|
||||
app.frame_encoder = core.device.createCommandEncoder(&.{ .label = label });
|
||||
|
||||
// Grab the back buffer of the swapchain
|
||||
// TODO(Core)
|
||||
const back_buffer_view = core.state().swap_chain.getCurrentTextureView().?;
|
||||
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
|
||||
defer back_buffer_view.release();
|
||||
|
||||
// Begin render pass
|
||||
|
|
@ -222,44 +214,41 @@ fn tick(
|
|||
.load_op = .clear,
|
||||
.store_op = .store,
|
||||
}};
|
||||
app.state().frame_render_pass = app.state().frame_encoder.beginRenderPass(&gpu.RenderPassDescriptor.init(.{
|
||||
app.frame_render_pass = app.frame_encoder.beginRenderPass(&gpu.RenderPassDescriptor.init(.{
|
||||
.label = label,
|
||||
.color_attachments = &color_attachments,
|
||||
}));
|
||||
|
||||
// Render our sprite batch
|
||||
sprite_pipeline.state().render_pass = app.state().frame_render_pass;
|
||||
sprite_pipeline.state().render_pass = app.frame_render_pass;
|
||||
sprite_pipeline.schedule(.render);
|
||||
|
||||
// Finish the frame once rendering is done.
|
||||
app.schedule(.end_frame);
|
||||
|
||||
app.state().time += delta_time;
|
||||
app.time += delta_time;
|
||||
}
|
||||
|
||||
fn endFrame(app: *Mod, core: *mach.Core.Mod) !void {
|
||||
fn endFrame(app: *App, core: *mach.Core) !void {
|
||||
// Finish render pass
|
||||
app.state().frame_render_pass.end();
|
||||
const label = @tagName(name) ++ ".endFrame";
|
||||
var command = app.state().frame_encoder.finish(&.{ .label = label });
|
||||
core.state().queue.submit(&[_]*gpu.CommandBuffer{command});
|
||||
app.frame_render_pass.end();
|
||||
const label = @tagName(mach_module) ++ ".endFrame";
|
||||
var command = app.frame_encoder.finish(&.{ .label = label });
|
||||
core.queue.submit(&[_]*gpu.CommandBuffer{command});
|
||||
command.release();
|
||||
app.state().frame_encoder.release();
|
||||
app.state().frame_render_pass.release();
|
||||
|
||||
// Present the frame
|
||||
core.schedule(.present_frame);
|
||||
app.frame_encoder.release();
|
||||
app.frame_render_pass.release();
|
||||
|
||||
// Every second, update the window title with the FPS
|
||||
if (app.state().fps_timer.read() >= 1.0) {
|
||||
try core.state().printTitle(
|
||||
core.state().main_window,
|
||||
if (app.fps_timer.read() >= 1.0) {
|
||||
try core.printTitle(
|
||||
core.main_window,
|
||||
"glyphs [ FPS: {d} ] [ Sprites: {d} ]",
|
||||
.{ app.state().frame_count, app.state().sprites },
|
||||
.{ app.frame_count, app.sprites },
|
||||
);
|
||||
core.schedule(.update);
|
||||
app.state().fps_timer.reset();
|
||||
app.state().frame_count = 0;
|
||||
app.fps_timer.reset();
|
||||
app.frame_count = 0;
|
||||
}
|
||||
app.state().frame_count += 1;
|
||||
app.frame_count += 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,14 +4,9 @@ const freetype = @import("freetype");
|
|||
const std = @import("std");
|
||||
const assets = @import("assets");
|
||||
|
||||
pub const name = .glyphs;
|
||||
pub const Mod = mach.Mod(@This());
|
||||
pub const mach_module = .glyphs;
|
||||
|
||||
pub const systems = .{
|
||||
.init = .{ .handler = init },
|
||||
.deinit = .{ .handler = deinit },
|
||||
.prepare = .{ .handler = prepare },
|
||||
};
|
||||
pub const mach_systems = .{ .init, .deinit, .prepare };
|
||||
|
||||
const RegionMap = std.AutoArrayHashMapUnmanaged(u21, mach.gfx.Atlas.Region);
|
||||
|
||||
|
|
@ -35,17 +30,17 @@ fn deinit(glyphs: *Mod) !void {
|
|||
}
|
||||
|
||||
fn init(
|
||||
core: *mach.Core.Mod,
|
||||
core: *mach.Core,
|
||||
glyphs: *Mod,
|
||||
) !void {
|
||||
const device = core.state().device;
|
||||
const device = core.device;
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
// rgba32_pixels
|
||||
const img_size = gpu.Extent3D{ .width = 1024, .height = 1024 };
|
||||
|
||||
// Create a GPU texture
|
||||
const label = @tagName(name) ++ ".init";
|
||||
const label = @tagName(mach_module) ++ ".init";
|
||||
const texture = device.createTexture(&.{
|
||||
.label = label,
|
||||
.size = img_size,
|
||||
|
|
@ -75,7 +70,7 @@ fn init(
|
|||
});
|
||||
}
|
||||
|
||||
fn prepare(core: *mach.Core.Mod, glyphs: *Mod) !void {
|
||||
fn prepare(core: *mach.Core, glyphs: *Mod) !void {
|
||||
var s = glyphs.state();
|
||||
|
||||
// Prepare which glyphs we will render
|
||||
|
|
@ -124,5 +119,5 @@ fn prepare(core: *mach.Core.Mod, glyphs: *Mod) !void {
|
|||
.bytes_per_row = @as(u32, @intCast(img_size.width * 4)),
|
||||
.rows_per_image = @as(u32, @intCast(img_size.height)),
|
||||
};
|
||||
core.state().queue.writeTexture(&.{ .texture = s.texture }, &data_layout, &img_size, s.texture_atlas.data);
|
||||
core.queue.writeTexture(&.{ .texture = s.texture }, &data_layout, &img_size, s.texture_atlas.data);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,23 @@
|
|||
const std = @import("std");
|
||||
const mach = @import("mach");
|
||||
|
||||
// The global list of Mach modules our application may use.
|
||||
pub const modules = .{
|
||||
// The set of Mach modules our application may use.
|
||||
const Modules = mach.Modules(.{
|
||||
mach.Core,
|
||||
mach.gfx.sprite_modules,
|
||||
@import("App.zig"),
|
||||
@import("Glyphs.zig"),
|
||||
};
|
||||
});
|
||||
|
||||
// TODO: move this to a mach "entrypoint" zig module which handles nuances like WASM requires.
|
||||
pub fn main() !void {
|
||||
const allocator = std.heap.c_allocator;
|
||||
|
||||
// Initialize module system
|
||||
try mach.mods.init(allocator);
|
||||
// The set of Mach modules our application may use.
|
||||
var mods = Modules.init(allocator);
|
||||
// TODO: enable mods.deinit(allocator); for allocator leak detection
|
||||
// defer mods.deinit(allocator);
|
||||
|
||||
// Schedule .app.start to run.
|
||||
mach.mods.schedule(.app, .start);
|
||||
|
||||
// Dispatch systems forever or until there are none left to dispatch. If your app uses mach.Core
|
||||
// then this will block forever and never return.
|
||||
try mach.mods.dispatch(.{});
|
||||
const app = mods.get(.app);
|
||||
app.run(.main);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ const Vec3 = math.Vec3;
|
|||
const Mat3x3 = math.Mat3x3;
|
||||
const Mat4x4 = math.Mat4x4;
|
||||
|
||||
const App = @This();
|
||||
|
||||
pub const mach_module = .app;
|
||||
|
||||
pub const mach_systems = .{ .start, .init, .deinit, .tick, .end_frame, .audio_state_change };
|
||||
|
||||
// TODO: banish global allocator
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
|
||||
|
|
@ -36,42 +42,26 @@ frame_encoder: *gpu.CommandEncoder = undefined,
|
|||
frame_render_pass: *gpu.RenderPassEncoder = undefined,
|
||||
sfx: mach.Audio.Opus,
|
||||
|
||||
// Define the globally unique name of our module. You can use any name here, but keep in mind no
|
||||
// two modules in the program can have the same name.
|
||||
pub const name = .app;
|
||||
pub const Mod = mach.Mod(@This());
|
||||
|
||||
pub const systems = .{
|
||||
.start = .{ .handler = start },
|
||||
.init = .{ .handler = init },
|
||||
.deinit = .{ .handler = deinit },
|
||||
.tick = .{ .handler = tick },
|
||||
.end_frame = .{ .handler = endFrame },
|
||||
.audio_state_change = .{ .handler = audioStateChange },
|
||||
};
|
||||
|
||||
fn deinit(
|
||||
core: *mach.Core.Mod,
|
||||
text_pipeline: *gfx.TextPipeline.Mod,
|
||||
sprite_pipeline: *gfx.SpritePipeline.Mod,
|
||||
audio: *mach.Audio.Mod,
|
||||
audio: *mach.Audio,
|
||||
) !void {
|
||||
text_pipeline.schedule(.deinit);
|
||||
sprite_pipeline.schedule(.deinit);
|
||||
core.schedule(.deinit);
|
||||
audio.schedule(.deinit);
|
||||
}
|
||||
|
||||
fn start(
|
||||
audio: *mach.Audio.Mod,
|
||||
audio: *mach.Audio,
|
||||
text_pipeline: *gfx.TextPipeline.Mod,
|
||||
text: *gfx.Text.Mod,
|
||||
sprite_pipeline: *gfx.SpritePipeline.Mod,
|
||||
core: *mach.Core.Mod,
|
||||
app: *Mod,
|
||||
core: *mach.Core,
|
||||
app: *App,
|
||||
) !void {
|
||||
// If you want to try fullscreen:
|
||||
// try core.set(core.state().main_window, .fullscreen, true);
|
||||
// try core.set(core.main_window, .fullscreen, true);
|
||||
|
||||
core.schedule(.init);
|
||||
audio.schedule(.init);
|
||||
|
|
@ -83,20 +73,23 @@ fn start(
|
|||
|
||||
fn init(
|
||||
entities: *mach.Entities.Mod,
|
||||
core: *mach.Core.Mod,
|
||||
audio: *mach.Audio.Mod,
|
||||
core: *mach.Core,
|
||||
audio: *mach.Audio,
|
||||
text_pipeline: *gfx.TextPipeline.Mod,
|
||||
text_style: *gfx.TextStyle.Mod,
|
||||
text: *gfx.Text.Mod,
|
||||
sprite_pipeline: *gfx.SpritePipeline.Mod,
|
||||
app: *Mod,
|
||||
app: *App,
|
||||
app_tick: mach.Call(App, .tick),
|
||||
app_deinit: mach.Call(App, .deinit),
|
||||
app_audio_state_change: mach.Call(App, .audio_state_change),
|
||||
) !void {
|
||||
core.state().on_tick = app.system(.tick);
|
||||
core.state().on_exit = app.system(.deinit);
|
||||
core.on_tick = app_tick.id;
|
||||
core.on_exit = app_deinit.id;
|
||||
|
||||
// Configure the audio module to run our audio_state_change system when entities audio finishes
|
||||
// playing
|
||||
audio.state().on_state_change = app.system(.audio_state_change);
|
||||
audio.on_state_change = app_audio_state_change.id;
|
||||
|
||||
// Create a sprite rendering pipeline
|
||||
const allocator = gpa.allocator();
|
||||
|
|
@ -127,7 +120,8 @@ fn init(
|
|||
, .{});
|
||||
text.schedule(.update);
|
||||
|
||||
const window_height: f32 = @floatFromInt(core.get(core.state().main_window, .height).?);
|
||||
const win = core.windows.get(core.main_window).?;
|
||||
const window_height: f32 = @floatFromInt(win.height);
|
||||
const info_text = try entities.new();
|
||||
try text.set(info_text, .pipeline, text_rendering_pipeline);
|
||||
try text.set(info_text, .transform, Mat4x4.translate(vec3(0, (window_height / 2.0) - 50.0, 0)));
|
||||
|
|
@ -154,14 +148,13 @@ fn init(
|
|||
.pipeline = pipeline,
|
||||
.sfx = sfx,
|
||||
});
|
||||
core.schedule(.start);
|
||||
}
|
||||
|
||||
fn audioStateChange(entities: *mach.Entities.Mod) !void {
|
||||
// Find audio entities that are no longer playing
|
||||
var q = try entities.query(.{
|
||||
.ids = mach.Entities.Mod.read(.id),
|
||||
.playings = mach.Audio.Mod.read(.playing),
|
||||
.playings = mach.Audio.read(.playing),
|
||||
});
|
||||
while (q.next()) |v| {
|
||||
for (v.ids, v.playings) |id, playing| {
|
||||
|
|
@ -175,18 +168,18 @@ fn audioStateChange(entities: *mach.Entities.Mod) !void {
|
|||
|
||||
fn tick(
|
||||
entities: *mach.Entities.Mod,
|
||||
core: *mach.Core.Mod,
|
||||
core: *mach.Core,
|
||||
sprite: *gfx.Sprite.Mod,
|
||||
sprite_pipeline: *gfx.SpritePipeline.Mod,
|
||||
text: *gfx.Text.Mod,
|
||||
text_pipeline: *gfx.TextPipeline.Mod,
|
||||
app: *Mod,
|
||||
audio: *mach.Audio.Mod,
|
||||
app: *App,
|
||||
audio: *mach.Audio,
|
||||
) !void {
|
||||
// TODO(important): event polling should occur in mach.Core module and get fired as ECS events.
|
||||
// TODO(Core)
|
||||
var gotta_go_fast = app.state().gotta_go_fast;
|
||||
while (core.state().nextEvent()) |event| {
|
||||
var gotta_go_fast = app.gotta_go_fast;
|
||||
while (core.nextEvent()) |event| {
|
||||
switch (event) {
|
||||
.key_press => |ev| {
|
||||
switch (ev.key) {
|
||||
|
|
@ -200,52 +193,53 @@ fn tick(
|
|||
else => {},
|
||||
}
|
||||
},
|
||||
.close => core.schedule(.exit),
|
||||
.close => core.exit(),
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
app.state().gotta_go_fast = gotta_go_fast;
|
||||
app.gotta_go_fast = gotta_go_fast;
|
||||
|
||||
// Every second, update the frame rate
|
||||
if (app.state().fps_timer.read() >= 1.0) {
|
||||
app.state().frame_rate = app.state().frame_count;
|
||||
app.state().fps_timer.reset();
|
||||
app.state().frame_count = 0;
|
||||
if (app.fps_timer.read() >= 1.0) {
|
||||
app.frame_rate = app.frame_count;
|
||||
app.fps_timer.reset();
|
||||
app.frame_count = 0;
|
||||
}
|
||||
|
||||
try gfx.Text.allocPrintText(
|
||||
text,
|
||||
app.state().info_text,
|
||||
app.state().info_text_style,
|
||||
app.info_text,
|
||||
app.info_text_style,
|
||||
"[ FPS: {d} ]\n[ Sprites spawned: {d} ]",
|
||||
.{ app.state().frame_rate, app.state().num_sprites_spawned },
|
||||
.{ app.frame_rate, app.num_sprites_spawned },
|
||||
);
|
||||
text.schedule(.update);
|
||||
|
||||
// var player_transform = sprite.get(app.state().player, .transform).?;
|
||||
// var player_transform = sprite.get(app.player, .transform).?;
|
||||
// var player_pos = player_transform.translation();
|
||||
const window_width: f32 = @floatFromInt(core.get(core.state().main_window, .width).?);
|
||||
const win = core.windows.get(core.main_window).?;
|
||||
const window_width: f32 = @floatFromInt(win.width);
|
||||
|
||||
const entities_per_second: f32 = @floatFromInt(
|
||||
app.state().rand.random().intRangeAtMost(usize, 0, if (gotta_go_fast) 50 else 10),
|
||||
app.rand.random().intRangeAtMost(usize, 0, if (gotta_go_fast) 50 else 10),
|
||||
);
|
||||
if (app.state().spawn_timer.read() > 1.0 / entities_per_second) {
|
||||
if (app.spawn_timer.read() > 1.0 / entities_per_second) {
|
||||
// Spawn new entities
|
||||
_ = app.state().spawn_timer.lap();
|
||||
_ = app.spawn_timer.lap();
|
||||
|
||||
var new_pos = vec3(-(window_width / 2), 0, 0);
|
||||
new_pos.v[1] += app.state().rand.random().floatNorm(f32) * 50;
|
||||
new_pos.v[1] += app.rand.random().floatNorm(f32) * 50;
|
||||
|
||||
const new_entity = try entities.new();
|
||||
try sprite.set(new_entity, .transform, Mat4x4.translate(new_pos));
|
||||
try sprite.set(new_entity, .size, vec2(32, 32));
|
||||
try sprite.set(new_entity, .uv_transform, Mat3x3.translate(vec2(0, 0)));
|
||||
try sprite.set(new_entity, .pipeline, app.state().pipeline);
|
||||
app.state().num_sprites_spawned += 1;
|
||||
try sprite.set(new_entity, .pipeline, app.pipeline);
|
||||
app.num_sprites_spawned += 1;
|
||||
}
|
||||
|
||||
// Multiply by delta_time to ensure that movement is the same speed regardless of the frame rate.
|
||||
const delta_time = app.state().timer.lap();
|
||||
const delta_time = app.timer.lap();
|
||||
|
||||
// Move entities to the right, and make them smaller the further they travel
|
||||
var q = try entities.query(.{
|
||||
|
|
@ -263,11 +257,11 @@ fn tick(
|
|||
|
||||
// Play a new SFX
|
||||
const e = try entities.new();
|
||||
try audio.set(e, .samples, app.state().sfx.samples);
|
||||
try audio.set(e, .channels, app.state().sfx.channels);
|
||||
try audio.set(e, .samples, app.sfx.samples);
|
||||
try audio.set(e, .channels, app.sfx.channels);
|
||||
try audio.set(e, .index, 0);
|
||||
try audio.set(e, .playing, true);
|
||||
app.state().score += 1;
|
||||
app.score += 1;
|
||||
} else {
|
||||
var transform = Mat4x4.ident;
|
||||
transform = transform.mul(&Mat4x4.translate(location.add(&vec3(speed * delta_time, (speed / 2.0) * delta_time * progression, 0))));
|
||||
|
|
@ -284,12 +278,12 @@ fn tick(
|
|||
text_pipeline.schedule(.pre_render);
|
||||
|
||||
// Create a command encoder for this frame
|
||||
const label = @tagName(name) ++ ".tick";
|
||||
app.state().frame_encoder = core.state().device.createCommandEncoder(&.{ .label = label });
|
||||
const label = @tagName(mach_module) ++ ".tick";
|
||||
app.frame_encoder = core.device.createCommandEncoder(&.{ .label = label });
|
||||
|
||||
// Grab the back buffer of the swapchain
|
||||
// TODO(Core)
|
||||
const back_buffer_view = core.state().swap_chain.getCurrentTextureView().?;
|
||||
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
|
||||
defer back_buffer_view.release();
|
||||
|
||||
// Begin render pass
|
||||
|
|
@ -300,45 +294,42 @@ fn tick(
|
|||
.load_op = .clear,
|
||||
.store_op = .store,
|
||||
}};
|
||||
app.state().frame_render_pass = app.state().frame_encoder.beginRenderPass(&gpu.RenderPassDescriptor.init(.{
|
||||
app.frame_render_pass = app.frame_encoder.beginRenderPass(&gpu.RenderPassDescriptor.init(.{
|
||||
.label = label,
|
||||
.color_attachments = &color_attachments,
|
||||
}));
|
||||
|
||||
// Render our sprite batch
|
||||
sprite_pipeline.state().render_pass = app.state().frame_render_pass;
|
||||
sprite_pipeline.state().render_pass = app.frame_render_pass;
|
||||
sprite_pipeline.schedule(.render);
|
||||
|
||||
// Render our text batch
|
||||
text_pipeline.state().render_pass = app.state().frame_render_pass;
|
||||
text_pipeline.state().render_pass = app.frame_render_pass;
|
||||
text_pipeline.schedule(.render);
|
||||
|
||||
// Finish the frame once rendering is done.
|
||||
app.schedule(.end_frame);
|
||||
|
||||
app.state().time += delta_time;
|
||||
app.time += delta_time;
|
||||
}
|
||||
|
||||
fn endFrame(app: *Mod, core: *mach.Core.Mod) !void {
|
||||
fn endFrame(app: *App, core: *mach.Core) !void {
|
||||
// Finish render pass
|
||||
app.state().frame_render_pass.end();
|
||||
const label = @tagName(name) ++ ".endFrame";
|
||||
var command = app.state().frame_encoder.finish(&.{ .label = label });
|
||||
core.state().queue.submit(&[_]*gpu.CommandBuffer{command});
|
||||
app.frame_render_pass.end();
|
||||
const label = @tagName(mach_module) ++ ".endFrame";
|
||||
var command = app.frame_encoder.finish(&.{ .label = label });
|
||||
core.queue.submit(&[_]*gpu.CommandBuffer{command});
|
||||
command.release();
|
||||
app.state().frame_encoder.release();
|
||||
app.state().frame_render_pass.release();
|
||||
app.frame_encoder.release();
|
||||
app.frame_render_pass.release();
|
||||
|
||||
// Present the frame
|
||||
core.schedule(.present_frame);
|
||||
|
||||
app.state().frame_count += 1;
|
||||
app.frame_count += 1;
|
||||
}
|
||||
|
||||
// TODO: move this helper into gfx module
|
||||
fn loadTexture(core: *mach.Core.Mod, allocator: std.mem.Allocator) !*gpu.Texture {
|
||||
const device = core.state().device;
|
||||
const queue = core.state().queue;
|
||||
fn loadTexture(core: *mach.Core, allocator: std.mem.Allocator) !*gpu.Texture {
|
||||
const device = core.device;
|
||||
const queue = core.queue;
|
||||
|
||||
// Load the image from memory
|
||||
var img = try zigimg.Image.fromMemory(allocator, assets.sprites_sheet_png);
|
||||
|
|
@ -346,7 +337,7 @@ fn loadTexture(core: *mach.Core.Mod, allocator: std.mem.Allocator) !*gpu.Texture
|
|||
const img_size = gpu.Extent3D{ .width = @as(u32, @intCast(img.width)), .height = @as(u32, @intCast(img.height)) };
|
||||
|
||||
// Create a GPU texture
|
||||
const label = @tagName(name) ++ ".loadTexture";
|
||||
const label = @tagName(mach_module) ++ ".loadTexture";
|
||||
const texture = device.createTexture(&.{
|
||||
.label = label,
|
||||
.size = img_size,
|
||||
|
|
|
|||
|
|
@ -1,26 +1,24 @@
|
|||
const std = @import("std");
|
||||
const mach = @import("mach");
|
||||
|
||||
// The global list of Mach modules our application may use.
|
||||
pub const modules = .{
|
||||
// The set of Mach modules our application may use.
|
||||
const Modules = mach.Modules(.{
|
||||
mach.Core,
|
||||
mach.gfx.sprite_modules,
|
||||
mach.gfx.text_modules,
|
||||
mach.Audio,
|
||||
@import("App.zig"),
|
||||
};
|
||||
});
|
||||
|
||||
// TODO: move this to a mach "entrypoint" zig module which handles nuances like WASM requires.
|
||||
pub fn main() !void {
|
||||
const allocator = std.heap.c_allocator;
|
||||
|
||||
// Initialize module system
|
||||
try mach.mods.init(allocator);
|
||||
// The set of Mach modules our application may use.
|
||||
var mods = Modules.init(allocator);
|
||||
// TODO: enable mods.deinit(allocator); for allocator leak detection
|
||||
// defer mods.deinit(allocator);
|
||||
|
||||
// Schedule .app.start to run.
|
||||
mach.mods.schedule(.app, .start);
|
||||
|
||||
// Dispatch systems forever or until there are none left to dispatch. If your app uses mach.Core
|
||||
// then this will block forever and never return.
|
||||
try mach.mods.dispatch(.{});
|
||||
const app = mods.get(.app);
|
||||
app.run(.main);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,41 +16,41 @@ const gpu = mach.gpu;
|
|||
const math = mach.math;
|
||||
const sysaudio = mach.sysaudio;
|
||||
|
||||
pub const App = @This();
|
||||
const App = @This();
|
||||
|
||||
pub const mach_module = .app;
|
||||
|
||||
pub const mach_systems = .{ .start, .init, .deinit, .tick, .audio_state_change };
|
||||
|
||||
// TODO: banish global allocator
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
|
||||
pub const name = .app;
|
||||
pub const Mod = mach.Mod(@This());
|
||||
|
||||
pub const systems = .{
|
||||
.start = .{ .handler = start },
|
||||
.init = .{ .handler = init },
|
||||
.deinit = .{ .handler = deinit },
|
||||
.tick = .{ .handler = tick },
|
||||
.audio_state_change = .{ .handler = audioStateChange },
|
||||
};
|
||||
|
||||
pub const components = .{
|
||||
.play_after = .{ .type = f32 },
|
||||
};
|
||||
|
||||
ghost_key_mode: bool = false,
|
||||
|
||||
fn start(core: *mach.Core.Mod, audio: *mach.Audio.Mod, app: *Mod) void {
|
||||
fn start(core: *mach.Core, audio: *mach.Audio, app: *App) void {
|
||||
core.schedule(.init);
|
||||
audio.schedule(.init);
|
||||
app.schedule(.init);
|
||||
}
|
||||
|
||||
fn init(core: *mach.Core.Mod, audio: *mach.Audio.Mod, app: *Mod) void {
|
||||
core.state().on_tick = app.system(.tick);
|
||||
core.state().on_exit = app.system(.deinit);
|
||||
fn init(
|
||||
core: *mach.Core,
|
||||
audio: *mach.Audio,
|
||||
app: *App,
|
||||
app_tick: mach.Call(App, .tick),
|
||||
app_deinit: mach.Call(App, .deinit),
|
||||
app_audio_state_change: mach.Call(App, .audio_state_change),
|
||||
) !void {
|
||||
core.on_tick = app_tick.id;
|
||||
core.on_exit = app_deinit.id;
|
||||
|
||||
// Configure the audio module to send our app's .audio_state_change event when an entity's sound
|
||||
// finishes playing.
|
||||
audio.state().on_state_change = app.system(.audio_state_change);
|
||||
audio.on_state_change = app_audio_state_change.id;
|
||||
|
||||
// Initialize piano module state
|
||||
app.init(.{});
|
||||
|
|
@ -60,24 +60,21 @@ fn init(core: *mach.Core.Mod, audio: *mach.Audio.Mod, app: *Mod) void {
|
|||
std.debug.print("[spacebar] enable ghost-key mode (demonstrate seamless back-to-back sound playback)\n", .{});
|
||||
std.debug.print("[arrow up] increase volume 10%\n", .{});
|
||||
std.debug.print("[arrow down] decrease volume 10%\n", .{});
|
||||
|
||||
core.schedule(.start);
|
||||
}
|
||||
|
||||
fn deinit(core: *mach.Core.Mod, audio: *mach.Audio.Mod) void {
|
||||
fn deinit(audio: *mach.Audio) void {
|
||||
audio.schedule(.deinit);
|
||||
core.schedule(.deinit);
|
||||
}
|
||||
|
||||
fn audioStateChange(
|
||||
entities: *mach.Entities.Mod,
|
||||
audio: *mach.Audio.Mod,
|
||||
app: *Mod,
|
||||
audio: *mach.Audio,
|
||||
app: *App,
|
||||
) !void {
|
||||
// Find audio entities that are no longer playing
|
||||
var q = try entities.query(.{
|
||||
.ids = mach.Entities.Mod.read(.id),
|
||||
.playings = mach.Audio.Mod.read(.playing),
|
||||
.playings = mach.Audio.read(.playing),
|
||||
});
|
||||
while (q.next()) |v| {
|
||||
for (v.ids, v.playings) |id, playing| {
|
||||
|
|
@ -87,7 +84,7 @@ fn audioStateChange(
|
|||
// Play a new sound
|
||||
const e = try entities.new();
|
||||
try audio.set(e, .samples, try fillTone(audio, frequency));
|
||||
try audio.set(e, .channels, @intCast(audio.state().player.channels().len));
|
||||
try audio.set(e, .channels, @intCast(audio.player.channels().len));
|
||||
try audio.set(e, .playing, true);
|
||||
try audio.set(e, .index, 0);
|
||||
}
|
||||
|
|
@ -100,24 +97,24 @@ fn audioStateChange(
|
|||
|
||||
fn tick(
|
||||
entities: *mach.Entities.Mod,
|
||||
core: *mach.Core.Mod,
|
||||
audio: *mach.Audio.Mod,
|
||||
app: *Mod,
|
||||
core: *mach.Core,
|
||||
audio: *mach.Audio,
|
||||
app: *App,
|
||||
) !void {
|
||||
while (core.state().nextEvent()) |event| {
|
||||
while (core.nextEvent()) |event| {
|
||||
switch (event) {
|
||||
.key_press => |ev| {
|
||||
switch (ev.key) {
|
||||
// Controls
|
||||
.space => app.state().ghost_key_mode = !app.state().ghost_key_mode,
|
||||
.space => app.ghost_key_mode = !app.ghost_key_mode,
|
||||
.down => {
|
||||
const vol = math.clamp(try audio.state().player.volume() - 0.1, 0, 1);
|
||||
try audio.state().player.setVolume(vol);
|
||||
const vol = math.clamp(try audio.player.volume() - 0.1, 0, 1);
|
||||
try audio.player.setVolume(vol);
|
||||
std.debug.print("[volume] {d:.0}%\n", .{vol * 100.0});
|
||||
},
|
||||
.up => {
|
||||
const vol = math.clamp(try audio.state().player.volume() + 0.1, 0, 1);
|
||||
try audio.state().player.setVolume(vol);
|
||||
const vol = math.clamp(try audio.player.volume() + 0.1, 0, 1);
|
||||
try audio.player.setVolume(vol);
|
||||
std.debug.print("[volume] {d:.0}%\n", .{vol * 100.0});
|
||||
},
|
||||
|
||||
|
|
@ -126,11 +123,11 @@ fn tick(
|
|||
// Play a new sound
|
||||
const e = try entities.new();
|
||||
try audio.set(e, .samples, try fillTone(audio, keyToFrequency(ev.key)));
|
||||
try audio.set(e, .channels, @intCast(audio.state().player.channels().len));
|
||||
try audio.set(e, .channels, @intCast(audio.player.channels().len));
|
||||
try audio.set(e, .playing, true);
|
||||
try audio.set(e, .index, 0);
|
||||
|
||||
if (app.state().ghost_key_mode) {
|
||||
if (app.ghost_key_mode) {
|
||||
// After that sound plays, we'll chain on another sound that is one semi-tone higher.
|
||||
const one_semi_tone_higher = keyToFrequency(ev.key) * math.pow(f32, 2.0, (1.0 / 12.0));
|
||||
try app.set(e, .play_after, one_semi_tone_higher);
|
||||
|
|
@ -138,19 +135,19 @@ fn tick(
|
|||
},
|
||||
}
|
||||
},
|
||||
.close => core.schedule(.exit),
|
||||
.close => core.exit(),
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
// Grab the back buffer of the swapchain
|
||||
// TODO(Core)
|
||||
const back_buffer_view = core.state().swap_chain.getCurrentTextureView().?;
|
||||
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
|
||||
defer back_buffer_view.release();
|
||||
|
||||
// Create a command encoder
|
||||
const label = @tagName(name) ++ ".tick";
|
||||
const encoder = core.state().device.createCommandEncoder(&.{ .label = label });
|
||||
const label = @tagName(mach_module) ++ ".tick";
|
||||
const encoder = core.device.createCommandEncoder(&.{ .label = label });
|
||||
defer encoder.release();
|
||||
|
||||
// Begin render pass
|
||||
|
|
@ -175,15 +172,12 @@ fn tick(
|
|||
// Submit our commands to the queue
|
||||
var command = encoder.finish(&.{ .label = label });
|
||||
defer command.release();
|
||||
core.state().queue.submit(&[_]*gpu.CommandBuffer{command});
|
||||
|
||||
// Present the frame
|
||||
core.schedule(.present_frame);
|
||||
core.queue.submit(&[_]*gpu.CommandBuffer{command});
|
||||
}
|
||||
|
||||
fn fillTone(audio: *mach.Audio.Mod, frequency: f32) ![]const f32 {
|
||||
const channels = audio.state().player.channels().len;
|
||||
const sample_rate: f32 = @floatFromInt(audio.state().player.sampleRate());
|
||||
fn fillTone(audio: *mach.Audio, frequency: f32) ![]const f32 {
|
||||
const channels = audio.player.channels().len;
|
||||
const sample_rate: f32 = @floatFromInt(audio.player.sampleRate());
|
||||
const duration: f32 = 1.5 * @as(f32, @floatFromInt(channels)) * sample_rate; // play the tone for 1.5s
|
||||
const gain = 0.1;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,24 +1,22 @@
|
|||
const std = @import("std");
|
||||
const mach = @import("mach");
|
||||
|
||||
// The global list of Mach modules our application may use.
|
||||
pub const modules = .{
|
||||
// The set of Mach modules our application may use.
|
||||
const Modules = mach.Modules(.{
|
||||
mach.Core,
|
||||
mach.Audio,
|
||||
@import("App.zig"),
|
||||
};
|
||||
});
|
||||
|
||||
// TODO: move this to a mach "entrypoint" zig module which handles nuances like WASM requires.
|
||||
pub fn main() !void {
|
||||
const allocator = std.heap.c_allocator;
|
||||
|
||||
// Initialize module system
|
||||
try mach.mods.init(allocator);
|
||||
// The set of Mach modules our application may use.
|
||||
var mods = Modules.init(allocator);
|
||||
// TODO: enable mods.deinit(allocator); for allocator leak detection
|
||||
// defer mods.deinit(allocator);
|
||||
|
||||
// Schedule .app.start to run.
|
||||
mach.mods.schedule(.app, .start);
|
||||
|
||||
// Dispatch systems forever or until there are none left to dispatch. If your app uses mach.Core
|
||||
// then this will block forever and never return.
|
||||
try mach.mods.dispatch(.{});
|
||||
const app = mods.get(.app);
|
||||
app.run(.main);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,16 +15,9 @@ pub const App = @This();
|
|||
// TODO: banish global allocator
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
|
||||
pub const name = .app;
|
||||
pub const Mod = mach.Mod(@This());
|
||||
pub const mach_module = .app;
|
||||
|
||||
pub const systems = .{
|
||||
.start = .{ .handler = start },
|
||||
.init = .{ .handler = init },
|
||||
.deinit = .{ .handler = deinit },
|
||||
.tick = .{ .handler = tick },
|
||||
.audio_state_change = .{ .handler = audioStateChange },
|
||||
};
|
||||
pub const mach_systems = .{ .start, .init, .deinit, .tick, .audio_state_change };
|
||||
|
||||
pub const components = .{
|
||||
.is_bgm = .{ .type = void },
|
||||
|
|
@ -33,9 +26,9 @@ pub const components = .{
|
|||
sfx: mach.Audio.Opus,
|
||||
|
||||
fn start(
|
||||
core: *mach.Core.Mod,
|
||||
audio: *mach.Audio.Mod,
|
||||
app: *Mod,
|
||||
core: *mach.Core,
|
||||
audio: *mach.Audio,
|
||||
app: *App,
|
||||
) !void {
|
||||
core.schedule(.init);
|
||||
audio.schedule(.init);
|
||||
|
|
@ -44,16 +37,19 @@ fn start(
|
|||
|
||||
fn init(
|
||||
entities: *mach.Entities.Mod,
|
||||
core: *mach.Core.Mod,
|
||||
audio: *mach.Audio.Mod,
|
||||
app: *Mod,
|
||||
core: *mach.Core,
|
||||
audio: *mach.Audio,
|
||||
app: *App,
|
||||
app_tick: mach.Call(App, .tick),
|
||||
app_deinit: mach.Call(App, .deinit),
|
||||
app_audio_state_change: mach.Call(App, .audio_state_change),
|
||||
) !void {
|
||||
core.state().on_tick = app.system(.tick);
|
||||
core.state().on_exit = app.system(.deinit);
|
||||
core.on_tick = app_tick.id;
|
||||
core.on_exit = app_deinit.id;
|
||||
|
||||
// Configure the audio module to send our app's .audio_state_change event when an entity's sound
|
||||
// finishes playing.
|
||||
audio.state().on_state_change = app.system(.audio_state_change);
|
||||
audio.on_state_change = app_audio_state_change.id;
|
||||
|
||||
const bgm_fbs = std.io.fixedBufferStream(assets.bgm.bit_bit_loop);
|
||||
const bgm_sound_stream = std.io.StreamSource{ .const_buffer = bgm_fbs };
|
||||
|
|
@ -77,24 +73,21 @@ fn init(
|
|||
std.debug.print("[typing] Play SFX\n", .{});
|
||||
std.debug.print("[arrow up] increase volume 10%\n", .{});
|
||||
std.debug.print("[arrow down] decrease volume 10%\n", .{});
|
||||
|
||||
core.schedule(.start);
|
||||
}
|
||||
|
||||
fn deinit(core: *mach.Core.Mod, audio: *mach.Audio.Mod) void {
|
||||
fn deinit(audio: *mach.Audio) void {
|
||||
audio.schedule(.deinit);
|
||||
core.schedule(.deinit);
|
||||
}
|
||||
|
||||
fn audioStateChange(
|
||||
entities: *mach.Entities.Mod,
|
||||
audio: *mach.Audio.Mod,
|
||||
app: *Mod,
|
||||
audio: *mach.Audio,
|
||||
app: *App,
|
||||
) !void {
|
||||
// Find audio entities that are no longer playing
|
||||
var q = try entities.query(.{
|
||||
.ids = mach.Entities.Mod.read(.id),
|
||||
.playings = mach.Audio.Mod.read(.playing),
|
||||
.playings = mach.Audio.read(.playing),
|
||||
});
|
||||
while (q.next()) |v| {
|
||||
for (v.ids, v.playings) |id, playing| {
|
||||
|
|
@ -114,45 +107,45 @@ fn audioStateChange(
|
|||
|
||||
fn tick(
|
||||
entities: *mach.Entities.Mod,
|
||||
core: *mach.Core.Mod,
|
||||
audio: *mach.Audio.Mod,
|
||||
app: *Mod,
|
||||
core: *mach.Core,
|
||||
audio: *mach.Audio,
|
||||
app: *App,
|
||||
) !void {
|
||||
while (core.state().nextEvent()) |event| {
|
||||
while (core.nextEvent()) |event| {
|
||||
switch (event) {
|
||||
.key_press => |ev| switch (ev.key) {
|
||||
.down => {
|
||||
const vol = math.clamp(try audio.state().player.volume() - 0.1, 0, 1);
|
||||
try audio.state().player.setVolume(vol);
|
||||
const vol = math.clamp(try audio.player.volume() - 0.1, 0, 1);
|
||||
try audio.player.setVolume(vol);
|
||||
std.debug.print("[volume] {d:.0}%\n", .{vol * 100.0});
|
||||
},
|
||||
.up => {
|
||||
const vol = math.clamp(try audio.state().player.volume() + 0.1, 0, 1);
|
||||
try audio.state().player.setVolume(vol);
|
||||
const vol = math.clamp(try audio.player.volume() + 0.1, 0, 1);
|
||||
try audio.player.setVolume(vol);
|
||||
std.debug.print("[volume] {d:.0}%\n", .{vol * 100.0});
|
||||
},
|
||||
else => {
|
||||
// Play a new SFX
|
||||
const e = try entities.new();
|
||||
try audio.set(e, .samples, app.state().sfx.samples);
|
||||
try audio.set(e, .channels, app.state().sfx.channels);
|
||||
try audio.set(e, .samples, app.sfx.samples);
|
||||
try audio.set(e, .channels, app.sfx.channels);
|
||||
try audio.set(e, .index, 0);
|
||||
try audio.set(e, .playing, true);
|
||||
},
|
||||
},
|
||||
.close => core.schedule(.exit),
|
||||
.close => core.exit(),
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
// Grab the back buffer of the swapchain
|
||||
// TODO(Core)
|
||||
const back_buffer_view = core.state().swap_chain.getCurrentTextureView().?;
|
||||
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
|
||||
defer back_buffer_view.release();
|
||||
|
||||
// Create a command encoder
|
||||
const label = @tagName(name) ++ ".tick";
|
||||
const encoder = core.state().device.createCommandEncoder(&.{ .label = label });
|
||||
const label = @tagName(mach_module) ++ ".tick";
|
||||
const encoder = core.device.createCommandEncoder(&.{ .label = label });
|
||||
defer encoder.release();
|
||||
|
||||
// Begin render pass
|
||||
|
|
@ -177,8 +170,5 @@ fn tick(
|
|||
// Submit our commands to the queue
|
||||
var command = encoder.finish(&.{ .label = label });
|
||||
defer command.release();
|
||||
core.state().queue.submit(&[_]*gpu.CommandBuffer{command});
|
||||
|
||||
// Present the frame
|
||||
core.schedule(.present_frame);
|
||||
core.queue.submit(&[_]*gpu.CommandBuffer{command});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,22 @@
|
|||
const std = @import("std");
|
||||
const mach = @import("mach");
|
||||
|
||||
// The global list of Mach modules our application may use.
|
||||
pub const modules = .{
|
||||
// The set of Mach modules our application may use.
|
||||
const Modules = mach.Modules(.{
|
||||
mach.Core,
|
||||
mach.Audio,
|
||||
@import("App.zig"),
|
||||
};
|
||||
});
|
||||
|
||||
// TODO: move this to a mach "entrypoint" zig module which handles nuances like WASM requires.
|
||||
pub fn main() !void {
|
||||
const allocator = std.heap.c_allocator;
|
||||
|
||||
// Initialize module system
|
||||
try mach.mods.init(allocator);
|
||||
// The set of Mach modules our application may use.
|
||||
var mods = Modules.init(allocator);
|
||||
// TODO: enable mods.deinit(allocator); for allocator leak detection
|
||||
// defer mods.deinit(allocator);
|
||||
|
||||
// Schedule .app.start to run.
|
||||
mach.mods.schedule(.app, .start);
|
||||
|
||||
// Dispatch systems forever or until there are none left to dispatch. If your app uses mach.Core
|
||||
// then this will block forever and never return.
|
||||
try mach.mods.dispatch(.{});
|
||||
const app = mods.get(.app);
|
||||
app.run(.main);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,12 @@ const Vec3 = math.Vec3;
|
|||
const Mat3x3 = math.Mat3x3;
|
||||
const Mat4x4 = math.Mat4x4;
|
||||
|
||||
const App = @This();
|
||||
|
||||
pub const mach_module = .app;
|
||||
|
||||
pub const mach_systems = .{ .start, .init, .deinit, .tick, .end_frame };
|
||||
|
||||
// TODO: banish global allocator
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
|
||||
|
|
@ -32,31 +38,14 @@ pipeline: mach.EntityID,
|
|||
frame_encoder: *gpu.CommandEncoder = undefined,
|
||||
frame_render_pass: *gpu.RenderPassEncoder = undefined,
|
||||
|
||||
// Define the globally unique name of our module. You can use any name here, but keep in mind no
|
||||
// two modules in the program can have the same name.
|
||||
pub const name = .app;
|
||||
pub const Mod = mach.Mod(@This());
|
||||
|
||||
pub const systems = .{
|
||||
.start = .{ .handler = start },
|
||||
.init = .{ .handler = init },
|
||||
.deinit = .{ .handler = deinit },
|
||||
.tick = .{ .handler = tick },
|
||||
.end_frame = .{ .handler = endFrame },
|
||||
};
|
||||
|
||||
fn deinit(
|
||||
core: *mach.Core.Mod,
|
||||
sprite_pipeline: *gfx.SpritePipeline.Mod,
|
||||
) !void {
|
||||
fn deinit(sprite_pipeline: *gfx.SpritePipeline.Mod) !void {
|
||||
sprite_pipeline.schedule(.deinit);
|
||||
core.schedule(.deinit);
|
||||
}
|
||||
|
||||
fn start(
|
||||
core: *mach.Core.Mod,
|
||||
core: *mach.Core,
|
||||
sprite_pipeline: *gfx.SpritePipeline.Mod,
|
||||
app: *Mod,
|
||||
app: *App,
|
||||
) !void {
|
||||
core.schedule(.init);
|
||||
sprite_pipeline.schedule(.init);
|
||||
|
|
@ -65,13 +54,15 @@ fn start(
|
|||
|
||||
fn init(
|
||||
entities: *mach.Entities.Mod,
|
||||
core: *mach.Core.Mod,
|
||||
core: *mach.Core,
|
||||
sprite: *gfx.Sprite.Mod,
|
||||
sprite_pipeline: *gfx.SpritePipeline.Mod,
|
||||
app: *Mod,
|
||||
app: *App,
|
||||
app_tick: mach.Call(App, .tick),
|
||||
app_deinit: mach.Call(App, .deinit),
|
||||
) !void {
|
||||
core.state().on_tick = app.system(.tick);
|
||||
core.state().on_exit = app.system(.deinit);
|
||||
core.on_tick = app_tick.id;
|
||||
core.on_exit = app_deinit.id;
|
||||
|
||||
// We can create entities, and set components on them. Note that components live in a module
|
||||
// namespace, e.g. the `.mach_gfx_sprite` module could have a 3D `.location` component with a different
|
||||
|
|
@ -103,20 +94,18 @@ fn init(
|
|||
.allocator = allocator,
|
||||
.pipeline = pipeline,
|
||||
});
|
||||
|
||||
core.schedule(.start);
|
||||
}
|
||||
|
||||
fn tick(
|
||||
entities: *mach.Entities.Mod,
|
||||
core: *mach.Core.Mod,
|
||||
core: *mach.Core,
|
||||
sprite: *gfx.Sprite.Mod,
|
||||
sprite_pipeline: *gfx.SpritePipeline.Mod,
|
||||
app: *Mod,
|
||||
app: *App,
|
||||
) !void {
|
||||
var direction = app.state().direction;
|
||||
var spawning = app.state().spawning;
|
||||
while (core.state().nextEvent()) |event| {
|
||||
var direction = app.direction;
|
||||
var spawning = app.spawning;
|
||||
while (core.nextEvent()) |event| {
|
||||
switch (event) {
|
||||
.key_press => |ev| {
|
||||
switch (ev.key) {
|
||||
|
|
@ -138,34 +127,34 @@ fn tick(
|
|||
else => {},
|
||||
}
|
||||
},
|
||||
.close => core.schedule(.exit),
|
||||
.close => core.exit(),
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
app.state().direction = direction;
|
||||
app.state().spawning = spawning;
|
||||
app.direction = direction;
|
||||
app.spawning = spawning;
|
||||
|
||||
var player_transform = sprite.get(app.state().player, .transform).?;
|
||||
var player_transform = sprite.get(app.player, .transform).?;
|
||||
var player_pos = player_transform.translation();
|
||||
if (spawning and app.state().spawn_timer.read() > 1.0 / 60.0) {
|
||||
if (spawning and app.spawn_timer.read() > 1.0 / 60.0) {
|
||||
// Spawn new entities
|
||||
_ = app.state().spawn_timer.lap();
|
||||
_ = app.spawn_timer.lap();
|
||||
for (0..100) |_| {
|
||||
var new_pos = player_pos;
|
||||
new_pos.v[0] += app.state().rand.random().floatNorm(f32) * 25;
|
||||
new_pos.v[1] += app.state().rand.random().floatNorm(f32) * 25;
|
||||
new_pos.v[0] += app.rand.random().floatNorm(f32) * 25;
|
||||
new_pos.v[1] += app.rand.random().floatNorm(f32) * 25;
|
||||
|
||||
const new_entity = try entities.new();
|
||||
try sprite.set(new_entity, .transform, Mat4x4.translate(new_pos).mul(&Mat4x4.scale(Vec3.splat(0.3))));
|
||||
try sprite.set(new_entity, .size, vec2(32, 32));
|
||||
try sprite.set(new_entity, .uv_transform, Mat3x3.translate(vec2(0, 0)));
|
||||
try sprite.set(new_entity, .pipeline, app.state().pipeline);
|
||||
app.state().sprites += 1;
|
||||
try sprite.set(new_entity, .pipeline, app.pipeline);
|
||||
app.sprites += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Multiply by delta_time to ensure that movement is the same speed regardless of the frame rate.
|
||||
const delta_time = app.state().timer.lap();
|
||||
const delta_time = app.timer.lap();
|
||||
|
||||
// Rotate entities
|
||||
var q = try entities.query(.{
|
||||
|
|
@ -179,8 +168,8 @@ fn tick(
|
|||
// transform = transform.mul(&Mat4x4.translate(location));
|
||||
var transform = Mat4x4.ident;
|
||||
transform = transform.mul(&Mat4x4.translate(location));
|
||||
transform = transform.mul(&Mat4x4.rotateZ(2 * math.pi * app.state().time));
|
||||
transform = transform.mul(&Mat4x4.scaleScalar(@min(math.cos(app.state().time / 2.0), 0.5)));
|
||||
transform = transform.mul(&Mat4x4.rotateZ(2 * math.pi * app.time));
|
||||
transform = transform.mul(&Mat4x4.scaleScalar(@min(math.cos(app.time / 2.0), 0.5)));
|
||||
entity_transform.* = transform;
|
||||
}
|
||||
}
|
||||
|
|
@ -190,19 +179,19 @@ fn tick(
|
|||
const speed = 200.0;
|
||||
player_pos.v[0] += direction.x() * speed * delta_time;
|
||||
player_pos.v[1] += direction.y() * speed * delta_time;
|
||||
try sprite.set(app.state().player, .transform, Mat4x4.translate(player_pos));
|
||||
try sprite.set(app.player, .transform, Mat4x4.translate(player_pos));
|
||||
sprite.schedule(.update);
|
||||
|
||||
// Perform pre-render work
|
||||
sprite_pipeline.schedule(.pre_render);
|
||||
|
||||
// Create a command encoder for this frame
|
||||
const label = @tagName(name) ++ ".tick";
|
||||
app.state().frame_encoder = core.state().device.createCommandEncoder(&.{ .label = label });
|
||||
const label = @tagName(mach_module) ++ ".tick";
|
||||
app.frame_encoder = core.device.createCommandEncoder(&.{ .label = label });
|
||||
|
||||
// Grab the back buffer of the swapchain
|
||||
// TODO(Core)
|
||||
const back_buffer_view = core.state().swap_chain.getCurrentTextureView().?;
|
||||
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
|
||||
defer back_buffer_view.release();
|
||||
|
||||
// Begin render pass
|
||||
|
|
@ -213,52 +202,49 @@ fn tick(
|
|||
.load_op = .clear,
|
||||
.store_op = .store,
|
||||
}};
|
||||
app.state().frame_render_pass = app.state().frame_encoder.beginRenderPass(&gpu.RenderPassDescriptor.init(.{
|
||||
app.frame_render_pass = app.frame_encoder.beginRenderPass(&gpu.RenderPassDescriptor.init(.{
|
||||
.label = label,
|
||||
.color_attachments = &color_attachments,
|
||||
}));
|
||||
|
||||
// Render our sprite batch
|
||||
sprite_pipeline.state().render_pass = app.state().frame_render_pass;
|
||||
sprite_pipeline.state().render_pass = app.frame_render_pass;
|
||||
sprite_pipeline.schedule(.render);
|
||||
|
||||
// Finish the frame once rendering is done.
|
||||
app.schedule(.end_frame);
|
||||
|
||||
app.state().time += delta_time;
|
||||
app.time += delta_time;
|
||||
}
|
||||
|
||||
fn endFrame(app: *Mod, core: *mach.Core.Mod) !void {
|
||||
fn endFrame(app: *App, core: *mach.Core) !void {
|
||||
// Finish render pass
|
||||
app.state().frame_render_pass.end();
|
||||
const label = @tagName(name) ++ ".endFrame";
|
||||
var command = app.state().frame_encoder.finish(&.{ .label = label });
|
||||
core.state().queue.submit(&[_]*gpu.CommandBuffer{command});
|
||||
app.frame_render_pass.end();
|
||||
const label = @tagName(mach_module) ++ ".endFrame";
|
||||
var command = app.frame_encoder.finish(&.{ .label = label });
|
||||
core.queue.submit(&[_]*gpu.CommandBuffer{command});
|
||||
command.release();
|
||||
app.state().frame_encoder.release();
|
||||
app.state().frame_render_pass.release();
|
||||
|
||||
// Present the frame
|
||||
core.schedule(.present_frame);
|
||||
app.frame_encoder.release();
|
||||
app.frame_render_pass.release();
|
||||
|
||||
// Every second, update the window title with the FPS
|
||||
if (app.state().fps_timer.read() >= 1.0) {
|
||||
try core.state().printTitle(
|
||||
core.state().main_window,
|
||||
if (app.fps_timer.read() >= 1.0) {
|
||||
try core.printTitle(
|
||||
core.main_window,
|
||||
"sprite [ FPS: {d} ] [ Sprites: {d} ]",
|
||||
.{ app.state().frame_count, app.state().sprites },
|
||||
.{ app.frame_count, app.sprites },
|
||||
);
|
||||
core.schedule(.update);
|
||||
app.state().fps_timer.reset();
|
||||
app.state().frame_count = 0;
|
||||
app.fps_timer.reset();
|
||||
app.frame_count = 0;
|
||||
}
|
||||
app.state().frame_count += 1;
|
||||
app.frame_count += 1;
|
||||
}
|
||||
|
||||
// TODO: move this helper into gfx module
|
||||
fn loadTexture(core: *mach.Core.Mod, allocator: std.mem.Allocator) !*gpu.Texture {
|
||||
const device = core.state().device;
|
||||
const queue = core.state().queue;
|
||||
fn loadTexture(core: *mach.Core, allocator: std.mem.Allocator) !*gpu.Texture {
|
||||
const device = core.device;
|
||||
const queue = core.queue;
|
||||
|
||||
// Load the image from memory
|
||||
var img = try zigimg.Image.fromMemory(allocator, assets.sprites_sheet_png);
|
||||
|
|
@ -266,7 +252,7 @@ fn loadTexture(core: *mach.Core.Mod, allocator: std.mem.Allocator) !*gpu.Texture
|
|||
const img_size = gpu.Extent3D{ .width = @as(u32, @intCast(img.width)), .height = @as(u32, @intCast(img.height)) };
|
||||
|
||||
// Create a GPU texture
|
||||
const label = @tagName(name) ++ ".loadTexture";
|
||||
const label = @tagName(mach_module) ++ ".loadTexture";
|
||||
const texture = device.createTexture(&.{
|
||||
.label = label,
|
||||
.size = img_size,
|
||||
|
|
|
|||
|
|
@ -1,24 +1,22 @@
|
|||
const std = @import("std");
|
||||
const mach = @import("mach");
|
||||
|
||||
// The global list of Mach modules our application may use.
|
||||
pub const modules = .{
|
||||
// The set of Mach modules our application may use.
|
||||
const Modules = mach.Modules(.{
|
||||
mach.Core,
|
||||
mach.gfx.sprite_modules,
|
||||
@import("App.zig"),
|
||||
};
|
||||
});
|
||||
|
||||
// TODO: move this to a mach "entrypoint" zig module which handles nuances like WASM requires.
|
||||
pub fn main() !void {
|
||||
const allocator = std.heap.c_allocator;
|
||||
|
||||
// Initialize module system
|
||||
try mach.mods.init(allocator);
|
||||
// The set of Mach modules our application may use.
|
||||
var mods = Modules.init(allocator);
|
||||
// TODO: enable mods.deinit(allocator); for allocator leak detection
|
||||
// defer mods.deinit(allocator);
|
||||
|
||||
// Schedule .app.start to run.
|
||||
mach.mods.schedule(.app, .start);
|
||||
|
||||
// Dispatch systems forever or until there are none left to dispatch. If your app uses mach.Core
|
||||
// then this will block forever and never return.
|
||||
try mach.mods.dispatch(.{});
|
||||
const app = mods.get(.app);
|
||||
app.run(.main);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,12 @@ const Vec3 = math.Vec3;
|
|||
const Mat3x3 = math.Mat3x3;
|
||||
const Mat4x4 = math.Mat4x4;
|
||||
|
||||
const App = @This();
|
||||
|
||||
pub const mach_module = .app;
|
||||
|
||||
pub const mach_systems = .{ .start, .init, .deinit, .tick, .end_frame };
|
||||
|
||||
timer: mach.time.Timer,
|
||||
player: mach.EntityID,
|
||||
direction: Vec2 = vec2(0, 0),
|
||||
|
|
@ -28,19 +34,6 @@ pipeline: mach.EntityID,
|
|||
frame_encoder: *gpu.CommandEncoder = undefined,
|
||||
frame_render_pass: *gpu.RenderPassEncoder = undefined,
|
||||
|
||||
// Define the globally unique name of our module. You can use any name here, but keep in mind no
|
||||
// two modules in the program can have the same name.
|
||||
pub const name = .app;
|
||||
pub const Mod = mach.Mod(@This());
|
||||
|
||||
pub const systems = .{
|
||||
.start = .{ .handler = start },
|
||||
.init = .{ .handler = init },
|
||||
.deinit = .{ .handler = deinit },
|
||||
.tick = .{ .handler = tick },
|
||||
.end_frame = .{ .handler = endFrame },
|
||||
};
|
||||
|
||||
const upscale = 1.0;
|
||||
|
||||
const text1: []const []const u8 = &.{
|
||||
|
|
@ -51,19 +44,15 @@ const text1: []const []const u8 = &.{
|
|||
|
||||
const text2: []const []const u8 = &.{"$!?"};
|
||||
|
||||
fn deinit(
|
||||
core: *mach.Core.Mod,
|
||||
text_pipeline: *gfx.TextPipeline.Mod,
|
||||
) !void {
|
||||
fn deinit(text_pipeline: *gfx.TextPipeline.Mod) !void {
|
||||
text_pipeline.schedule(.deinit);
|
||||
core.schedule(.deinit);
|
||||
}
|
||||
|
||||
fn start(
|
||||
core: *mach.Core.Mod,
|
||||
core: *mach.Core,
|
||||
text: *gfx.Text.Mod,
|
||||
text_pipeline: *gfx.TextPipeline.Mod,
|
||||
app: *Mod,
|
||||
app: *App,
|
||||
) !void {
|
||||
core.schedule(.init);
|
||||
text.schedule(.init);
|
||||
|
|
@ -73,14 +62,16 @@ fn start(
|
|||
|
||||
fn init(
|
||||
entities: *mach.Entities.Mod,
|
||||
core: *mach.Core.Mod,
|
||||
core: *mach.Core,
|
||||
text: *gfx.Text.Mod,
|
||||
text_pipeline: *gfx.TextPipeline.Mod,
|
||||
text_style: *gfx.TextStyle.Mod,
|
||||
app: *Mod,
|
||||
app: *App,
|
||||
app_tick: mach.Call(App, .tick),
|
||||
app_deinit: mach.Call(App, .deinit),
|
||||
) !void {
|
||||
core.state().on_tick = app.system(.tick);
|
||||
core.state().on_exit = app.system(.deinit);
|
||||
core.on_tick = app_tick.id;
|
||||
core.on_exit = app_deinit.id;
|
||||
|
||||
// TODO: a better way to initialize entities with default values
|
||||
// TODO(text): ability to specify other style options (custom font name, font color, italic/bold, etc.)
|
||||
|
|
@ -114,20 +105,18 @@ fn init(
|
|||
.style1 = style1,
|
||||
.pipeline = pipeline,
|
||||
});
|
||||
|
||||
core.schedule(.start);
|
||||
}
|
||||
|
||||
fn tick(
|
||||
entities: *mach.Entities.Mod,
|
||||
core: *mach.Core.Mod,
|
||||
core: *mach.Core,
|
||||
text: *gfx.Text.Mod,
|
||||
text_pipeline: *gfx.TextPipeline.Mod,
|
||||
app: *Mod,
|
||||
app: *App,
|
||||
) !void {
|
||||
var direction = app.state().direction;
|
||||
var spawning = app.state().spawning;
|
||||
while (core.state().nextEvent()) |event| {
|
||||
var direction = app.direction;
|
||||
var spawning = app.spawning;
|
||||
while (core.nextEvent()) |event| {
|
||||
switch (event) {
|
||||
.key_press => |ev| {
|
||||
switch (ev.key) {
|
||||
|
|
@ -149,33 +138,33 @@ fn tick(
|
|||
else => {},
|
||||
}
|
||||
},
|
||||
.close => core.schedule(.exit),
|
||||
.close => core.exit(),
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
app.state().direction = direction;
|
||||
app.state().spawning = spawning;
|
||||
app.direction = direction;
|
||||
app.spawning = spawning;
|
||||
|
||||
var player_transform = text.get(app.state().player, .transform).?;
|
||||
var player_transform = text.get(app.player, .transform).?;
|
||||
var player_pos = player_transform.translation().divScalar(upscale);
|
||||
if (spawning and app.state().spawn_timer.read() > (1.0 / 60.0)) {
|
||||
if (spawning and app.spawn_timer.read() > (1.0 / 60.0)) {
|
||||
// Spawn new entities
|
||||
_ = app.state().spawn_timer.lap();
|
||||
_ = app.spawn_timer.lap();
|
||||
for (0..10) |_| {
|
||||
var new_pos = player_pos;
|
||||
new_pos.v[0] += app.state().rand.random().floatNorm(f32) * 50;
|
||||
new_pos.v[1] += app.state().rand.random().floatNorm(f32) * 50;
|
||||
new_pos.v[0] += app.rand.random().floatNorm(f32) * 50;
|
||||
new_pos.v[1] += app.rand.random().floatNorm(f32) * 50;
|
||||
|
||||
// Create some text
|
||||
const new_entity = try entities.new();
|
||||
try text.set(new_entity, .pipeline, app.state().pipeline);
|
||||
try text.set(new_entity, .pipeline, app.pipeline);
|
||||
try text.set(new_entity, .transform, Mat4x4.scaleScalar(upscale).mul(&Mat4x4.translate(new_pos)));
|
||||
try gfx.Text.allocPrintText(text, new_entity, app.state().style1, "?!$", .{});
|
||||
try gfx.Text.allocPrintText(text, new_entity, app.style1, "?!$", .{});
|
||||
}
|
||||
}
|
||||
|
||||
// Multiply by delta_time to ensure that movement is the same speed regardless of the frame rate.
|
||||
const delta_time = app.state().timer.lap();
|
||||
const delta_time = app.timer.lap();
|
||||
|
||||
// Rotate entities
|
||||
var q = try entities.query(.{
|
||||
|
|
@ -189,8 +178,8 @@ fn tick(
|
|||
// transform = transform.mul(&Mat4x4.translate(location));
|
||||
var transform = Mat4x4.ident;
|
||||
transform = transform.mul(&Mat4x4.translate(location));
|
||||
transform = transform.mul(&Mat4x4.rotateZ(2 * math.pi * app.state().time));
|
||||
transform = transform.mul(&Mat4x4.scaleScalar(@min(math.cos(app.state().time / 2.0), 0.5)));
|
||||
transform = transform.mul(&Mat4x4.rotateZ(2 * math.pi * app.time));
|
||||
transform = transform.mul(&Mat4x4.scaleScalar(@min(math.cos(app.time / 2.0), 0.5)));
|
||||
entity_transform.* = transform;
|
||||
}
|
||||
}
|
||||
|
|
@ -200,20 +189,20 @@ fn tick(
|
|||
const speed = 200.0 / upscale;
|
||||
player_pos.v[0] += direction.x() * speed * delta_time;
|
||||
player_pos.v[1] += direction.y() * speed * delta_time;
|
||||
try text.set(app.state().player, .transform, Mat4x4.scaleScalar(upscale).mul(&Mat4x4.translate(player_pos)));
|
||||
try text.set(app.state().player, .dirty, true);
|
||||
try text.set(app.player, .transform, Mat4x4.scaleScalar(upscale).mul(&Mat4x4.translate(player_pos)));
|
||||
try text.set(app.player, .dirty, true);
|
||||
text.schedule(.update);
|
||||
|
||||
// Perform pre-render work
|
||||
text_pipeline.schedule(.pre_render);
|
||||
|
||||
// Create a command encoder for this frame
|
||||
const label = @tagName(name) ++ ".tick";
|
||||
app.state().frame_encoder = core.state().device.createCommandEncoder(&.{ .label = label });
|
||||
const label = @tagName(mach_module) ++ ".tick";
|
||||
app.frame_encoder = core.device.createCommandEncoder(&.{ .label = label });
|
||||
|
||||
// Grab the back buffer of the swapchain
|
||||
// TODO(Core)
|
||||
const back_buffer_view = core.state().swap_chain.getCurrentTextureView().?;
|
||||
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
|
||||
defer back_buffer_view.release();
|
||||
|
||||
// Begin render pass
|
||||
|
|
@ -224,40 +213,37 @@ fn tick(
|
|||
.load_op = .clear,
|
||||
.store_op = .store,
|
||||
}};
|
||||
app.state().frame_render_pass = app.state().frame_encoder.beginRenderPass(&gpu.RenderPassDescriptor.init(.{
|
||||
app.frame_render_pass = app.frame_encoder.beginRenderPass(&gpu.RenderPassDescriptor.init(.{
|
||||
.label = label,
|
||||
.color_attachments = &color_attachments,
|
||||
}));
|
||||
|
||||
// Render our text batch
|
||||
text_pipeline.state().render_pass = app.state().frame_render_pass;
|
||||
text_pipeline.state().render_pass = app.frame_render_pass;
|
||||
text_pipeline.schedule(.render);
|
||||
|
||||
// Finish the frame once rendering is done.
|
||||
app.schedule(.end_frame);
|
||||
|
||||
app.state().time += delta_time;
|
||||
app.time += delta_time;
|
||||
}
|
||||
|
||||
fn endFrame(
|
||||
entities: *mach.Entities.Mod,
|
||||
app: *Mod,
|
||||
core: *mach.Core.Mod,
|
||||
app: *App,
|
||||
core: *mach.Core,
|
||||
) !void {
|
||||
// Finish render pass
|
||||
app.state().frame_render_pass.end();
|
||||
const label = @tagName(name) ++ ".endFrame";
|
||||
var command = app.state().frame_encoder.finish(&.{ .label = label });
|
||||
core.state().queue.submit(&[_]*gpu.CommandBuffer{command});
|
||||
app.frame_render_pass.end();
|
||||
const label = @tagName(mach_module) ++ ".endFrame";
|
||||
var command = app.frame_encoder.finish(&.{ .label = label });
|
||||
core.queue.submit(&[_]*gpu.CommandBuffer{command});
|
||||
command.release();
|
||||
app.state().frame_encoder.release();
|
||||
app.state().frame_render_pass.release();
|
||||
|
||||
// Present the frame
|
||||
core.schedule(.present_frame);
|
||||
app.frame_encoder.release();
|
||||
app.frame_render_pass.release();
|
||||
|
||||
// Every second, update the window title with the FPS
|
||||
if (app.state().fps_timer.read() >= 1.0) {
|
||||
if (app.fps_timer.read() >= 1.0) {
|
||||
// Gather some text rendering stats
|
||||
var num_texts: u32 = 0;
|
||||
var num_glyphs: usize = 0;
|
||||
|
|
@ -271,14 +257,14 @@ fn endFrame(
|
|||
}
|
||||
}
|
||||
|
||||
try core.state().printTitle(
|
||||
core.state().main_window,
|
||||
try core.printTitle(
|
||||
core.main_window,
|
||||
"text [ FPS: {d} ] [ Texts: {d} ] [ Glyphs: {d} ]",
|
||||
.{ app.state().frame_count, num_texts, num_glyphs },
|
||||
.{ app.frame_count, num_texts, num_glyphs },
|
||||
);
|
||||
core.schedule(.update);
|
||||
app.state().fps_timer.reset();
|
||||
app.state().frame_count = 0;
|
||||
app.fps_timer.reset();
|
||||
app.frame_count = 0;
|
||||
}
|
||||
app.state().frame_count += 1;
|
||||
app.frame_count += 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,22 @@
|
|||
const std = @import("std");
|
||||
const mach = @import("mach");
|
||||
|
||||
// The global list of Mach modules our application may use.
|
||||
pub const modules = .{
|
||||
// The set of Mach modules our application may use.
|
||||
const Modules = mach.Modules(.{
|
||||
mach.Core,
|
||||
mach.gfx.text_modules,
|
||||
@import("App.zig"),
|
||||
};
|
||||
});
|
||||
|
||||
// TODO: move this to a mach "entrypoint" zig module which handles nuances like WASM requires.
|
||||
pub fn main() !void {
|
||||
const allocator = std.heap.c_allocator;
|
||||
|
||||
// Initialize module system
|
||||
try mach.mods.init(allocator);
|
||||
// The set of Mach modules our application may use.
|
||||
var mods = Modules.init(allocator);
|
||||
// TODO: enable mods.deinit(allocator); for allocator leak detection
|
||||
// defer mods.deinit(allocator);
|
||||
|
||||
// Schedule .app.start to run.
|
||||
mach.mods.schedule(.app, .start);
|
||||
|
||||
// Dispatch systems forever or until there are none left to dispatch. If your app uses mach.Core
|
||||
// then this will block forever and never return.
|
||||
try mach.mods.dispatch(.{});
|
||||
const app = mods.get(.app);
|
||||
app.run(.main);
|
||||
}
|
||||
|
|
|
|||
704
src/Core.zig
704
src/Core.zig
|
|
@ -6,6 +6,8 @@ const mach = @import("main.zig");
|
|||
const gpu = mach.gpu;
|
||||
const log = std.log.scoped(.mach);
|
||||
|
||||
const Core = @This();
|
||||
|
||||
// Whether or not you can drive the main loop in a non-blocking fashion, or if the underlying
|
||||
// platform must take control and drive the main loop itself.
|
||||
pub const supports_non_blocking = switch (build_options.core_platform) {
|
||||
|
|
@ -26,102 +28,45 @@ const EventQueue = std.fifo.LinearFifo(Event, .Dynamic);
|
|||
/// A panic will occur if `supports_non_blocking == false` for the platform.
|
||||
pub var non_blocking = false;
|
||||
|
||||
pub const name = .mach_core;
|
||||
pub const mach_module = .mach_core;
|
||||
|
||||
pub const Mod = mach.Mod(@This());
|
||||
pub const mach_systems = .{ .main, .init, .presentFrame, .deinit };
|
||||
|
||||
pub const systems = .{
|
||||
.init = .{ .handler = init, .description =
|
||||
\\ Initialize mach.Core
|
||||
},
|
||||
windows: mach.Objects(struct {
|
||||
// Window title string
|
||||
// TODO: document how to set this using a format string
|
||||
// TODO: allocation/free strategy
|
||||
title: []const u8,
|
||||
|
||||
.start = .{ .handler = start, .description =
|
||||
\\ Indicates mach.Core should start its loop and begin scheduling your .app.tick system to run.
|
||||
\\
|
||||
\\ You should register core.state().on_tick and core.state().on_exit callbacks before scheduling
|
||||
\\ this to run.
|
||||
},
|
||||
// Texture format of the framebuffer (read-only)
|
||||
framebuffer_format: gpu.Texture.Format,
|
||||
|
||||
.update = .{ .handler = update, .description =
|
||||
\\ TODO
|
||||
},
|
||||
// Width of the framebuffer in texels (read-only)
|
||||
framebuffer_width: u32,
|
||||
|
||||
.present_frame = .{ .handler = presentFrame, .description =
|
||||
\\ Send this when rendering has finished and the swapchain should be presented.
|
||||
},
|
||||
// Height of the framebuffer in texels (read-only)
|
||||
framebuffer_height: u32,
|
||||
|
||||
.exit = .{ .handler = exit, .description =
|
||||
\\ Send this when you would like to exit the application.
|
||||
\\
|
||||
\\ When the next .present_frame runs, then core.state().on_exit will be scheduled to run giving
|
||||
\\ your app a chance to deinitialize itself after the last frame has been rendered, and
|
||||
\\ core.state().on_tick will no longer be sent.
|
||||
\\
|
||||
\\ When core.state().on_exit runs, it must schedule .mach_core.deinit to run which will cause
|
||||
\\ the app to finish.
|
||||
},
|
||||
// Width of the window in virtual pixels (read-only)
|
||||
width: u32,
|
||||
|
||||
.deinit = .{ .handler = deinit, .description =
|
||||
\\ Send this once your app is fully deinitialized and you are ready for mach.Core to exit for
|
||||
\\ good.
|
||||
},
|
||||
// Height of the window in virtual pixels (read-only)
|
||||
height: u32,
|
||||
|
||||
.started = .{ .handler = fn () void, .description =
|
||||
\\ An interrupt signal that mach.Core sends once it has started. This is an interrupt signal to
|
||||
\\ be used by the application entrypoint.
|
||||
},
|
||||
/// Whether the window is fullscreen (read-only)
|
||||
fullscreen: bool,
|
||||
}),
|
||||
|
||||
.frame_finished = .{ .handler = fn () void, .description =
|
||||
\\ An interrupt signal that mach.Core sends once a frame has been finished. This is an interrupt
|
||||
\\ signal to be used by the application entrypoint.
|
||||
},
|
||||
};
|
||||
|
||||
pub const components = .{
|
||||
.title = .{ .type = [:0]u8, .description =
|
||||
\\ Window title slice. Can be set with a format string and arguments via:
|
||||
\\
|
||||
\\ ```
|
||||
\\ try core.state().printTitle(core_mod.state().main_window, "Hello, {s}!", .{"Mach"});
|
||||
\\ ```
|
||||
\\
|
||||
\\ If setting this component yourself, ensure the buffer is allocated using core.state().allocator
|
||||
\\ as it will be freed for you as part of the .deinit event.
|
||||
},
|
||||
|
||||
.framebuffer_format = .{ .type = gpu.Texture.Format, .description =
|
||||
\\ The texture format of the framebuffer
|
||||
},
|
||||
|
||||
.framebuffer_width = .{ .type = u32, .description =
|
||||
\\ The width of the framebuffer in texels
|
||||
},
|
||||
|
||||
.framebuffer_height = .{ .type = u32, .description =
|
||||
\\ The height of the framebuffer in texels
|
||||
},
|
||||
|
||||
.width = .{ .type = u32, .description =
|
||||
\\ The width of the window in virtual pixels
|
||||
},
|
||||
|
||||
.height = .{ .type = u32, .description =
|
||||
\\ The height of the window in virtual pixels
|
||||
},
|
||||
|
||||
.fullscreen = .{ .type = bool, .description =
|
||||
\\ Whether the window should be fullscreen (only respected at .start time)
|
||||
},
|
||||
};
|
||||
global: mach.Object(struct {}),
|
||||
|
||||
/// Callback system invoked per tick (e.g. per-frame)
|
||||
on_tick: ?mach.AnySystem = null,
|
||||
on_tick: ?mach.FunctionID = null,
|
||||
|
||||
/// Callback system invoked when application is exiting
|
||||
on_exit: ?mach.AnySystem = null,
|
||||
on_exit: ?mach.FunctionID = null,
|
||||
|
||||
/// Main window of the application
|
||||
main_window: mach.EntityID,
|
||||
main_window: mach.ObjectID,
|
||||
|
||||
/// Current state of the application
|
||||
state: enum {
|
||||
|
|
@ -153,12 +98,7 @@ events: EventQueue,
|
|||
input_state: InputState,
|
||||
oom: std.Thread.ResetEvent = .{},
|
||||
|
||||
fn update(core: *Mod, entities: *mach.Entities.Mod) !void {
|
||||
_ = core;
|
||||
_ = entities;
|
||||
}
|
||||
|
||||
fn init(core: *Mod, entities: *mach.Entities.Mod) !void {
|
||||
pub fn init(core: *Core) !void {
|
||||
// TODO: this needs to be removed.
|
||||
const options: InitOptions = .{
|
||||
.allocator = std.heap.c_allocator,
|
||||
|
|
@ -168,10 +108,15 @@ fn init(core: *Mod, entities: *mach.Entities.Mod) !void {
|
|||
// TODO: fix all leaks and use options.allocator
|
||||
try mach.sysgpu.Impl.init(allocator, .{});
|
||||
|
||||
const main_window = try entities.new();
|
||||
try core.set(main_window, .fullscreen, false);
|
||||
try core.set(main_window, .width, 1920 / 2);
|
||||
try core.set(main_window, .height, 1080 / 2);
|
||||
const main_window = try core.windows.new(.{
|
||||
.title = options.title, // TODO
|
||||
.framebuffer_format = undefined, // TODO: null?
|
||||
.framebuffer_width = undefined, // TODO: null?
|
||||
.framebuffer_height = undefined, // TODO: null?
|
||||
.width = 1920 / 2,
|
||||
.height = 1080 / 2,
|
||||
.fullscreen = false,
|
||||
});
|
||||
|
||||
// Copy window title into owned buffer.
|
||||
var title: [256:0]u8 = undefined;
|
||||
|
|
@ -185,7 +130,12 @@ fn init(core: *Mod, entities: *mach.Entities.Mod) !void {
|
|||
|
||||
// TODO: remove undefined initialization (disgusting!)
|
||||
const platform: Platform = undefined;
|
||||
core.init(.{
|
||||
core.* = .{
|
||||
// TODO: this is a good example of why not *all* state fields should be allowed, must copy
|
||||
// the ones mach initialized
|
||||
.windows = core.windows,
|
||||
.global = core.global,
|
||||
|
||||
.allocator = allocator,
|
||||
.main_window = main_window,
|
||||
.events = events,
|
||||
|
|
@ -204,20 +154,19 @@ fn init(core: *Mod, entities: *mach.Entities.Mod) !void {
|
|||
.surface = undefined,
|
||||
.swap_chain = undefined,
|
||||
.descriptor = undefined,
|
||||
});
|
||||
const state = core.state();
|
||||
};
|
||||
|
||||
try Platform.init(&state.platform, core, options);
|
||||
try Platform.init(&core.platform, core, options);
|
||||
|
||||
state.instance = gpu.createInstance(null) orelse {
|
||||
core.instance = gpu.createInstance(null) orelse {
|
||||
log.err("failed to create GPU instance", .{});
|
||||
std.process.exit(1);
|
||||
};
|
||||
state.surface = state.instance.createSurface(&state.platform.surface_descriptor);
|
||||
core.surface = core.instance.createSurface(&core.platform.surface_descriptor);
|
||||
|
||||
var response: RequestAdapterResponse = undefined;
|
||||
state.instance.requestAdapter(&gpu.RequestAdapterOptions{
|
||||
.compatible_surface = state.surface,
|
||||
core.instance.requestAdapter(&gpu.RequestAdapterOptions{
|
||||
.compatible_surface = core.surface,
|
||||
.power_preference = options.power_preference,
|
||||
.force_fallback_adapter = .false,
|
||||
}, &response, requestAdapterCallback);
|
||||
|
|
@ -241,10 +190,10 @@ fn init(core: *Mod, entities: *mach.Entities.Mod) !void {
|
|||
props.driver_description,
|
||||
});
|
||||
|
||||
state.adapter = response.adapter.?;
|
||||
core.adapter = response.adapter.?;
|
||||
|
||||
// Create a device with default limits/features.
|
||||
state.device = response.adapter.?.createDevice(&.{
|
||||
core.device = response.adapter.?.createDevice(&.{
|
||||
.required_features_count = if (options.required_features) |v| @as(u32, @intCast(v.len)) else 0,
|
||||
.required_features = if (options.required_features) |v| @as(?[*]const gpu.FeatureName, v.ptr) else null,
|
||||
.required_limits = if (options.required_limits) |limits| @as(?*const gpu.RequiredLimits, &gpu.RequiredLimits{
|
||||
|
|
@ -256,10 +205,10 @@ fn init(core: *Mod, entities: *mach.Entities.Mod) !void {
|
|||
log.err("failed to create GPU device\n", .{});
|
||||
std.process.exit(1);
|
||||
};
|
||||
state.device.setUncapturedErrorCallback({}, printUnhandledErrorCallback);
|
||||
state.queue = state.device.getQueue();
|
||||
core.device.setUncapturedErrorCallback({}, printUnhandledErrorCallback);
|
||||
core.queue = core.device.getQueue();
|
||||
|
||||
state.descriptor = gpu.SwapChain.Descriptor{
|
||||
core.descriptor = gpu.SwapChain.Descriptor{
|
||||
.label = "main swap chain",
|
||||
.usage = options.swap_chain_usage,
|
||||
.format = .bgra8_unorm,
|
||||
|
|
@ -271,30 +220,34 @@ fn init(core: *Mod, entities: *mach.Entities.Mod) !void {
|
|||
.triple => .mailbox,
|
||||
},
|
||||
};
|
||||
state.swap_chain = state.device.createSwapChain(state.surface, &state.descriptor);
|
||||
core.swap_chain = core.device.createSwapChain(core.surface, &core.descriptor);
|
||||
|
||||
// TODO(important): update this information upon framebuffer resize events
|
||||
try core.set(state.main_window, .framebuffer_format, state.descriptor.format);
|
||||
try core.set(state.main_window, .framebuffer_width, state.descriptor.width);
|
||||
try core.set(state.main_window, .framebuffer_height, state.descriptor.height);
|
||||
try core.set(state.main_window, .width, state.platform.size.width);
|
||||
try core.set(state.main_window, .height, state.platform.size.height);
|
||||
var w = core.windows.get(core.main_window).?;
|
||||
w.framebuffer_format = core.descriptor.format;
|
||||
w.framebuffer_width = core.descriptor.width;
|
||||
w.framebuffer_height = core.descriptor.height;
|
||||
w.width = core.platform.size.width;
|
||||
w.height = core.platform.size.height;
|
||||
core.windows.set(core.main_window, w);
|
||||
|
||||
state.frame = .{ .target = 0 };
|
||||
state.input = .{ .target = 1 };
|
||||
try state.frame.start();
|
||||
try state.input.start();
|
||||
core.frame = .{ .target = 0 };
|
||||
core.input = .{ .target = 1 };
|
||||
try core.frame.start();
|
||||
try core.input.start();
|
||||
}
|
||||
|
||||
pub fn start(core: *Mod) !void {
|
||||
if (core.state().on_tick == null) @panic("core.state().on_tick callback system must be registered");
|
||||
if (core.state().on_exit == null) @panic("core.state().on_exit callback system must be registered");
|
||||
pub fn tick(core: *Core, present_frame: mach.Call(Core, .present_frame), runner: mach.Runner) void {
|
||||
runner.run(core.on_tick.?);
|
||||
runner.run(present_frame.id);
|
||||
}
|
||||
|
||||
// Signal that mach.Core has started.
|
||||
core.schedule(.started);
|
||||
pub fn main(core: *Core, present_frame: mach.Call(Core, .presentFrame), runner: mach.Runner) !void {
|
||||
if (core.on_tick == null) @panic("core.on_tick callback must be set");
|
||||
if (core.on_exit == null) @panic("core.on_exit callback must be set");
|
||||
|
||||
// Schedule the next app tick to run.
|
||||
core.scheduleAny(core.state().on_tick.?);
|
||||
runner.run(core.on_tick.?);
|
||||
runner.run(present_frame.id);
|
||||
|
||||
// If the user doesn't want mach.Core to take control of the main loop, we bail out - the next
|
||||
// app tick is already scheduled to run in the future and they'll .present_frame to return
|
||||
|
|
@ -310,14 +263,16 @@ pub fn start(core: *Mod) !void {
|
|||
// The user wants mach.Core to take control of the main loop.
|
||||
if (supports_non_blocking) {
|
||||
while (core.state().state != .exited) {
|
||||
dispatch();
|
||||
runner.run(core.on_tick.?);
|
||||
runner.run(present_frame.id);
|
||||
}
|
||||
|
||||
// Don't return, because Platform.run wouldn't either (marked noreturn due to underlying
|
||||
// platform APIs never returning.)
|
||||
std.process.exit(0);
|
||||
} else {
|
||||
// Platform drives the main loop.
|
||||
Platform.run(platform_update_callback, .{&mach.mods.mod.mach_core});
|
||||
Platform.run(platform_update_callback, .{ core, present_frame.id, runner });
|
||||
|
||||
// Platform.run should be marked noreturn, so this shouldn't ever run. But just in case we
|
||||
// accidentally introduce a different Platform.run in the future, we put an exit here for
|
||||
|
|
@ -326,46 +281,39 @@ pub fn start(core: *Mod) !void {
|
|||
}
|
||||
}
|
||||
|
||||
fn dispatch() void {
|
||||
mach.mods.dispatchUntil(.mach_core, .frame_finished) catch {
|
||||
@panic("Dispatch in Core failed");
|
||||
};
|
||||
fn platform_update_callback(core: *Core, present_frame: mach.FunctionID, runner: mach.Runner) !bool {
|
||||
runner.run(core.on_tick.?);
|
||||
runner.run(present_frame);
|
||||
|
||||
return core.state != .exited;
|
||||
}
|
||||
|
||||
fn platform_update_callback(core: *Mod) !bool {
|
||||
// Execute systems until .mach_core.frame_finished is dispatched, signalling a frame was
|
||||
// finished.
|
||||
try mach.mods.dispatchUntil(.mach_core, .frame_finished);
|
||||
pub fn deinit(core: *Core) !void {
|
||||
core.state = .exited;
|
||||
|
||||
return core.state().state != .exited;
|
||||
}
|
||||
|
||||
pub fn deinit(entities: *mach.Entities.Mod, core: *Mod) !void {
|
||||
const state = core.state();
|
||||
state.state = .exited;
|
||||
|
||||
var q = try entities.query(.{
|
||||
.titles = Mod.read(.title),
|
||||
});
|
||||
while (q.next()) |v| {
|
||||
for (v.titles) |title| {
|
||||
state.allocator.free(title);
|
||||
}
|
||||
}
|
||||
// TODO(object)(window-title)
|
||||
// var q = try entities.query(.{
|
||||
// .titles = Mod.read(.title),
|
||||
// });
|
||||
// while (q.next()) |v| {
|
||||
// for (v.titles) |title| {
|
||||
// state.allocator.free(title);
|
||||
// }
|
||||
// }
|
||||
|
||||
// GPU backend must be released BEFORE platform deinit, otherwise we may enter a race
|
||||
// where the GPU might try to present to the window server.
|
||||
state.swap_chain.release();
|
||||
state.queue.release();
|
||||
state.device.release();
|
||||
state.surface.release();
|
||||
state.adapter.release();
|
||||
state.instance.release();
|
||||
core.swap_chain.release();
|
||||
core.queue.release();
|
||||
core.device.release();
|
||||
core.surface.release();
|
||||
core.adapter.release();
|
||||
core.instance.release();
|
||||
|
||||
// Deinit the platform
|
||||
state.platform.deinit();
|
||||
core.platform.deinit();
|
||||
|
||||
state.events.deinit();
|
||||
core.events.deinit();
|
||||
}
|
||||
|
||||
/// Returns the next event until there are no more available. You should check for events during
|
||||
|
|
@ -409,42 +357,49 @@ pub fn outOfMemory(core: *@This()) bool {
|
|||
return true;
|
||||
}
|
||||
|
||||
/// Sets the window title. The string must be owned by Core, and will not be copied or freed. It is
|
||||
/// advised to use the `core.title` buffer for this purpose, e.g.:
|
||||
///
|
||||
/// ```
|
||||
/// const title = try std.fmt.bufPrintZ(&core.title, "Hello, world!", .{});
|
||||
/// core.setTitle(title);
|
||||
/// ```
|
||||
pub inline fn setTitle(core: *@This(), value: [:0]const u8) void {
|
||||
return core.platform.setTitle(value);
|
||||
}
|
||||
// TODO(object)
|
||||
// /// Sets the window title. The string must be owned by Core, and will not be copied or freed. It is
|
||||
// /// advised to use the `core.title` buffer for this purpose, e.g.:
|
||||
// ///
|
||||
// /// ```
|
||||
// /// const title = try std.fmt.bufPrintZ(&core.title, "Hello, world!", .{});
|
||||
// /// core.setTitle(title);
|
||||
// /// ```
|
||||
// pub inline fn setTitle(core: *@This(), value: [:0]const u8) void {
|
||||
// return core.platform.setTitle(value);
|
||||
// }
|
||||
|
||||
/// Set the window mode
|
||||
pub inline fn setDisplayMode(core: *@This(), mode: DisplayMode) void {
|
||||
return core.platform.setDisplayMode(mode);
|
||||
}
|
||||
// TODO(object)
|
||||
// /// Set the window mode
|
||||
// pub inline fn setDisplayMode(core: *@This(), mode: DisplayMode) void {
|
||||
// return core.platform.setDisplayMode(mode);
|
||||
// }
|
||||
|
||||
/// Returns the window mode
|
||||
pub inline fn displayMode(core: *@This()) DisplayMode {
|
||||
return core.platform.display_mode;
|
||||
}
|
||||
// TODO(object)
|
||||
// /// Returns the window mode
|
||||
// pub inline fn displayMode(core: *@This()) DisplayMode {
|
||||
// return core.platform.display_mode;
|
||||
// }
|
||||
|
||||
pub inline fn setBorder(core: *@This(), value: bool) void {
|
||||
return core.platform.setBorder(value);
|
||||
}
|
||||
// TODO(object)
|
||||
// pub inline fn setBorder(core: *@This(), value: bool) void {
|
||||
// return core.platform.setBorder(value);
|
||||
// }
|
||||
|
||||
pub inline fn border(core: *@This()) bool {
|
||||
return core.platform.border;
|
||||
}
|
||||
// TODO(object)
|
||||
// pub inline fn border(core: *@This()) bool {
|
||||
// return core.platform.border;
|
||||
// }
|
||||
|
||||
pub inline fn setHeadless(core: *@This(), value: bool) void {
|
||||
return core.platform.setHeadless(value);
|
||||
}
|
||||
// TODO(object)
|
||||
// pub inline fn setHeadless(core: *@This(), value: bool) void {
|
||||
// return core.platform.setHeadless(value);
|
||||
// }
|
||||
|
||||
pub inline fn headless(core: *@This()) bool {
|
||||
return core.platform.headless;
|
||||
}
|
||||
// TODO(object)
|
||||
// pub inline fn headless(core: *@This()) bool {
|
||||
// return core.platform.headless;
|
||||
// }
|
||||
|
||||
pub fn keyPressed(core: *@This(), key: Key) bool {
|
||||
return core.input_state.isKeyPressed(key);
|
||||
|
|
@ -466,230 +421,246 @@ pub fn mousePosition(core: *@This()) Position {
|
|||
return core.input_state.mouse_position;
|
||||
}
|
||||
|
||||
/// Set refresh rate synchronization mode. Default `.triple`
|
||||
///
|
||||
/// Calling this function also implicitly calls setFrameRateLimit for you:
|
||||
/// ```
|
||||
/// .none => setFrameRateLimit(0) // unlimited
|
||||
/// .double => setFrameRateLimit(0) // unlimited
|
||||
/// .triple => setFrameRateLimit(2 * max_monitor_refresh_rate)
|
||||
/// ```
|
||||
pub inline fn setVSync(core: *@This(), mode: VSyncMode) void {
|
||||
return core.platform.setVSync(mode);
|
||||
}
|
||||
// TODO(object)
|
||||
// /// Set refresh rate synchronization mode. Default `.triple`
|
||||
// ///
|
||||
// /// Calling this function also implicitly calls setFrameRateLimit for you:
|
||||
// /// ```
|
||||
// /// .none => setFrameRateLimit(0) // unlimited
|
||||
// /// .double => setFrameRateLimit(0) // unlimited
|
||||
// /// .triple => setFrameRateLimit(2 * max_monitor_refresh_rate)
|
||||
// /// ```
|
||||
// pub inline fn setVSync(core: *@This(), mode: VSyncMode) void {
|
||||
// return core.platform.setVSync(mode);
|
||||
// }
|
||||
|
||||
/// Returns refresh rate synchronization mode.
|
||||
pub inline fn vsync(core: *@This()) VSyncMode {
|
||||
return core.platform.vsync_mode;
|
||||
}
|
||||
// TODO(object)
|
||||
// /// Returns refresh rate synchronization mode.
|
||||
// pub inline fn vsync(core: *@This()) VSyncMode {
|
||||
// return core.platform.vsync_mode;
|
||||
// }
|
||||
|
||||
/// Sets the frame rate limit. Default 0 (unlimited)
|
||||
///
|
||||
/// This is applied *in addition* to the vsync mode.
|
||||
pub inline fn setFrameRateLimit(core: *@This(), limit: u32) void {
|
||||
core.frame.target = limit;
|
||||
}
|
||||
// TODO(object)
|
||||
// /// Sets the frame rate limit. Default 0 (unlimited)
|
||||
// ///
|
||||
// /// This is applied *in addition* to the vsync mode.
|
||||
// pub inline fn setFrameRateLimit(core: *@This(), limit: u32) void {
|
||||
// core.frame.target = limit;
|
||||
// }
|
||||
|
||||
/// Returns the frame rate limit, or zero if unlimited.
|
||||
pub inline fn frameRateLimit(core: *@This()) u32 {
|
||||
return core.frame.target;
|
||||
}
|
||||
// TODO(object)
|
||||
// /// Returns the frame rate limit, or zero if unlimited.
|
||||
// pub inline fn frameRateLimit(core: *@This()) u32 {
|
||||
// return core.frame.target;
|
||||
// }
|
||||
|
||||
/// Set the window size, in subpixel units.
|
||||
pub inline fn setSize(core: *@This(), value: Size) void {
|
||||
return core.platform.setSize(value);
|
||||
}
|
||||
// TODO(object)
|
||||
// /// Set the window size, in subpixel units.
|
||||
// pub inline fn setSize(core: *@This(), value: Size) void {
|
||||
// return core.platform.setSize(value);
|
||||
// }
|
||||
|
||||
/// Returns the window size, in subpixel units.
|
||||
pub inline fn size(core: *@This()) Size {
|
||||
return core.platform.size;
|
||||
}
|
||||
// TODO(object)
|
||||
// /// Returns the window size, in subpixel units.
|
||||
// pub inline fn size(core: *@This()) Size {
|
||||
// return core.platform.size;
|
||||
// }
|
||||
|
||||
pub inline fn setCursorMode(core: *@This(), mode: CursorMode) void {
|
||||
return core.platform.setCursorMode(mode);
|
||||
}
|
||||
// TODO(object)
|
||||
// pub inline fn setCursorMode(core: *@This(), mode: CursorMode) void {
|
||||
// return core.platform.setCursorMode(mode);
|
||||
// }
|
||||
|
||||
pub inline fn cursorMode(core: *@This()) CursorMode {
|
||||
return core.platform.cursorMode();
|
||||
}
|
||||
// TODO(object)
|
||||
// pub inline fn cursorMode(core: *@This()) CursorMode {
|
||||
// return core.platform.cursorMode();
|
||||
// }
|
||||
|
||||
pub inline fn setCursorShape(core: *@This(), cursor: CursorShape) void {
|
||||
return core.platform.setCursorShape(cursor);
|
||||
}
|
||||
// TODO(object)
|
||||
// pub inline fn setCursorShape(core: *@This(), cursor: CursorShape) void {
|
||||
// return core.platform.setCursorShape(cursor);
|
||||
// }
|
||||
|
||||
pub inline fn cursorShape(core: *@This()) CursorShape {
|
||||
return core.platform.cursorShape();
|
||||
}
|
||||
// TODO(object)
|
||||
// pub inline fn cursorShape(core: *@This()) CursorShape {
|
||||
// return core.platform.cursorShape();
|
||||
// }
|
||||
|
||||
/// Sets the minimum target frequency of the input handling thread.
|
||||
///
|
||||
/// Input handling (the main thread) runs at a variable frequency. The thread blocks until there are
|
||||
/// input events available, or until it needs to unblock in order to achieve the minimum target
|
||||
/// frequency which is your collaboration point of opportunity with the main thread.
|
||||
///
|
||||
/// For example, by default (`setInputFrequency(1)`) mach-core will aim to invoke `updateMainThread`
|
||||
/// at least once per second (but potentially much more, e.g. once per every mouse movement or
|
||||
/// keyboard button press.) If you were to increase the input frequency to say 60hz e.g.
|
||||
/// `setInputFrequency(60)` then mach-core will aim to invoke your `updateMainThread` 60 times per
|
||||
/// second.
|
||||
///
|
||||
/// An input frequency of zero implies unlimited, in which case the main thread will busy-wait.
|
||||
///
|
||||
/// # Multithreaded mach-core behavior
|
||||
///
|
||||
/// On some platforms, mach-core is able to handle input and rendering independently for
|
||||
/// improved performance and responsiveness.
|
||||
///
|
||||
/// | Platform | Threading |
|
||||
/// |----------|-----------------|
|
||||
/// | Desktop | Multi threaded |
|
||||
/// | Browser | Single threaded |
|
||||
/// | Mobile | TBD |
|
||||
///
|
||||
/// On single-threaded platforms, `update` and the (optional) `updateMainThread` callback are
|
||||
/// invoked in sequence, one after the other, on the same thread.
|
||||
///
|
||||
/// On multi-threaded platforms, `init` and `deinit` are called on the main thread, while `update`
|
||||
/// is called on a separate rendering thread. The (optional) `updateMainThread` callback can be
|
||||
/// used in cases where you must run a function on the main OS thread (such as to open a native
|
||||
/// file dialog on macOS, since many system GUI APIs must be run on the main OS thread.) It is
|
||||
/// advised you do not use this callback to run any code except when absolutely neccessary, as
|
||||
/// it is in direct contention with input handling.
|
||||
///
|
||||
/// APIs which are not accessible from a specific thread are declared as such, otherwise can be
|
||||
/// called from any thread as they are internally synchronized.
|
||||
pub inline fn setInputFrequency(core: *@This(), input_frequency: u32) void {
|
||||
core.input.target = input_frequency;
|
||||
}
|
||||
// TODO(object)
|
||||
// /// Sets the minimum target frequency of the input handling thread.
|
||||
// ///
|
||||
// /// Input handling (the main thread) runs at a variable frequency. The thread blocks until there are
|
||||
// /// input events available, or until it needs to unblock in order to achieve the minimum target
|
||||
// /// frequency which is your collaboration point of opportunity with the main thread.
|
||||
// ///
|
||||
// /// For example, by default (`setInputFrequency(1)`) mach-core will aim to invoke `updateMainThread`
|
||||
// /// at least once per second (but potentially much more, e.g. once per every mouse movement or
|
||||
// /// keyboard button press.) If you were to increase the input frequency to say 60hz e.g.
|
||||
// /// `setInputFrequency(60)` then mach-core will aim to invoke your `updateMainThread` 60 times per
|
||||
// /// second.
|
||||
// ///
|
||||
// /// An input frequency of zero implies unlimited, in which case the main thread will busy-wait.
|
||||
// ///
|
||||
// /// # Multithreaded mach-core behavior
|
||||
// ///
|
||||
// /// On some platforms, mach-core is able to handle input and rendering independently for
|
||||
// /// improved performance and responsiveness.
|
||||
// ///
|
||||
// /// | Platform | Threading |
|
||||
// /// |----------|-----------------|
|
||||
// /// | Desktop | Multi threaded |
|
||||
// /// | Browser | Single threaded |
|
||||
// /// | Mobile | TBD |
|
||||
// ///
|
||||
// /// On single-threaded platforms, `update` and the (optional) `updateMainThread` callback are
|
||||
// /// invoked in sequence, one after the other, on the same thread.
|
||||
// ///
|
||||
// /// On multi-threaded platforms, `init` and `deinit` are called on the main thread, while `update`
|
||||
// /// is called on a separate rendering thread. The (optional) `updateMainThread` callback can be
|
||||
// /// used in cases where you must run a function on the main OS thread (such as to open a native
|
||||
// /// file dialog on macOS, since many system GUI APIs must be run on the main OS thread.) It is
|
||||
// /// advised you do not use this callback to run any code except when absolutely neccessary, as
|
||||
// /// it is in direct contention with input handling.
|
||||
// ///
|
||||
// /// APIs which are not accessible from a specific thread are declared as such, otherwise can be
|
||||
// /// called from any thread as they are internally synchronized.
|
||||
// pub inline fn setInputFrequency(core: *@This(), input_frequency: u32) void {
|
||||
// core.input.target = input_frequency;
|
||||
// }
|
||||
|
||||
/// Returns the input frequency, or zero if unlimited (busy-waiting mode)
|
||||
pub inline fn inputFrequency(core: *@This()) u32 {
|
||||
return core.input.target;
|
||||
}
|
||||
// TODO(object)
|
||||
// /// Returns the input frequency, or zero if unlimited (busy-waiting mode)
|
||||
// pub inline fn inputFrequency(core: *@This()) u32 {
|
||||
// return core.input.target;
|
||||
// }
|
||||
|
||||
/// Returns the actual number of frames rendered (`update` calls that returned) in the last second.
|
||||
///
|
||||
/// This is updated once per second.
|
||||
pub inline fn frameRate(core: *@This()) u32 {
|
||||
return core.frame.rate;
|
||||
}
|
||||
// TODO(object)
|
||||
// /// Returns the actual number of frames rendered (`update` calls that returned) in the last second.
|
||||
// ///
|
||||
// /// This is updated once per second.
|
||||
// pub inline fn frameRate(core: *@This()) u32 {
|
||||
// return core.frame.rate;
|
||||
// }
|
||||
|
||||
/// Returns the actual number of input thread iterations in the last second. See setInputFrequency
|
||||
/// for what this means.
|
||||
///
|
||||
/// This is updated once per second.
|
||||
pub inline fn inputRate(core: *@This()) u32 {
|
||||
return core.input.rate;
|
||||
}
|
||||
// TODO(object)
|
||||
// /// Returns the actual number of input thread iterations in the last second. See setInputFrequency
|
||||
// /// for what this means.
|
||||
// ///
|
||||
// /// This is updated once per second.
|
||||
// pub inline fn inputRate(core: *@This()) u32 {
|
||||
// return core.input.rate;
|
||||
// }
|
||||
|
||||
/// Returns the underlying native NSWindow pointer
|
||||
///
|
||||
/// May only be called on macOS.
|
||||
pub fn nativeWindowCocoa(core: *@This()) *anyopaque {
|
||||
return core.platform.nativeWindowCocoa();
|
||||
}
|
||||
// TODO(object)
|
||||
// /// Returns the underlying native NSWindow pointer
|
||||
// ///
|
||||
// /// May only be called on macOS.
|
||||
// pub fn nativeWindowCocoa(core: *@This()) *anyopaque {
|
||||
// return core.platform.nativeWindowCocoa();
|
||||
// }
|
||||
|
||||
/// Returns the underlying native Windows' HWND pointer
|
||||
///
|
||||
/// May only be called on Windows.
|
||||
pub fn nativeWindowWin32(core: *@This()) std.os.windows.HWND {
|
||||
return core.platform.nativeWindowWin32();
|
||||
}
|
||||
// TODO(object)
|
||||
// /// Returns the underlying native Windows' HWND pointer
|
||||
// ///
|
||||
// /// May only be called on Windows.
|
||||
// pub fn nativeWindowWin32(core: *@This()) std.os.windows.HWND {
|
||||
// return core.platform.nativeWindowWin32();
|
||||
// }
|
||||
|
||||
fn presentFrame(core: *Mod, entities: *mach.Entities.Mod) !void {
|
||||
const state: *@This() = core.state();
|
||||
pub fn presentFrame(core: *Core, core_deinit: mach.Call(Core, .deinit), runner: mach.Runner) !void {
|
||||
// TODO(object)(window-title)
|
||||
// // Update windows title
|
||||
// var num_windows: usize = 0;
|
||||
// var q = try entities.query(.{
|
||||
// .ids = mach.Entities.Mod.read(.id),
|
||||
// .titles = Mod.read(.title),
|
||||
// });
|
||||
// while (q.next()) |v| {
|
||||
// for (v.ids, v.titles) |_, title| {
|
||||
// num_windows += 1;
|
||||
// state.platform.setTitle(title);
|
||||
// }
|
||||
// }
|
||||
// if (num_windows > 1) @panic("mach: Core currently only supports a single window");
|
||||
|
||||
// Update windows title
|
||||
var num_windows: usize = 0;
|
||||
var q = try entities.query(.{
|
||||
.ids = mach.Entities.Mod.read(.id),
|
||||
.titles = Mod.read(.title),
|
||||
});
|
||||
while (q.next()) |v| {
|
||||
for (v.ids, v.titles) |_, title| {
|
||||
num_windows += 1;
|
||||
state.platform.setTitle(title);
|
||||
}
|
||||
}
|
||||
if (num_windows > 1) @panic("mach: Core currently only supports a single window");
|
||||
|
||||
_ = try state.platform.update();
|
||||
_ = try core.platform.update();
|
||||
mach.sysgpu.Impl.deviceTick(state.device);
|
||||
state.swap_chain.present();
|
||||
core.swap_chain.present();
|
||||
|
||||
// Update swapchain for the next frame
|
||||
if (state.swap_chain_update.isSet()) blk: {
|
||||
state.swap_chain_update.reset();
|
||||
if (core.swap_chain_update.isSet()) blk: {
|
||||
core.swap_chain_update.reset();
|
||||
|
||||
switch (state.platform.vsync_mode) {
|
||||
.triple => state.frame.target = 2 * state.platform.refresh_rate,
|
||||
else => state.frame.target = 0,
|
||||
switch (core.platform.vsync_mode) {
|
||||
.triple => core.frame.target = 2 * core.platform.refresh_rate,
|
||||
else => core.frame.target = 0,
|
||||
}
|
||||
|
||||
if (state.platform.size.width == 0 or state.platform.size.height == 0) break :blk;
|
||||
if (core.platform.size.width == 0 or core.platform.size.height == 0) break :blk;
|
||||
|
||||
state.descriptor.present_mode = switch (state.platform.vsync_mode) {
|
||||
core.descriptor.present_mode = switch (core.platform.vsync_mode) {
|
||||
.none => .immediate,
|
||||
.double => .fifo,
|
||||
.triple => .mailbox,
|
||||
};
|
||||
state.descriptor.width = @intCast(state.platform.size.width);
|
||||
state.descriptor.height = @intCast(state.platform.size.height);
|
||||
state.swap_chain.release();
|
||||
state.swap_chain = state.device.createSwapChain(state.surface, &state.descriptor);
|
||||
core.descriptor.width = @intCast(core.platform.size.width);
|
||||
core.descriptor.height = @intCast(core.platform.size.height);
|
||||
core.swap_chain.release();
|
||||
core.swap_chain = core.device.createSwapChain(core.surface, &core.descriptor);
|
||||
}
|
||||
|
||||
// TODO(important): update this information in response to resize events rather than
|
||||
// after frame submission
|
||||
try core.set(state.main_window, .framebuffer_format, state.descriptor.format);
|
||||
try core.set(state.main_window, .framebuffer_width, state.descriptor.width);
|
||||
try core.set(state.main_window, .framebuffer_height, state.descriptor.height);
|
||||
try core.set(state.main_window, .width, state.platform.size.width);
|
||||
try core.set(state.main_window, .height, state.platform.size.height);
|
||||
var win = core.windows.get(core.main_window).?;
|
||||
win.framebuffer_format = core.descriptor.format;
|
||||
win.framebuffer_width = core.descriptor.width;
|
||||
win.framebuffer_height = core.descriptor.height;
|
||||
win.width = core.platform.size.width;
|
||||
win.height = core.platform.size.height;
|
||||
core.windows.set(core.main_window, win);
|
||||
|
||||
// Signal that the frame was finished.
|
||||
core.schedule(.frame_finished);
|
||||
// Record to frame rate frequency monitor that a frame was finished.
|
||||
core.frame.tick();
|
||||
|
||||
switch (core.state().state) {
|
||||
.running => core.scheduleAny(core.state().on_tick.?),
|
||||
switch (core.state) {
|
||||
.running => {},
|
||||
.exiting => {
|
||||
core.scheduleAny(core.state().on_exit.?);
|
||||
core.state().state = .deinitializing;
|
||||
core.state = .deinitializing;
|
||||
runner.run(core.on_exit.?);
|
||||
runner.run(core_deinit.id);
|
||||
},
|
||||
.deinitializing => {},
|
||||
.exited => @panic("application not running"),
|
||||
}
|
||||
|
||||
// Record to frame rate frequency monitor that a frame was finished.
|
||||
state.frame.tick();
|
||||
}
|
||||
|
||||
/// Prints into the window title buffer using a format string and arguments. e.g.
|
||||
///
|
||||
/// ```
|
||||
/// try core.state().printTitle(core_mod, core_mod.state().main_window, "Hello, {s}!", .{"Mach"});
|
||||
/// ```
|
||||
pub fn printTitle(
|
||||
core: *@This(),
|
||||
window_id: mach.EntityID,
|
||||
comptime fmt: []const u8,
|
||||
args: anytype,
|
||||
) !void {
|
||||
_ = window_id;
|
||||
// Allocate and assign a new window title slice.
|
||||
const slice = try std.fmt.allocPrintZ(core.allocator, fmt, args);
|
||||
defer core.allocator.free(slice);
|
||||
core.setTitle(slice);
|
||||
// TODO(object)(window-title)
|
||||
// /// Prints into the window title buffer using a format string and arguments. e.g.
|
||||
// ///
|
||||
// /// ```
|
||||
// /// try core.state().printTitle(core_mod, core_mod.state().main_window, "Hello, {s}!", .{"Mach"});
|
||||
// /// ```
|
||||
// pub fn printTitle(
|
||||
// core: *@This(),
|
||||
// window_id: mach.EntityID,
|
||||
// comptime fmt: []const u8,
|
||||
// args: anytype,
|
||||
// ) !void {
|
||||
// _ = window_id;
|
||||
// // Allocate and assign a new window title slice.
|
||||
// const slice = try std.fmt.allocPrintZ(core.allocator, fmt, args);
|
||||
// defer core.allocator.free(slice);
|
||||
// core.setTitle(slice);
|
||||
|
||||
// TODO: This function does not have access to *core.Mod to update
|
||||
// try core.Mod.set(window_id, .title, slice);
|
||||
// // TODO: This function does not have access to *core.Mod to update
|
||||
// // try core.Mod.set(window_id, .title, slice);
|
||||
// }
|
||||
|
||||
pub fn exit(core: *Core) void {
|
||||
core.state = .exiting;
|
||||
}
|
||||
|
||||
fn exit(core: *Mod) void {
|
||||
core.state().state = .exiting;
|
||||
}
|
||||
|
||||
pub inline fn requestAdapterCallback(
|
||||
inline fn requestAdapterCallback(
|
||||
context: *RequestAdapterResponse,
|
||||
status: gpu.RequestAdapterStatus,
|
||||
adapter: ?*gpu.Adapter,
|
||||
|
|
@ -755,7 +726,8 @@ const Platform = switch (build_options.core_platform) {
|
|||
.null => @import("core/Null.zig"),
|
||||
};
|
||||
|
||||
// TODO: this needs to be removed.
|
||||
// TODO(object): this struct should not exist
|
||||
// TODO: this should not be here, it is exposed because the platform implementations need it.
|
||||
pub const InitOptions = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
is_app: bool = false,
|
||||
|
|
@ -1072,7 +1044,7 @@ pub const Position = struct {
|
|||
y: f64,
|
||||
};
|
||||
|
||||
pub const RequestAdapterResponse = struct {
|
||||
const RequestAdapterResponse = struct {
|
||||
status: gpu.RequestAdapterStatus,
|
||||
adapter: ?*gpu.Adapter,
|
||||
message: ?[*:0]const u8,
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ pub fn run(comptime on_each_update_fn: anytype, args_tuple: std.meta.ArgsTuple(@
|
|||
|
||||
pub fn init(
|
||||
darwin: *Darwin,
|
||||
core: *Core.Mod,
|
||||
core: *Core,
|
||||
options: InitOptions,
|
||||
) !void {
|
||||
var surface_descriptor = gpu.Surface.Descriptor{};
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ const MISSING_FEATURES_WAYLAND = [_][]const u8{ "Changing display mode", "VSync"
|
|||
|
||||
pub fn init(
|
||||
linux: *Linux,
|
||||
core: *Core.Mod,
|
||||
core: *Core,
|
||||
options: InitOptions,
|
||||
) !void {
|
||||
linux.allocator = options.allocator;
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ surface_descriptor: gpu.Surface.Descriptor,
|
|||
|
||||
pub fn init(
|
||||
nul: *Null,
|
||||
core: *Core.Mod,
|
||||
core: *Core,
|
||||
options: InitOptions,
|
||||
) !void {
|
||||
_ = nul;
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ state: *Core,
|
|||
// ------------------------------
|
||||
pub fn init(
|
||||
self: *Win32,
|
||||
core: *Core.Mod,
|
||||
core: *Core,
|
||||
options: InitOptions,
|
||||
) !void {
|
||||
self.state = core.state();
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ modifier_indices: KeyModInd,
|
|||
|
||||
pub fn init(
|
||||
linux: *Linux,
|
||||
core: *Core.Mod,
|
||||
core: *Core,
|
||||
options: InitOptions,
|
||||
) !void {
|
||||
libwaylandclient_global = try LibWaylandClient.load();
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ surface_descriptor: *gpu.Surface.DescriptorFromXlibWindow,
|
|||
|
||||
pub fn init(
|
||||
linux: *Linux,
|
||||
core: *Core.Mod,
|
||||
core: *Core,
|
||||
options: InitOptions,
|
||||
) !void {
|
||||
// TODO(core): return errors.NotSupported if not supported
|
||||
|
|
|
|||
109
src/main.zig
109
src/main.zig
|
|
@ -4,6 +4,8 @@ const build_options = @import("build-options");
|
|||
const builtin = @import("builtin");
|
||||
const std = @import("std");
|
||||
|
||||
pub const is_debug = builtin.mode == .Debug;
|
||||
|
||||
// Core
|
||||
pub const Core = if (build_options.want_core) @import("Core.zig") else struct {};
|
||||
|
||||
|
|
@ -19,34 +21,86 @@ pub const sysaudio = if (build_options.want_sysaudio) @import("sysaudio/main.zig
|
|||
pub const sysgpu = if (build_options.want_sysgpu) @import("sysgpu/main.zig") else struct {};
|
||||
pub const gpu = if (build_options.want_sysgpu) @import("sysgpu/main.zig").sysgpu else struct {};
|
||||
|
||||
// Module system
|
||||
pub const modules = blk: {
|
||||
if (!@hasDecl(@import("root"), "modules")) {
|
||||
@compileError("expected `pub const modules = .{};` in root file");
|
||||
}
|
||||
break :blk merge(.{
|
||||
builtin_modules,
|
||||
@import("root").modules,
|
||||
});
|
||||
};
|
||||
pub const ModSet = @import("module/main.zig").ModSet;
|
||||
pub const Modules = @import("module/main.zig").Modules(modules);
|
||||
pub const Mod = ModSet(modules).Mod;
|
||||
pub const ModuleName = @import("module/main.zig").ModuleName(modules);
|
||||
pub const EntityID = @import("module/main.zig").EntityID; // TODO: rename to just Entity?
|
||||
pub const Archetype = @import("module/main.zig").Archetype;
|
||||
pub const Modules = @import("module.zig").Modules;
|
||||
|
||||
pub const ModuleID = @import("module/main.zig").ModuleID;
|
||||
pub const SystemID = @import("module/main.zig").SystemID;
|
||||
pub const AnySystem = @import("module/main.zig").AnySystem;
|
||||
pub const merge = @import("module/main.zig").merge;
|
||||
pub const builtin_modules = @import("module/main.zig").builtin_modules;
|
||||
pub const Entities = @import("module/main.zig").Entities;
|
||||
pub const ModuleID = @import("module.zig").ModuleID;
|
||||
pub const ModuleFunctionID = @import("module.zig").ModuleFunctionID;
|
||||
pub const FunctionID = @import("module.zig").FunctionID;
|
||||
pub const Call = @import("module.zig").Call;
|
||||
pub const Runner = @import("module.zig").Runner;
|
||||
|
||||
pub const is_debug = builtin.mode == .Debug;
|
||||
pub const ObjectID = u32;
|
||||
|
||||
// The global set of all Mach modules that may be used in the program.
|
||||
pub var mods: Modules = undefined;
|
||||
pub fn Objects(comptime T: type) type {
|
||||
return struct {
|
||||
internal: struct {
|
||||
allocator: std.mem.Allocator,
|
||||
id_counter: ObjectID = 0,
|
||||
ids: std.AutoArrayHashMapUnmanaged(ObjectID, u32) = .{},
|
||||
data: std.MultiArrayList(T) = .{},
|
||||
},
|
||||
|
||||
pub const IsMachObjects = void;
|
||||
|
||||
// Only iteration, get(i) and set(i) are supported currently.
|
||||
pub const Slice = struct {
|
||||
len: usize,
|
||||
|
||||
internal: std.MultiArrayList(T).Slice,
|
||||
|
||||
pub fn set(s: *Slice, index: usize, elem: T) void {
|
||||
s.internal.set(index, elem);
|
||||
}
|
||||
|
||||
pub fn get(s: Slice, index: usize) T {
|
||||
return s.internal.get(index);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn new(objs: *@This(), value: T) std.mem.Allocator.Error!ObjectID {
|
||||
const allocator = objs.internal.allocator;
|
||||
const ids = &objs.internal.ids;
|
||||
const data = &objs.internal.data;
|
||||
|
||||
const new_index = try data.addOne(allocator);
|
||||
errdefer _ = data.pop();
|
||||
|
||||
const new_object_id = objs.internal.id_counter;
|
||||
try ids.putNoClobber(allocator, new_object_id, @intCast(new_index));
|
||||
objs.internal.id_counter += 1;
|
||||
data.set(new_index, value);
|
||||
return new_object_id;
|
||||
}
|
||||
|
||||
pub fn set(objs: *@This(), id: ObjectID, value: T) void {
|
||||
const ids = &objs.internal.ids;
|
||||
const data = &objs.internal.data;
|
||||
|
||||
const index = ids.get(id) orelse std.debug.panic("invalid object: {any}", .{id});
|
||||
data.set(index, value);
|
||||
}
|
||||
|
||||
pub fn get(objs: *@This(), id: ObjectID) ?T {
|
||||
const ids = &objs.internal.ids;
|
||||
const data = &objs.internal.data;
|
||||
|
||||
const index = ids.get(id) orelse return null;
|
||||
return data.get(index);
|
||||
}
|
||||
|
||||
pub fn slice(objs: *@This()) Slice {
|
||||
return Slice{ .len = objs.internal.data.len, .internal = objs.internal.data };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn Object(comptime T: type) type {
|
||||
return T;
|
||||
}
|
||||
|
||||
pub fn schedule(v: anytype) @TypeOf(v) {
|
||||
return v;
|
||||
}
|
||||
|
||||
test {
|
||||
// TODO: refactor code so we can use this here:
|
||||
|
|
@ -59,11 +113,6 @@ test {
|
|||
_ = math;
|
||||
_ = testing;
|
||||
_ = time;
|
||||
std.testing.refAllDeclsRecursive(@import("module/Archetype.zig"));
|
||||
std.testing.refAllDeclsRecursive(@import("module/entities.zig"));
|
||||
// std.testing.refAllDeclsRecursive(@import("module/main.zig"));
|
||||
std.testing.refAllDeclsRecursive(@import("module/module.zig"));
|
||||
std.testing.refAllDeclsRecursive(@import("module/StringTable.zig"));
|
||||
std.testing.refAllDeclsRecursive(gamemode);
|
||||
std.testing.refAllDeclsRecursive(math);
|
||||
}
|
||||
|
|
|
|||
368
src/module.zig
Normal file
368
src/module.zig
Normal file
|
|
@ -0,0 +1,368 @@
|
|||
const std = @import("std");
|
||||
|
||||
/// Unique identifier for every module in the program, including those only known at runtime.
|
||||
pub const ModuleID = u32;
|
||||
|
||||
/// Unique identifier for a function within a single module, including those only known at runtime.
|
||||
pub const ModuleFunctionID = u16;
|
||||
|
||||
/// Unique identifier for a function within a module, including those only known at runtime.
|
||||
pub const FunctionID = struct { module_id: ModuleID, fn_id: ModuleFunctionID };
|
||||
|
||||
pub fn Call(module_tag_or_type: anytype, fn_name_tag: anytype) type {
|
||||
return struct {
|
||||
pub const IsMachCall = void;
|
||||
|
||||
pub const module_name = module_tag_or_type;
|
||||
pub const fn_name = fn_name_tag;
|
||||
|
||||
id: FunctionID,
|
||||
};
|
||||
}
|
||||
|
||||
pub const Runner = struct {
|
||||
pub const IsMachRunner = void;
|
||||
|
||||
ctx: *anyopaque,
|
||||
_run: *const fn (ctx: *anyopaque, fn_id: FunctionID) void,
|
||||
|
||||
pub fn run(r: *const @This(), fn_id: FunctionID) void {
|
||||
r._run(r.ctx, fn_id);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn Modules(module_lists: anytype) type {
|
||||
inline for (moduleTuple(module_lists)) |module| {
|
||||
validate(module);
|
||||
}
|
||||
return struct {
|
||||
/// All modules
|
||||
pub const modules = moduleTuple(module_lists);
|
||||
|
||||
/// Enum describing every module name compiled into the program.
|
||||
pub const ModuleName = NameEnum(modules);
|
||||
|
||||
mods: ModulesByName(modules),
|
||||
|
||||
/// Enum describing all declarations for a given comptime-known module.
|
||||
fn ModuleFunctionName(comptime module_name: ModuleName) type {
|
||||
const module = @field(ModuleTypesByName(modules){}, @tagName(module_name));
|
||||
validate(module);
|
||||
|
||||
var enum_fields: []const std.builtin.Type.EnumField = &[0]std.builtin.Type.EnumField{};
|
||||
var i: u32 = 0;
|
||||
inline for (module.mach_systems) |fn_tag| {
|
||||
// TODO: verify decls are Fn or mach.schedule() decl
|
||||
enum_fields = enum_fields ++ [_]std.builtin.Type.EnumField{.{ .name = @tagName(fn_tag), .value = i }};
|
||||
i += 1;
|
||||
}
|
||||
return @Type(.{
|
||||
.Enum = .{
|
||||
.tag_type = if (enum_fields.len > 0) std.math.IntFittingRange(0, enum_fields.len - 1) else u0,
|
||||
.fields = enum_fields,
|
||||
.decls = &[_]std.builtin.Type.Declaration{},
|
||||
.is_exhaustive = true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) @This() {
|
||||
var m: @This() = .{
|
||||
.mods = undefined,
|
||||
};
|
||||
inline for (@typeInfo(@TypeOf(m.mods)).Struct.fields) |field| {
|
||||
// TODO(objects): module-state-init
|
||||
var mod: @TypeOf(@field(m.mods, field.name)) = undefined;
|
||||
inline for (@typeInfo(@TypeOf(mod)).Struct.fields) |mod_field| {
|
||||
if (@typeInfo(mod_field.type) == .Struct and @hasDecl(mod_field.type, "IsMachObjects")) {
|
||||
@field(mod, mod_field.name).internal = .{
|
||||
.allocator = allocator,
|
||||
};
|
||||
}
|
||||
}
|
||||
@field(m.mods, field.name) = mod;
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
pub fn deinit(m: *@This(), allocator: std.mem.Allocator) void {
|
||||
// TODO
|
||||
_ = m;
|
||||
_ = allocator;
|
||||
}
|
||||
|
||||
pub fn Module(module_tag_or_type: anytype) type {
|
||||
const module_name: ModuleName = blk: {
|
||||
if (@typeInfo(@TypeOf(module_tag_or_type)) == .EnumLiteral or @typeInfo(@TypeOf(module_tag_or_type)) == .Enum) break :blk @as(ModuleName, module_tag_or_type);
|
||||
validate(module_tag_or_type);
|
||||
break :blk module_tag_or_type.mach_module;
|
||||
};
|
||||
|
||||
const module = @field(ModuleTypesByName(modules){}, @tagName(module_name));
|
||||
validate(module);
|
||||
|
||||
return struct {
|
||||
mods: *ModulesByName(modules),
|
||||
modules: *Modules(module_lists),
|
||||
|
||||
pub const mod_name: ModuleName = module_name;
|
||||
|
||||
pub fn getFunction(fn_name: ModuleFunctionName(mod_name)) FunctionID {
|
||||
return .{
|
||||
.module_id = @intFromEnum(mod_name),
|
||||
.fn_id = @intFromEnum(fn_name),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn run(
|
||||
m: *const @This(),
|
||||
comptime fn_name: ModuleFunctionName(module_name),
|
||||
) void {
|
||||
const debug_name = @tagName(module_name) ++ "." ++ @tagName(fn_name);
|
||||
const f = @field(module, @tagName(fn_name));
|
||||
const F = @TypeOf(f);
|
||||
|
||||
if (@typeInfo(F) == .Struct and @typeInfo(F).Struct.is_tuple) {
|
||||
// Run a list of functions instead of a single function
|
||||
// TODO: verify this is a mach.schedule() decl
|
||||
if (module_name != .app) @compileLog(module_name);
|
||||
inline for (f) |schedule_entry| {
|
||||
// TODO: unify with Modules(modules).get(M)
|
||||
const callMod: Module(schedule_entry.@"0") = .{ .mods = m.mods, .modules = m.modules };
|
||||
const callFn = @as(ModuleFunctionName(@TypeOf(callMod).mod_name), schedule_entry.@"1");
|
||||
callMod.run(callFn);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Inject arguments
|
||||
var args: std.meta.ArgsTuple(F) = undefined;
|
||||
outer: inline for (@typeInfo(std.meta.ArgsTuple(F)).Struct.fields) |arg| {
|
||||
if (@typeInfo(arg.type) == .Pointer and
|
||||
@typeInfo(std.meta.Child(arg.type)) == .Struct and
|
||||
comptime isValid(std.meta.Child(arg.type)))
|
||||
{
|
||||
// *Module argument
|
||||
// TODO: better error if @field(m.mods, ...) fails ("module not registered")
|
||||
@field(args, arg.name) = &@field(m.mods, @tagName(std.meta.Child(arg.type).mach_module));
|
||||
continue :outer;
|
||||
}
|
||||
if (@typeInfo(arg.type) == .Struct and @hasDecl(arg.type, "IsMachCall")) {
|
||||
const machCall = arg.type;
|
||||
@field(args, arg.name) = .{
|
||||
.id = Module(@field(machCall, "module_name")).getFunction(@field(machCall, "fn_name")),
|
||||
};
|
||||
continue :outer;
|
||||
}
|
||||
if (@typeInfo(arg.type) == .Struct and @hasDecl(arg.type, "IsMachRunner")) {
|
||||
@field(args, arg.name) = Runner{
|
||||
.ctx = m.modules,
|
||||
._run = (struct {
|
||||
pub fn run(ctx: *anyopaque, fn_id: FunctionID) void {
|
||||
const modules2: *Modules(module_lists) = @ptrCast(@alignCast(ctx));
|
||||
modules2.callDynamic(fn_id);
|
||||
}
|
||||
}).run,
|
||||
};
|
||||
continue :outer;
|
||||
}
|
||||
@compileError("mach: function " ++ debug_name ++ " has an invalid argument(" ++ arg.name ++ ") type: " ++ @typeName(arg.type));
|
||||
}
|
||||
|
||||
const Ret = @typeInfo(F).Fn.return_type orelse void;
|
||||
switch (@typeInfo(Ret)) {
|
||||
// TODO: define error handling of runnable functions
|
||||
.ErrorUnion => @call(.auto, f, args) catch |err| std.debug.panic("error: {s}", .{@errorName(err)}),
|
||||
else => @call(.auto, f, args),
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get(m: *@This(), module_tag_or_type: anytype) Module(module_tag_or_type) {
|
||||
return .{ .mods = &m.mods, .modules = m };
|
||||
}
|
||||
|
||||
pub fn callDynamic(m: *@This(), f: FunctionID) void {
|
||||
const module_name: ModuleName = @enumFromInt(f.module_id);
|
||||
switch (module_name) {
|
||||
inline else => |mod_name| {
|
||||
const module_fn_name: ModuleFunctionName(mod_name) = @enumFromInt(f.fn_id);
|
||||
const mod: Module(mod_name) = .{ .mods = &m.mods, .modules = m };
|
||||
const module = @field(ModuleTypesByName(modules){}, @tagName(mod_name));
|
||||
validate(module);
|
||||
|
||||
switch (module_fn_name) {
|
||||
inline else => |fn_name| mod.run(fn_name),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Validates that the given struct is a Mach module.
|
||||
fn validate(comptime module: anytype) void {
|
||||
if (!@hasDecl(module, "mach_module")) @compileError("mach: invalid module, missing `pub const mach_module = .foo_name;` declaration: " ++ @typeName(@TypeOf(module)));
|
||||
if (@typeInfo(@TypeOf(module.mach_module)) != .EnumLiteral) @compileError("mach: invalid module, expected `pub const mach_module = .foo_name;` declaration, found: " ++ @typeName(@TypeOf(module.mach_module)));
|
||||
}
|
||||
|
||||
fn isValid(comptime module: anytype) bool {
|
||||
if (!@hasDecl(module, "mach_module")) return false;
|
||||
if (@typeInfo(@TypeOf(module.mach_module)) != .EnumLiteral) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Given a tuple of Mach module structs, returns an enum which has every possible comptime-known
|
||||
/// module name.
|
||||
fn NameEnum(comptime mods: anytype) type {
|
||||
var enum_fields: []const std.builtin.Type.EnumField = &[0]std.builtin.Type.EnumField{};
|
||||
for (mods, 0..) |module, i| {
|
||||
validate(module);
|
||||
enum_fields = enum_fields ++ [_]std.builtin.Type.EnumField{.{ .name = @tagName(module.mach_module), .value = i }};
|
||||
}
|
||||
return @Type(.{
|
||||
.Enum = .{
|
||||
.tag_type = std.math.IntFittingRange(0, enum_fields.len - 1),
|
||||
.fields = enum_fields,
|
||||
.decls = &[_]std.builtin.Type.Declaration{},
|
||||
.is_exhaustive = true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/// Given a tuple of module structs or module struct tuples:
|
||||
///
|
||||
/// ```
|
||||
/// .{
|
||||
/// .{ Baz, .{ Bar, Foo, .{ Fam } }, Bar },
|
||||
/// Foo,
|
||||
/// Bam,
|
||||
/// .{ Foo, Bam },
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Returns a flat tuple, deduplicated:
|
||||
///
|
||||
/// .{ Baz, Bar, Foo, Fam, Bar, Bam }
|
||||
///
|
||||
fn moduleTuple(comptime tuple: anytype) ModuleTuple(tuple) {
|
||||
return ModuleTuple(tuple){};
|
||||
}
|
||||
|
||||
/// Type-returning variant of merge()
|
||||
fn ModuleTuple(comptime tuple: anytype) type {
|
||||
if (@typeInfo(@TypeOf(tuple)) != .Struct or !@typeInfo(@TypeOf(tuple)).Struct.is_tuple) {
|
||||
@compileError("Expected to find a tuple, found: " ++ @typeName(@TypeOf(tuple)));
|
||||
}
|
||||
|
||||
var tuple_fields: []const std.builtin.Type.StructField = &[0]std.builtin.Type.StructField{};
|
||||
loop: inline for (tuple) |elem| {
|
||||
if (@typeInfo(@TypeOf(elem)) == .Type and @typeInfo(elem) == .Struct) {
|
||||
// Struct type
|
||||
validate(elem);
|
||||
for (tuple_fields) |field| if (@as(*const type, @ptrCast(field.default_value.?)).* == elem)
|
||||
continue :loop;
|
||||
|
||||
var num_buf: [128]u8 = undefined;
|
||||
tuple_fields = tuple_fields ++ [_]std.builtin.Type.StructField{.{
|
||||
.name = std.fmt.bufPrintZ(&num_buf, "{d}", .{tuple_fields.len}) catch unreachable,
|
||||
.type = type,
|
||||
.default_value = &elem,
|
||||
.is_comptime = false,
|
||||
.alignment = if (@sizeOf(elem) > 0) @alignOf(elem) else 0,
|
||||
}};
|
||||
} else if (@typeInfo(@TypeOf(elem)) == .Struct and @typeInfo(@TypeOf(elem)).Struct.is_tuple) {
|
||||
// Nested tuple
|
||||
inline for (moduleTuple(elem)) |nested| {
|
||||
validate(nested);
|
||||
for (tuple_fields) |field| if (@as(*const type, @ptrCast(field.default_value.?)).* == nested)
|
||||
continue :loop;
|
||||
|
||||
var num_buf: [128]u8 = undefined;
|
||||
tuple_fields = tuple_fields ++ [_]std.builtin.Type.StructField{.{
|
||||
.name = std.fmt.bufPrintZ(&num_buf, "{d}", .{tuple_fields.len}) catch unreachable,
|
||||
.type = type,
|
||||
.default_value = &nested,
|
||||
.is_comptime = false,
|
||||
.alignment = if (@sizeOf(nested) > 0) @alignOf(nested) else 0,
|
||||
}};
|
||||
}
|
||||
} else {
|
||||
@compileError("Expected to find a tuple or struct type, found: " ++ @typeName(@TypeOf(elem)));
|
||||
}
|
||||
}
|
||||
return @Type(.{
|
||||
.Struct = .{
|
||||
.is_tuple = true,
|
||||
.layout = .auto,
|
||||
.decls = &.{},
|
||||
.fields = tuple_fields,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/// Given .{Foo, Bar, Baz} Mach modules, returns .{.foo = Foo, .bar = Bar, .baz = Baz} with field
|
||||
/// names corresponding to each module's `pub const mach_module = .foo;` name.
|
||||
fn ModuleTypesByName(comptime modules: anytype) type {
|
||||
var fields: []const std.builtin.Type.StructField = &[0]std.builtin.Type.StructField{};
|
||||
for (modules) |M| {
|
||||
fields = fields ++ [_]std.builtin.Type.StructField{.{
|
||||
.name = @tagName(M.mach_module),
|
||||
.type = type,
|
||||
.default_value = &M,
|
||||
.is_comptime = true,
|
||||
.alignment = @alignOf(type),
|
||||
}};
|
||||
}
|
||||
return @Type(.{
|
||||
.Struct = .{
|
||||
.layout = .auto,
|
||||
.is_tuple = false,
|
||||
.fields = fields,
|
||||
.decls = &[_]std.builtin.Type.Declaration{},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/// Given .{Foo, Bar, Baz} Mach modules, returns .{.foo: Foo = undefined, .bar: Bar = undefined, .baz: Baz = undefined}
|
||||
/// with field names corresponding to each module's `pub const mach_module = .foo;` name, and each Foo type.
|
||||
fn ModulesByName(comptime modules: anytype) type {
|
||||
var fields: []const std.builtin.Type.StructField = &[0]std.builtin.Type.StructField{};
|
||||
for (modules) |M| {
|
||||
fields = fields ++ [_]std.builtin.Type.StructField{.{
|
||||
.name = @tagName(M.mach_module),
|
||||
.type = M,
|
||||
.default_value = &@as(M, undefined),
|
||||
.is_comptime = false,
|
||||
.alignment = @alignOf(M),
|
||||
}};
|
||||
}
|
||||
return @Type(.{
|
||||
.Struct = .{
|
||||
.layout = .auto,
|
||||
.is_tuple = false,
|
||||
.fields = fields,
|
||||
.decls = &[_]std.builtin.Type.Declaration{},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// // Returns true if a and b are both functions and are equal
|
||||
// fn isFnAndEqual(comptime a: anytype, comptime b: anytype) bool {
|
||||
// const A = @TypeOf(a);
|
||||
// const B = @TypeOf(b);
|
||||
// if (@typeInfo(A) != .Fn or @typeInfo(B) != .Fn) return false;
|
||||
// const x = @typeInfo(A).Fn;
|
||||
// const y = @typeInfo(B).Fn;
|
||||
// if (x.calling_convention != y.calling_convention) return false;
|
||||
// if (x.is_generic != y.is_generic) return false;
|
||||
// if (x.is_var_args != y.is_var_args) return false;
|
||||
// if ((x.return_type != null) != (y.return_type != null)) return false;
|
||||
// if (x.return_type != null) if (x.return_type.? != y.return_type.?) return false;
|
||||
// if (x.params.len != y.params.len) return false;
|
||||
// if (x.params.ptr != y.params.ptr) return false;
|
||||
// if (A != B) return false;
|
||||
// if (a != b) return false;
|
||||
// return true;
|
||||
// }
|
||||
|
|
@ -1,267 +0,0 @@
|
|||
//! Represents a single archetype. i.e., entities which have a specific set of components. When a
|
||||
//! component is added or removed from an entity, it's archetype changes because the archetype is
|
||||
//! the set of components an entity has.
|
||||
//!
|
||||
//! Database equivalent: a table where rows are entities and columns are components (dense storage).
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const testing = std.testing;
|
||||
const assert = std.debug.assert;
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const is_debug = @import("../main.zig").is_debug;
|
||||
|
||||
const StringTable = @import("StringTable.zig");
|
||||
const ComponentTypesByName = @import("module.zig").ComponentTypesByName;
|
||||
|
||||
const Archetype = @This();
|
||||
|
||||
/// Describes a single column of the archetype (table); i.e. a single type of component
|
||||
pub const Column = struct {
|
||||
/// The unique name of the component this column stores.
|
||||
name: StringTable.Index,
|
||||
|
||||
/// A unique identifier for the programming-language type this column stores. In the case of Zig
|
||||
/// this is a comptime type identifier. For other languages, it may be something else or simply
|
||||
/// zero if unused.
|
||||
///
|
||||
/// This value need only uniquely identify the column type for the duration of a single build of
|
||||
/// the program.
|
||||
type_id: u32,
|
||||
|
||||
/// The size of the component this column stores.
|
||||
size: u32,
|
||||
|
||||
/// The alignment of the component type this column stores.
|
||||
alignment: u16,
|
||||
|
||||
/// The actual memory where the values are stored. The length/capacity is Archetype.len and
|
||||
/// Archetype.capacity, as all columns in an Archetype have identical lengths/capacities.
|
||||
values: []u8,
|
||||
};
|
||||
|
||||
/// The length of the table (in-use number of rows)
|
||||
len: u32,
|
||||
|
||||
/// The capacity of the table (total allocated number of rows)
|
||||
capacity: u32,
|
||||
|
||||
/// Describes the columns in this table. Each column stores all rows for that column.
|
||||
columns: []Column,
|
||||
|
||||
/// A reference to the string table that can be used to identify Column.name's
|
||||
component_names: *StringTable,
|
||||
|
||||
/// A hash composed of all Column.name's, effectively acting as the unique name of this table.
|
||||
hash: u64,
|
||||
|
||||
/// An index to Database.archetypes, used in the event of a *bucket* hash collision (not a collision
|
||||
/// of the .hash field) - see Database.archetypeOrPut for details.
|
||||
next: ?u32 = null,
|
||||
|
||||
pub fn deinit(storage: *Archetype, gpa: Allocator) void {
|
||||
if (storage.capacity > 0) {
|
||||
for (storage.columns) |column| gpa.free(column.values);
|
||||
}
|
||||
gpa.free(storage.columns);
|
||||
}
|
||||
|
||||
/// appends a new row to this table, with all undefined values.
|
||||
pub fn appendUndefined(storage: *Archetype, gpa: Allocator) !u32 {
|
||||
try storage.ensureUnusedCapacity(gpa, 1);
|
||||
assert(storage.len < storage.capacity);
|
||||
const row_index = storage.len;
|
||||
storage.len += 1;
|
||||
return row_index;
|
||||
}
|
||||
|
||||
pub fn undoAppend(storage: *Archetype) void {
|
||||
storage.len -= 1;
|
||||
}
|
||||
|
||||
/// Ensures there is enough unused capacity to store `num_rows`.
|
||||
pub fn ensureUnusedCapacity(storage: *Archetype, gpa: Allocator, num_rows: usize) !void {
|
||||
return storage.ensureTotalCapacity(gpa, storage.len + num_rows);
|
||||
}
|
||||
|
||||
/// Ensures the total capacity is enough to store `new_capacity` rows total.
|
||||
pub fn ensureTotalCapacity(storage: *Archetype, gpa: Allocator, new_capacity: usize) !void {
|
||||
var better_capacity = storage.capacity;
|
||||
if (better_capacity >= new_capacity) return;
|
||||
|
||||
while (true) {
|
||||
better_capacity +|= better_capacity / 2 + 8;
|
||||
if (better_capacity >= new_capacity) break;
|
||||
}
|
||||
|
||||
return storage.setCapacity(gpa, better_capacity);
|
||||
}
|
||||
|
||||
const max_align_padding = 64;
|
||||
|
||||
/// Sets the capacity to exactly `new_capacity` rows total
|
||||
///
|
||||
/// Asserts `new_capacity >= storage.len`, if you want to shrink capacity then change the len
|
||||
/// yourself first.
|
||||
pub fn setCapacity(storage: *Archetype, gpa: Allocator, new_capacity: usize) !void {
|
||||
assert(new_capacity >= storage.len);
|
||||
|
||||
// TODO: ensure columns are sorted by type_id
|
||||
for (storage.columns) |*column| {
|
||||
const old_values = column.values;
|
||||
const new_values = try gpa.alloc(u8, (new_capacity * column.size) + max_align_padding);
|
||||
if (storage.capacity > 0) {
|
||||
// Note: this copies alignment padding (which is fine, since it is a constant amount.)
|
||||
@memcpy(new_values[0..old_values.len], old_values);
|
||||
gpa.free(old_values);
|
||||
}
|
||||
column.values = new_values;
|
||||
}
|
||||
storage.capacity = @as(u32, @intCast(new_capacity));
|
||||
}
|
||||
|
||||
/// Sets the value of the named components (columns) for the given row in the table.
|
||||
pub fn set(storage: *Archetype, row_index: u32, name: StringTable.Index, component: anytype) void {
|
||||
const ColumnType = @TypeOf(component);
|
||||
if (@sizeOf(ColumnType) == 0) return;
|
||||
if (is_debug) debugAssertColumnType(storage, storage.columnByName(name).?, @TypeOf(component));
|
||||
storage.setDynamic(
|
||||
row_index,
|
||||
name,
|
||||
std.mem.asBytes(&component),
|
||||
@alignOf(@TypeOf(component)),
|
||||
typeId(@TypeOf(component)),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn setDynamic(storage: *Archetype, row_index: u32, name: StringTable.Index, component: []const u8, alignment: u16, type_id: u32) void {
|
||||
if (is_debug) {
|
||||
// TODO: improve error messages
|
||||
assert(storage.len != 0 and storage.len >= row_index);
|
||||
assert(storage.columnByName(name).?.type_id == type_id);
|
||||
assert(storage.columnByName(name).?.size == component.len);
|
||||
assert(storage.columnByName(name).?.alignment == alignment);
|
||||
}
|
||||
|
||||
const values = storage.getColumnValuesRaw(name) orelse @panic("no such component");
|
||||
const start = component.len * row_index;
|
||||
@memcpy(values[start .. start + component.len], component);
|
||||
}
|
||||
|
||||
pub fn get(storage: *Archetype, row_index: u32, name: StringTable.Index, comptime ColumnType: type) ?ColumnType {
|
||||
if (is_debug) debugAssertColumnType(storage, storage.columnByName(name) orelse return null, ColumnType);
|
||||
|
||||
const bytes = storage.getDynamic(row_index, name, @sizeOf(ColumnType), @alignOf(ColumnType), typeId(ColumnType)) orelse return null;
|
||||
return @as(*ColumnType, @alignCast(@ptrCast(bytes.ptr))).*;
|
||||
}
|
||||
|
||||
pub fn getDynamic(storage: *Archetype, row_index: u32, name: StringTable.Index, size: u32, alignment: u16, type_id: u32) ?[]u8 {
|
||||
const values = storage.getColumnValuesRaw(name) orelse return null;
|
||||
if (is_debug) {
|
||||
// TODO: improve error messages
|
||||
assert(storage.columnByName(name).?.size == size);
|
||||
assert(storage.columnByName(name).?.alignment == alignment);
|
||||
assert(storage.columnByName(name).?.type_id == type_id);
|
||||
}
|
||||
|
||||
const start = size * row_index;
|
||||
const end = start + size;
|
||||
return values[start..end];
|
||||
}
|
||||
|
||||
/// Swap-removes the specified row with the last row in the table.
|
||||
pub fn remove(storage: *Archetype, row_index: u32) void {
|
||||
assert(row_index < storage.len);
|
||||
if (storage.len > 1 and row_index != storage.len - 1) {
|
||||
for (storage.columns) |column| {
|
||||
const aligned_values = storage.aligned(&column, column.values);
|
||||
const dstStart = column.size * row_index;
|
||||
const dst = aligned_values[dstStart .. dstStart + column.size];
|
||||
const srcStart = column.size * (storage.len - 1);
|
||||
const src = aligned_values[srcStart .. srcStart + column.size];
|
||||
@memcpy(dst, src);
|
||||
}
|
||||
}
|
||||
storage.len -= 1;
|
||||
}
|
||||
|
||||
/// Tells if this archetype has every one of the given components.
|
||||
pub fn hasComponents(storage: *Archetype, names: []const u32) bool {
|
||||
for (names) |name| {
|
||||
if (!storage.hasComponent(name)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Tells if this archetype has a component with the specified name.
|
||||
pub fn hasComponent(storage: *Archetype, name: StringTable.Index) bool {
|
||||
return storage.columnByName(name) != null;
|
||||
}
|
||||
|
||||
/// Given a column.values slice which is unaligned, adds the neccessary padding
|
||||
/// to achieve alignment for the column's data type, and returns the padded slice.
|
||||
inline fn aligned(storage: *Archetype, column: *const Column, values: []u8) []u8 {
|
||||
const aligned_addr = std.mem.alignForward(usize, @intFromPtr(values.ptr), column.alignment);
|
||||
if (is_debug) {
|
||||
const padding_bytes = aligned_addr - @as(usize, @intFromPtr(values.ptr));
|
||||
if (padding_bytes > max_align_padding) @panic("mach: max_align_padding is too low, this is a bug");
|
||||
}
|
||||
return @as([*]u8, @ptrFromInt(aligned_addr))[0 .. storage.capacity * column.size];
|
||||
}
|
||||
|
||||
pub fn getColumnValues(storage: *Archetype, name: StringTable.Index, comptime ColumnType: type) ?[]ColumnType {
|
||||
const values = storage.getColumnValuesRaw(name) orelse return null;
|
||||
if (is_debug) debugAssertColumnType(storage, storage.columnByName(name).?, ColumnType);
|
||||
var ptr = @as([*]ColumnType, @ptrCast(@alignCast(values.ptr)));
|
||||
const column_values = ptr[0..storage.capacity];
|
||||
return column_values;
|
||||
}
|
||||
|
||||
pub fn getColumnValuesRaw(storage: *Archetype, name: StringTable.Index) ?[]u8 {
|
||||
const column = storage.columnByName(name) orelse return null;
|
||||
return storage.aligned(column, column.values);
|
||||
}
|
||||
|
||||
pub inline fn columnByName(storage: *Archetype, name: StringTable.Index) ?*Column {
|
||||
for (storage.columns) |*column| {
|
||||
if (column.name == name) return column;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn debugPrint(storage: *Archetype) void {
|
||||
std.debug.print("Archetype hash={} len={} capacity={} columns={}\n", .{ storage.hash, storage.len, storage.capacity, storage.columns.len });
|
||||
for (storage.columns, 0..) |*column, i| {
|
||||
std.debug.print("{}. '{s}'\n", .{ i, storage.component_names.string(column.name) });
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a unique comptime usize integer representing the type T. Value will change across
|
||||
/// different compilations.
|
||||
pub fn typeId(comptime T: type) u32 {
|
||||
_ = T;
|
||||
return @truncate(@intFromPtr(&struct {
|
||||
var x: u8 = 0;
|
||||
}.x));
|
||||
}
|
||||
|
||||
/// Asserts that T matches the type of the column.
|
||||
pub inline fn debugAssertColumnType(storage: *Archetype, column: *Archetype.Column, comptime T: type) void {
|
||||
if (is_debug) {
|
||||
if (typeId(T) != column.type_id) std.debug.panic("unexpected type: {s} expected: {s}", .{
|
||||
@typeName(T),
|
||||
storage.component_names.string(column.name),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Asserts that a tuple `row` to be e.g. appended to an archetype has values that actually match
|
||||
/// all of the columns of the archetype table.
|
||||
pub inline fn debugAssertRowType(storage: *Archetype, row: anytype) void {
|
||||
if (is_debug) {
|
||||
inline for (std.meta.fields(@TypeOf(row)), 0..) |field, index| {
|
||||
debugAssertColumnType(storage, &storage.columns[index], field.type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
//! Stores null-terminated strings and maps them to unique 32-bit indices.
|
||||
//!
|
||||
//! Lookups are omnidirectional: both (string -> index) and (index -> string) are supported
|
||||
//! operations.
|
||||
//!
|
||||
//! The implementation is based on:
|
||||
//! https://zig.news/andrewrk/how-to-use-hash-map-contexts-to-save-memory-when-doing-a-string-table-3l33
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const StringTable = @This();
|
||||
|
||||
string_bytes: std.ArrayListUnmanaged(u8) = .{},
|
||||
|
||||
/// Key is string_bytes index.
|
||||
string_table: std.HashMapUnmanaged(u32, void, IndexContext, std.hash_map.default_max_load_percentage) = .{},
|
||||
|
||||
pub const Index = u32;
|
||||
|
||||
/// Returns the index of a string key, if it exists.
|
||||
pub fn index(table: *StringTable, key: []const u8) ?Index {
|
||||
const slice_context: SliceAdapter = .{ .string_bytes = &table.string_bytes };
|
||||
const found_entry = table.string_table.getEntryAdapted(key, slice_context);
|
||||
if (found_entry) |e| return e.key_ptr.*;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns the index of a string key, inserting if not exists.
|
||||
pub fn indexOrPut(table: *StringTable, allocator: std.mem.Allocator, key: []const u8) !Index {
|
||||
const slice_context: SliceAdapter = .{ .string_bytes = &table.string_bytes };
|
||||
const index_context: IndexContext = .{ .string_bytes = &table.string_bytes };
|
||||
const entry = try table.string_table.getOrPutContextAdapted(allocator, key, slice_context, index_context);
|
||||
if (!entry.found_existing) {
|
||||
entry.key_ptr.* = @intCast(table.string_bytes.items.len);
|
||||
try table.string_bytes.appendSlice(allocator, key);
|
||||
try table.string_bytes.append(allocator, '\x00');
|
||||
}
|
||||
return entry.key_ptr.*;
|
||||
}
|
||||
|
||||
/// Returns a null-terminated string given the index
|
||||
pub fn string(table: *StringTable, idx: Index) [:0]const u8 {
|
||||
return std.mem.span(@as([*:0]const u8, @ptrCast(table.string_bytes.items.ptr)) + idx);
|
||||
}
|
||||
|
||||
pub fn deinit(table: *StringTable, allocator: std.mem.Allocator) void {
|
||||
table.string_bytes.deinit(allocator);
|
||||
table.string_table.deinit(allocator);
|
||||
}
|
||||
|
||||
const IndexContext = struct {
|
||||
string_bytes: *std.ArrayListUnmanaged(u8),
|
||||
|
||||
pub fn eql(ctx: IndexContext, a: u32, b: u32) bool {
|
||||
_ = ctx;
|
||||
return a == b;
|
||||
}
|
||||
|
||||
pub fn hash(ctx: IndexContext, x: u32) u64 {
|
||||
const x_slice = std.mem.span(@as([*:0]const u8, @ptrCast(ctx.string_bytes.items.ptr)) + x);
|
||||
return std.hash_map.hashString(x_slice);
|
||||
}
|
||||
};
|
||||
|
||||
const SliceAdapter = struct {
|
||||
string_bytes: *std.ArrayListUnmanaged(u8),
|
||||
|
||||
pub fn eql(adapter: SliceAdapter, a_slice: []const u8, b: u32) bool {
|
||||
const b_slice = std.mem.span(@as([*:0]const u8, @ptrCast(adapter.string_bytes.items.ptr)) + b);
|
||||
return std.mem.eql(u8, a_slice, b_slice);
|
||||
}
|
||||
|
||||
pub fn hash(adapter: SliceAdapter, adapted_key: []const u8) u64 {
|
||||
_ = adapter;
|
||||
return std.hash_map.hashString(adapted_key);
|
||||
}
|
||||
};
|
||||
|
||||
test {
|
||||
const gpa = std.testing.allocator;
|
||||
|
||||
var table: StringTable = .{};
|
||||
defer table.deinit(gpa);
|
||||
|
||||
const index_context: IndexContext = .{ .string_bytes = &table.string_bytes };
|
||||
_ = index_context;
|
||||
|
||||
// "hello" -> index 0
|
||||
const hello_index = try table.indexOrPut(gpa, "hello");
|
||||
try std.testing.expectEqual(@as(Index, 0), hello_index);
|
||||
|
||||
try std.testing.expectEqual(@as(Index, 6), try table.indexOrPut(gpa, "world"));
|
||||
try std.testing.expectEqual(@as(Index, 12), try table.indexOrPut(gpa, "foo"));
|
||||
try std.testing.expectEqual(@as(Index, 16), try table.indexOrPut(gpa, "bar"));
|
||||
try std.testing.expectEqual(@as(Index, 20), try table.indexOrPut(gpa, "baz"));
|
||||
|
||||
// index 0 -> "hello"
|
||||
try std.testing.expectEqualStrings("hello", table.string(hello_index));
|
||||
|
||||
// Lookup "hello" -> index 0
|
||||
try std.testing.expectEqual(hello_index, table.index("hello").?);
|
||||
|
||||
// Lookup "foobar" -> null
|
||||
try std.testing.expectEqual(@as(?Index, null), table.index("foobar"));
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,106 +0,0 @@
|
|||
const std = @import("std");
|
||||
const mach = @import("../main.zig");
|
||||
const testing = std.testing;
|
||||
|
||||
pub const EntityID = @import("entities.zig").EntityID;
|
||||
pub const Database = @import("entities.zig").Database;
|
||||
pub const Archetype = @import("Archetype.zig");
|
||||
pub const ModSet = @import("module.zig").ModSet;
|
||||
pub const Modules = @import("module.zig").Modules;
|
||||
pub const ModuleName = @import("module.zig").ModuleName;
|
||||
pub const ModuleID = @import("module.zig").ModuleID;
|
||||
pub const SystemID = @import("module.zig").SystemID;
|
||||
pub const AnySystem = @import("module.zig").AnySystem;
|
||||
pub const Merge = @import("module.zig").Merge;
|
||||
pub const merge = @import("module.zig").merge;
|
||||
|
||||
pub const builtin_modules = .{Entities};
|
||||
|
||||
/// Builtin .entities module
|
||||
pub const Entities = struct {
|
||||
pub const name = .entities;
|
||||
|
||||
pub const Mod = mach.Mod(@This());
|
||||
|
||||
pub const components = .{
|
||||
.id = .{ .type = EntityID, .description = "Entity ID" },
|
||||
};
|
||||
};
|
||||
|
||||
test {
|
||||
// std.testing.refAllDeclsRecursive(@This());
|
||||
std.testing.refAllDeclsRecursive(@import("Archetype.zig"));
|
||||
std.testing.refAllDeclsRecursive(@import("entities.zig"));
|
||||
std.testing.refAllDeclsRecursive(@import("StringTable.zig"));
|
||||
}
|
||||
|
||||
test "entities DB" {
|
||||
const allocator = testing.allocator;
|
||||
|
||||
const root = struct {
|
||||
pub const modules = merge(.{ builtin_modules, Renderer, Physics });
|
||||
|
||||
const Physics = struct {
|
||||
pointer: u8,
|
||||
|
||||
pub const name = .physics;
|
||||
pub const components = .{
|
||||
.id = .{ .type = u32 },
|
||||
};
|
||||
pub const systems = .{
|
||||
.tick = .{ .handler = tick },
|
||||
};
|
||||
|
||||
fn tick(physics: *mach.ModSet(modules).Mod(Physics)) void {
|
||||
_ = physics;
|
||||
}
|
||||
};
|
||||
|
||||
const Renderer = struct {
|
||||
pub const name = .renderer;
|
||||
pub const components = .{
|
||||
.id = .{ .type = u16 },
|
||||
};
|
||||
pub const systems = .{
|
||||
.tick = .{ .handler = tick },
|
||||
};
|
||||
|
||||
fn tick(
|
||||
physics: *mach.ModSet(modules).Mod(Physics),
|
||||
renderer: *mach.ModSet(modules).Mod(Renderer),
|
||||
) void {
|
||||
_ = renderer;
|
||||
_ = physics;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
// Create a world.
|
||||
var world: Modules(root.modules) = undefined;
|
||||
try world.init(allocator);
|
||||
defer world.deinit(allocator);
|
||||
|
||||
// Initialize module state.
|
||||
var entities = &world.mod.entities;
|
||||
var physics = &world.mod.physics;
|
||||
var renderer = &world.mod.renderer;
|
||||
physics.init(.{ .pointer = 123 });
|
||||
_ = physics.state().pointer; // == 123
|
||||
|
||||
const player1 = try entities.new();
|
||||
const player2 = try entities.new();
|
||||
const player3 = try entities.new();
|
||||
try physics.set(player1, .id, 1001);
|
||||
try renderer.set(player1, .id, 1001);
|
||||
|
||||
try physics.set(player2, .id, 1002);
|
||||
try physics.set(player3, .id, 1003);
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
// Schedule systems to run
|
||||
world.mod.renderer.schedule(.tick);
|
||||
|
||||
// Dispatch systems
|
||||
try world.dispatch(.{});
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue