make it clear how to use module system without mach.Core (remove mach.App)

Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
This commit is contained in:
Stephen Gutekanst 2024-08-24 21:34:35 -07:00 committed by Stephen Gutekanst
parent 7ac5bef717
commit 642cc9b7f7
20 changed files with 567 additions and 334 deletions

View file

@ -5,8 +5,8 @@ pub const name = .app;
pub const Mod = mach.Mod(@This());
pub const systems = .{
.start = .{ .handler = start },
.init = .{ .handler = init },
.after_init = .{ .handler = afterInit },
.deinit = .{ .handler = deinit },
.tick = .{ .handler = tick },
};
@ -14,17 +14,20 @@ pub const systems = .{
title_timer: mach.Timer,
pipeline: *gpu.RenderPipeline,
pub fn deinit(core: *mach.Core.Mod, game: *Mod) void {
game.state().pipeline.release();
pub fn deinit(core: *mach.Core.Mod, app: *Mod) void {
app.state().pipeline.release();
core.schedule(.deinit);
}
fn init(game: *Mod, core: *mach.Core.Mod) !void {
fn start(app: *Mod, core: *mach.Core.Mod) !void {
core.schedule(.init);
game.schedule(.after_init);
app.schedule(.init);
}
fn afterInit(game: *Mod, core: *mach.Core.Mod) !void {
fn init(app: *Mod, core: *mach.Core.Mod) !void {
core.state().on_tick = app.system(.tick);
core.state().on_exit = app.system(.deinit);
// Create our shader module
const shader_module = core.state().device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl"));
defer shader_module.release();
@ -58,7 +61,7 @@ fn afterInit(game: *Mod, core: *mach.Core.Mod) !void {
const pipeline = core.state().device.createRenderPipeline(&pipeline_descriptor);
// Store our render pipeline in our module's state, so we can access it later on.
game.init(.{
app.init(.{
.title_timer = try mach.Timer.start(),
.pipeline = pipeline,
});
@ -67,7 +70,7 @@ fn afterInit(game: *Mod, core: *mach.Core.Mod) !void {
core.schedule(.start);
}
fn tick(core: *mach.Core.Mod, game: *Mod) !void {
fn tick(core: *mach.Core.Mod, app: *Mod) !void {
var iter = core.state().pollEvents();
while (iter.next()) |event| {
switch (event) {
@ -101,7 +104,7 @@ fn tick(core: *mach.Core.Mod, game: *Mod) !void {
defer render_pass.release();
// Draw
render_pass.setPipeline(game.state().pipeline);
render_pass.setPipeline(app.state().pipeline);
render_pass.draw(3, 1, 0, 0);
// Finish render pass
@ -116,8 +119,8 @@ fn tick(core: *mach.Core.Mod, game: *Mod) !void {
core.schedule(.present_frame);
// update the window title every second
if (game.state().title_timer.read() >= 1.0) {
game.state().title_timer.reset();
if (app.state().title_timer.read() >= 1.0) {
app.state().title_timer.reset();
try updateWindowTitle(core);
}
}

View file

@ -1,15 +1,41 @@
const std = @import("std");
const mach = @import("mach");
// The global list of Mach modules registered for use in our application.
// The global list of Mach modules our application may use.
pub const modules = .{
mach.Core,
@import("App.zig"),
};
pub fn main() !void {
// Initialize mach.Core
try mach.core.initModule();
const allocator = std.heap.c_allocator;
// Main loop
while (try mach.core.tick()) {}
// Initialize module system
try mach.mods.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:
const stack_space = try allocator.alloc(u8, 8 * 1024 * 1024);
try mach.mods.dispatchUntil(stack_space, .mach_core, .started);
// On some platforms, you can drive the mach.Core main loop yourself - but this isn't
// possible on all platforms.
if (mach.Core.supports_non_blocking) {
mach.Core.non_blocking = true;
while (mach.mods.mod.mach_core.state != .exited) {
// Execute systems until a frame has been finished.
try mach.mods.dispatchUntil(stack_space, .mach_core, .frame_finished);
}
} 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(stack_space, .{});
}
}

View file

@ -5,8 +5,8 @@ pub const name = .app;
pub const Mod = mach.Mod(@This());
pub const systems = .{
.start = .{ .handler = start },
.init = .{ .handler = init },
.after_init = .{ .handler = afterInit },
.deinit = .{ .handler = deinit },
.tick = .{ .handler = tick },
};
@ -14,17 +14,20 @@ pub const systems = .{
title_timer: mach.Timer,
pipeline: *gpu.RenderPipeline,
pub fn deinit(core: *mach.Core.Mod, game: *Mod) void {
game.state().pipeline.release();
pub fn deinit(core: *mach.Core.Mod, app: *Mod) void {
app.state().pipeline.release();
core.schedule(.deinit);
}
fn init(game: *Mod, core: *mach.Core.Mod) !void {
fn start(app: *Mod, core: *mach.Core.Mod) !void {
core.schedule(.init);
game.schedule(.after_init);
app.schedule(.init);
}
fn afterInit(game: *Mod, core: *mach.Core.Mod) !void {
fn init(app: *Mod, core: *mach.Core.Mod) !void {
core.state().on_tick = app.system(.tick);
core.state().on_exit = app.system(.deinit);
// Create our shader module
const shader_module = core.state().device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl"));
defer shader_module.release();
@ -58,7 +61,7 @@ fn afterInit(game: *Mod, core: *mach.Core.Mod) !void {
const pipeline = core.state().device.createRenderPipeline(&pipeline_descriptor);
// Store our render pipeline in our module's state, so we can access it later on.
game.init(.{
app.init(.{
.title_timer = try mach.Timer.start(),
.pipeline = pipeline,
});
@ -67,7 +70,7 @@ fn afterInit(game: *Mod, core: *mach.Core.Mod) !void {
core.schedule(.start);
}
fn tick(core: *mach.Core.Mod, game: *Mod) !void {
fn tick(core: *mach.Core.Mod, app: *Mod) !void {
var iter = core.state().pollEvents();
while (iter.next()) |event| {
switch (event) {
@ -101,7 +104,7 @@ fn tick(core: *mach.Core.Mod, game: *Mod) !void {
defer render_pass.release();
// Draw
render_pass.setPipeline(game.state().pipeline);
render_pass.setPipeline(app.state().pipeline);
render_pass.draw(3, 1, 0, 0);
// Finish render pass
@ -116,8 +119,8 @@ fn tick(core: *mach.Core.Mod, game: *Mod) !void {
core.schedule(.present_frame);
// update the window title every second
if (game.state().title_timer.read() >= 1.0) {
game.state().title_timer.reset();
if (app.state().title_timer.read() >= 1.0) {
app.state().title_timer.reset();
try updateWindowTitle(core);
}
}

View file

@ -1,15 +1,24 @@
const std = @import("std");
const mach = @import("mach");
// The global list of Mach modules registered for use in our application.
// The global list of Mach modules our application may use.
pub const 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 {
// Initialize mach.Core
try mach.core.initModule();
const allocator = std.heap.c_allocator;
// Main loop
while (try mach.core.tick()) {}
// Initialize module system
try mach.mods.init(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.
const stack_space = try allocator.alloc(u8, 8 * 1024 * 1024);
try mach.mods.dispatch(stack_space, .{});
}

View file

@ -22,6 +22,7 @@ pub const components = .{
};
pub const systems = .{
.start = .{ .handler = start },
.init = .{ .handler = init },
.deinit = .{ .handler = deinit },
.tick = .{ .handler = tick },
@ -42,6 +43,16 @@ pub fn deinit(core: *mach.Core.Mod, renderer: *Renderer.Mod) void {
core.schedule(.deinit);
}
fn start(
core: *mach.Core.Mod,
renderer: *Renderer.Mod,
app: *Mod,
) !void {
core.schedule(.init);
renderer.schedule(.init);
app.schedule(.init);
}
fn init(
// These are injected dependencies - as long as these modules were registered in the top-level
// of the program we can have these types injected here, letting us work with other modules in
@ -49,10 +60,10 @@ fn init(
entities: *mach.Entities.Mod,
core: *mach.Core.Mod,
renderer: *Renderer.Mod,
game: *Mod,
app: *Mod,
) !void {
core.schedule(.init);
renderer.schedule(.init);
core.state().on_tick = app.system(.tick);
core.state().on_exit = app.system(.deinit);
// Create our player entity.
const player = try entities.new();
@ -68,9 +79,9 @@ 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 game.state() will panic indicating the state was never
// file. If this is not done, then app.state() will panic indicating the state was never
// initialized.
game.init(.{
app.init(.{
.timer = try mach.Timer.start(),
.spawn_timer = try mach.Timer.start(),
.player = player,
@ -83,11 +94,11 @@ fn tick(
entities: *mach.Entities.Mod,
core: *mach.Core.Mod,
renderer: *Renderer.Mod,
game: *Mod,
app: *Mod,
) !void {
var iter = core.state().pollEvents();
var direction = game.state().direction;
var spawning = game.state().spawning;
var direction = app.state().direction;
var spawning = app.state().spawning;
while (iter.next()) |event| {
switch (event) {
.key_press => |ev| {
@ -118,18 +129,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 game.state() simply returns a pointer to a global singleton of the struct defined
// Note that app.state() 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.
game.state().direction = direction;
game.state().spawning = spawning;
app.state().direction = direction;
app.state().spawning = spawning;
// Get the current player position
var player_pos = renderer.get(game.state().player, .position).?;
var player_pos = renderer.get(app.state().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 game.state().spawn_timer.read() > 1.0 / 60.0) {
_ = game.state().spawn_timer.lap(); // Reset the timer
if (spawning and app.state().spawn_timer.read() > 1.0 / 60.0) {
_ = app.state().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();
@ -137,19 +148,19 @@ fn tick(
try renderer.set(new_entity, .scale, 1.0 / 6.0);
// Tag the entity as one that follows the player
try game.set(new_entity, .follower, {});
try app.set(new_entity, .follower, {});
}
}
// Multiply by delta_time to ensure that movement is the same speed regardless of the frame rate.
const delta_time = game.state().timer.lap();
const delta_time = app.state().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(game.state().player, .position, player_pos);
try renderer.set(app.state().player, .position, player_pos);
// Query all the entities that have the .follower tag indicating they should follow the player.
// TODO(important): better querying API

View file

@ -1,3 +1,4 @@
const std = @import("std");
const mach = @import("mach");
// The global list of Mach modules registered for use in our application.
@ -7,11 +8,18 @@ pub const modules = .{
@import("Renderer.zig"),
};
// TODO: move this to a mach "entrypoint" zig module
// TODO: move this to a mach "entrypoint" zig module which handles nuances like WASM requires.
pub fn main() !void {
// Initialize mach core
try mach.core.initModule();
const allocator = std.heap.c_allocator;
// Main loop
while (try mach.core.tick()) {}
// Initialize module system
try mach.mods.init(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.
const stack_space = try allocator.alloc(u8, 8 * 1024 * 1024);
try mach.mods.dispatch(stack_space, .{});
}

View file

@ -32,10 +32,10 @@ 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 },
.after_init = .{ .handler = afterInit },
.end_frame = .{ .handler = endFrame },
};
@ -45,7 +45,7 @@ fn deinit(core: *mach.Core.Mod, sprite_pipeline: *gfx.SpritePipeline.Mod, glyphs
core.schedule(.deinit);
}
fn init(core: *mach.Core.Mod, sprite_pipeline: *gfx.SpritePipeline.Mod, glyphs: *Glyphs.Mod, game: *Mod) !void {
fn start(core: *mach.Core.Mod, sprite_pipeline: *gfx.SpritePipeline.Mod, glyphs: *Glyphs.Mod, app: *Mod) !void {
core.schedule(.init);
sprite_pipeline.schedule(.init);
glyphs.schedule(.init);
@ -54,17 +54,20 @@ fn init(core: *mach.Core.Mod, sprite_pipeline: *gfx.SpritePipeline.Mod, glyphs:
glyphs.schedule(.prepare);
// Run our init code after glyphs module is initialized.
game.schedule(.after_init);
app.schedule(.init);
}
fn afterInit(
fn init(
entities: *mach.Entities.Mod,
sprite: *gfx.Sprite.Mod,
sprite_pipeline: *gfx.SpritePipeline.Mod,
glyphs: *Glyphs.Mod,
game: *Mod,
app: *Mod,
core: *mach.Core.Mod,
) !void {
core.state().on_tick = app.system(.tick);
core.state().on_exit = app.system(.deinit);
// Create a sprite rendering pipeline
const texture = glyphs.state().texture;
const pipeline = try entities.new();
@ -84,7 +87,7 @@ fn afterInit(
try sprite.set(player, .uv_transform, Mat3x3.translate(vec2(@floatFromInt(r.x), @floatFromInt(r.y))));
sprite.schedule(.update);
game.init(.{
app.init(.{
.timer = try mach.Timer.start(),
.spawn_timer = try mach.Timer.start(),
.player = player,
@ -105,13 +108,13 @@ fn tick(
sprite: *gfx.Sprite.Mod,
sprite_pipeline: *gfx.SpritePipeline.Mod,
glyphs: *Glyphs.Mod,
game: *Mod,
app: *Mod,
) !void {
// TODO(important): event polling should occur in mach.Core module and get fired as ECS events.
// TODO(Core)
var iter = core.state().pollEvents();
var direction = game.state().direction;
var spawning = game.state().spawning;
var direction = app.state().direction;
var spawning = app.state().spawning;
while (iter.next()) |event| {
switch (event) {
.key_press => |ev| {
@ -138,33 +141,33 @@ fn tick(
else => {},
}
}
game.state().direction = direction;
game.state().spawning = spawning;
app.state().direction = direction;
app.state().spawning = spawning;
var player_transform = sprite.get(game.state().player, .transform).?;
var player_transform = sprite.get(app.state().player, .transform).?;
var player_pos = player_transform.translation();
if (!spawning and game.state().spawn_timer.read() > 1.0 / 60.0) {
if (!spawning and app.state().spawn_timer.read() > 1.0 / 60.0) {
// Spawn new entities
_ = game.state().spawn_timer.lap();
_ = app.state().spawn_timer.lap();
for (0..50) |_| {
var new_pos = player_pos;
new_pos.v[0] += game.state().rand.random().floatNorm(f32) * 25;
new_pos.v[1] += game.state().rand.random().floatNorm(f32) * 25;
new_pos.v[0] += app.state().rand.random().floatNorm(f32) * 25;
new_pos.v[1] += app.state().rand.random().floatNorm(f32) * 25;
const rand_index = game.state().rand.random().intRangeAtMost(usize, 0, glyphs.state().regions.count() - 1);
const rand_index = app.state().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, game.state().pipeline);
game.state().sprites += 1;
try sprite.set(new_entity, .pipeline, app.state().pipeline);
app.state().sprites += 1;
}
}
// Multiply by delta_time to ensure that movement is the same speed regardless of the frame rate.
const delta_time = game.state().timer.lap();
const delta_time = app.state().timer.lap();
// Animate entities
var q = try entities.query(.{
@ -178,15 +181,15 @@ fn tick(
// 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) {
try entities.remove(id);
game.state().sprites -= 1;
app.state().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 * game.state().time));
transform = transform.mul(&Mat4x4.scale(Vec3.splat(@max(math.cos(game.state().time / 2.0), 0.2))));
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))));
entity_transform.* = transform;
}
}
@ -199,7 +202,7 @@ fn tick(
player_transform = Mat4x4.translate(player_pos).mul(
&Mat4x4.scale(Vec3.splat(1.0)),
);
try sprite.set(game.state().player, .transform, player_transform);
try sprite.set(app.state().player, .transform, player_transform);
sprite.schedule(.update);
// Perform pre-render work
@ -207,7 +210,7 @@ fn tick(
// Create a command encoder for this frame
const label = @tagName(name) ++ ".tick";
game.state().frame_encoder = core.state().device.createCommandEncoder(&.{ .label = label });
app.state().frame_encoder = core.state().device.createCommandEncoder(&.{ .label = label });
// Grab the back buffer of the swapchain
// TODO(Core)
@ -222,44 +225,44 @@ fn tick(
.load_op = .clear,
.store_op = .store,
}};
game.state().frame_render_pass = game.state().frame_encoder.beginRenderPass(&gpu.RenderPassDescriptor.init(.{
app.state().frame_render_pass = app.state().frame_encoder.beginRenderPass(&gpu.RenderPassDescriptor.init(.{
.label = label,
.color_attachments = &color_attachments,
}));
// Render our sprite batch
sprite_pipeline.state().render_pass = game.state().frame_render_pass;
sprite_pipeline.state().render_pass = app.state().frame_render_pass;
sprite_pipeline.schedule(.render);
// Finish the frame once rendering is done.
game.schedule(.end_frame);
app.schedule(.end_frame);
game.state().time += delta_time;
app.state().time += delta_time;
}
fn endFrame(game: *Mod, core: *mach.Core.Mod) !void {
fn endFrame(app: *Mod, core: *mach.Core.Mod) !void {
// Finish render pass
game.state().frame_render_pass.end();
app.state().frame_render_pass.end();
const label = @tagName(name) ++ ".endFrame";
var command = game.state().frame_encoder.finish(&.{ .label = label });
var command = app.state().frame_encoder.finish(&.{ .label = label });
core.state().queue.submit(&[_]*gpu.CommandBuffer{command});
command.release();
game.state().frame_encoder.release();
game.state().frame_render_pass.release();
app.state().frame_encoder.release();
app.state().frame_render_pass.release();
// Present the frame
core.schedule(.present_frame);
// Every second, update the window title with the FPS
if (game.state().fps_timer.read() >= 1.0) {
if (app.state().fps_timer.read() >= 1.0) {
try core.state().printTitle(
core.state().main_window,
"glyphs [ FPS: {d} ] [ Sprites: {d} ]",
.{ game.state().frame_count, game.state().sprites },
.{ app.state().frame_count, app.state().sprites },
);
core.schedule(.update);
game.state().fps_timer.reset();
game.state().frame_count = 0;
app.state().fps_timer.reset();
app.state().frame_count = 0;
}
game.state().frame_count += 1;
app.state().frame_count += 1;
}

View file

@ -1,6 +1,7 @@
const std = @import("std");
const mach = @import("mach");
// The global list of Mach modules registered for use in our application.
// The global list of Mach modules our application may use.
pub const modules = .{
mach.Core,
mach.gfx.sprite_modules,
@ -8,11 +9,18 @@ pub const modules = .{
@import("Glyphs.zig"),
};
// TODO(important): use standard entrypoint instead
// TODO: move this to a mach "entrypoint" zig module which handles nuances like WASM requires.
pub fn main() !void {
// Initialize mach.Core
try mach.core.initModule();
const allocator = std.heap.c_allocator;
// Main loop
while (try mach.core.tick()) {}
// Initialize module system
try mach.mods.init(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.
const stack_space = try allocator.alloc(u8, 8 * 1024 * 1024);
try mach.mods.dispatch(stack_space, .{});
}

View file

@ -42,8 +42,8 @@ pub const name = .app;
pub const Mod = mach.Mod(@This());
pub const systems = .{
.start = .{ .handler = start },
.init = .{ .handler = init },
.after_init = .{ .handler = afterInit },
.deinit = .{ .handler = deinit },
.tick = .{ .handler = tick },
.end_frame = .{ .handler = endFrame },
@ -62,13 +62,13 @@ fn deinit(
audio.schedule(.deinit);
}
fn init(
fn start(
audio: *mach.Audio.Mod,
text_pipeline: *gfx.TextPipeline.Mod,
text: *gfx.Text.Mod,
sprite_pipeline: *gfx.SpritePipeline.Mod,
core: *mach.Core.Mod,
game: *Mod,
app: *Mod,
) !void {
// If you want to try fullscreen:
// try core.set(core.state().main_window, .fullscreen, true);
@ -78,10 +78,10 @@ fn init(
text.schedule(.init);
text_pipeline.schedule(.init);
sprite_pipeline.schedule(.init);
game.schedule(.after_init);
app.schedule(.init);
}
fn afterInit(
fn init(
entities: *mach.Entities.Mod,
core: *mach.Core.Mod,
audio: *mach.Audio.Mod,
@ -89,11 +89,14 @@ fn afterInit(
text_style: *gfx.TextStyle.Mod,
text: *gfx.Text.Mod,
sprite_pipeline: *gfx.SpritePipeline.Mod,
game: *Mod,
app: *Mod,
) !void {
core.state().on_tick = app.system(.tick);
core.state().on_exit = app.system(.deinit);
// Configure the audio module to run our audio_state_change system when entities audio finishes
// playing
audio.state().on_state_change = game.system(.audio_state_change);
audio.state().on_state_change = app.system(.audio_state_change);
// Create a sprite rendering pipeline
const allocator = gpa.allocator();
@ -135,7 +138,7 @@ fn afterInit(
const sfx_sound_stream = std.io.StreamSource{ .const_buffer = sfx_fbs };
const sfx = try mach.Audio.Opus.decodeStream(gpa.allocator(), sfx_sound_stream);
game.init(.{
app.init(.{
.info_text = info_text,
.info_text_style = style1,
.timer = try mach.Timer.start(),
@ -177,13 +180,13 @@ fn tick(
sprite_pipeline: *gfx.SpritePipeline.Mod,
text: *gfx.Text.Mod,
text_pipeline: *gfx.TextPipeline.Mod,
game: *Mod,
app: *Mod,
audio: *mach.Audio.Mod,
) !void {
// TODO(important): event polling should occur in mach.Core module and get fired as ECS events.
// TODO(Core)
var iter = core.state().pollEvents();
var gotta_go_fast = game.state().gotta_go_fast;
var gotta_go_fast = app.state().gotta_go_fast;
while (iter.next()) |event| {
switch (event) {
.key_press => |ev| {
@ -202,48 +205,48 @@ fn tick(
else => {},
}
}
game.state().gotta_go_fast = gotta_go_fast;
app.state().gotta_go_fast = gotta_go_fast;
// Every second, update the frame rate
if (game.state().fps_timer.read() >= 1.0) {
game.state().frame_rate = game.state().frame_count;
game.state().fps_timer.reset();
game.state().frame_count = 0;
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;
}
try gfx.Text.allocPrintText(
text,
game.state().info_text,
game.state().info_text_style,
app.state().info_text,
app.state().info_text_style,
"[ FPS: {d} ]\n[ Sprites spawned: {d} ]",
.{ game.state().frame_rate, game.state().num_sprites_spawned },
.{ app.state().frame_rate, app.state().num_sprites_spawned },
);
text.schedule(.update);
// var player_transform = sprite.get(game.state().player, .transform).?;
// var player_transform = sprite.get(app.state().player, .transform).?;
// var player_pos = player_transform.translation();
const window_width: f32 = @floatFromInt(core.get(core.state().main_window, .width).?);
const entities_per_second: f32 = @floatFromInt(
game.state().rand.random().intRangeAtMost(usize, 0, if (gotta_go_fast) 50 else 10),
app.state().rand.random().intRangeAtMost(usize, 0, if (gotta_go_fast) 50 else 10),
);
if (game.state().spawn_timer.read() > 1.0 / entities_per_second) {
if (app.state().spawn_timer.read() > 1.0 / entities_per_second) {
// Spawn new entities
_ = game.state().spawn_timer.lap();
_ = app.state().spawn_timer.lap();
var new_pos = vec3(-(window_width / 2), 0, 0);
new_pos.v[1] += game.state().rand.random().floatNorm(f32) * 50;
new_pos.v[1] += app.state().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, game.state().pipeline);
game.state().num_sprites_spawned += 1;
try sprite.set(new_entity, .pipeline, app.state().pipeline);
app.state().num_sprites_spawned += 1;
}
// Multiply by delta_time to ensure that movement is the same speed regardless of the frame rate.
const delta_time = game.state().timer.lap();
const delta_time = app.state().timer.lap();
// Move entities to the right, and make them smaller the further they travel
var q = try entities.query(.{
@ -261,11 +264,11 @@ fn tick(
// Play a new SFX
const e = try entities.new();
try audio.set(e, .samples, game.state().sfx.samples);
try audio.set(e, .channels, game.state().sfx.channels);
try audio.set(e, .samples, app.state().sfx.samples);
try audio.set(e, .channels, app.state().sfx.channels);
try audio.set(e, .index, 0);
try audio.set(e, .playing, true);
game.state().score += 1;
app.state().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))));
@ -283,7 +286,7 @@ fn tick(
// Create a command encoder for this frame
const label = @tagName(name) ++ ".tick";
game.state().frame_encoder = core.state().device.createCommandEncoder(&.{ .label = label });
app.state().frame_encoder = core.state().device.createCommandEncoder(&.{ .label = label });
// Grab the back buffer of the swapchain
// TODO(Core)
@ -298,39 +301,39 @@ fn tick(
.load_op = .clear,
.store_op = .store,
}};
game.state().frame_render_pass = game.state().frame_encoder.beginRenderPass(&gpu.RenderPassDescriptor.init(.{
app.state().frame_render_pass = app.state().frame_encoder.beginRenderPass(&gpu.RenderPassDescriptor.init(.{
.label = label,
.color_attachments = &color_attachments,
}));
// Render our sprite batch
sprite_pipeline.state().render_pass = game.state().frame_render_pass;
sprite_pipeline.state().render_pass = app.state().frame_render_pass;
sprite_pipeline.schedule(.render);
// Render our text batch
text_pipeline.state().render_pass = game.state().frame_render_pass;
text_pipeline.state().render_pass = app.state().frame_render_pass;
text_pipeline.schedule(.render);
// Finish the frame once rendering is done.
game.schedule(.end_frame);
app.schedule(.end_frame);
game.state().time += delta_time;
app.state().time += delta_time;
}
fn endFrame(game: *Mod, core: *mach.Core.Mod) !void {
fn endFrame(app: *Mod, core: *mach.Core.Mod) !void {
// Finish render pass
game.state().frame_render_pass.end();
app.state().frame_render_pass.end();
const label = @tagName(name) ++ ".endFrame";
var command = game.state().frame_encoder.finish(&.{ .label = label });
var command = app.state().frame_encoder.finish(&.{ .label = label });
core.state().queue.submit(&[_]*gpu.CommandBuffer{command});
command.release();
game.state().frame_encoder.release();
game.state().frame_render_pass.release();
app.state().frame_encoder.release();
app.state().frame_render_pass.release();
// Present the frame
core.schedule(.present_frame);
game.state().frame_count += 1;
app.state().frame_count += 1;
}
// TODO: move this helper into gfx module

View file

@ -1,6 +1,7 @@
const std = @import("std");
const mach = @import("mach");
// The global list of Mach modules registered for use in our application.
// The global list of Mach modules our application may use.
pub const modules = .{
mach.Core,
mach.gfx.sprite_modules,
@ -9,11 +10,18 @@ pub const modules = .{
@import("App.zig"),
};
// TODO(important): use standard entrypoint instead
// TODO: move this to a mach "entrypoint" zig module which handles nuances like WASM requires.
pub fn main() !void {
// Initialize mach.Core
try mach.core.initModule();
const allocator = std.heap.c_allocator;
// Main loop
while (try mach.core.tick()) {}
// Initialize module system
try mach.mods.init(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.
const stack_space = try allocator.alloc(u8, 8 * 1024 * 1024);
try mach.mods.dispatch(stack_space, .{});
}

View file

@ -25,8 +25,8 @@ pub const name = .app;
pub const Mod = mach.Mod(@This());
pub const systems = .{
.start = .{ .handler = start },
.init = .{ .handler = init },
.after_init = .{ .handler = afterInit },
.deinit = .{ .handler = deinit },
.tick = .{ .handler = tick },
.audio_state_change = .{ .handler = audioStateChange },
@ -38,10 +38,19 @@ pub const components = .{
ghost_key_mode: bool = false,
fn init(core: *mach.Core.Mod, audio: *mach.Audio.Mod, app: *Mod) void {
fn start(core: *mach.Core.Mod, audio: *mach.Audio.Mod, app: *Mod) void {
core.schedule(.init);
audio.schedule(.init);
app.schedule(.after_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);
// 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);
// Initialize piano module state
app.init(.{});
@ -55,12 +64,6 @@ fn init(core: *mach.Core.Mod, audio: *mach.Audio.Mod, app: *Mod) void {
core.schedule(.start);
}
fn afterInit(audio: *mach.Audio.Mod, app: *Mod) void {
// 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);
}
fn deinit(core: *mach.Core.Mod, audio: *mach.Audio.Mod) void {
audio.schedule(.deinit);
core.schedule(.deinit);

View file

@ -1,17 +1,25 @@
const std = @import("std");
const mach = @import("mach");
// The global list of Mach modules registered for use in our application.
// The global list of Mach modules our application may use.
pub const modules = .{
mach.Core,
mach.Audio,
@import("App.zig"),
};
// TODO(important): use standard entrypoint instead
// TODO: move this to a mach "entrypoint" zig module which handles nuances like WASM requires.
pub fn main() !void {
// Initialize mach.Core
try mach.core.initModule();
const allocator = std.heap.c_allocator;
// Main loop
while (try mach.core.tick()) {}
// Initialize module system
try mach.mods.init(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.
const stack_space = try allocator.alloc(u8, 8 * 1024 * 1024);
try mach.mods.dispatch(stack_space, .{});
}

View file

@ -19,8 +19,8 @@ pub const name = .app;
pub const Mod = mach.Mod(@This());
pub const systems = .{
.start = .{ .handler = start },
.init = .{ .handler = init },
.after_init = .{ .handler = afterInit },
.deinit = .{ .handler = deinit },
.tick = .{ .handler = tick },
.audio_state_change = .{ .handler = audioStateChange },
@ -32,15 +32,28 @@ pub const components = .{
sfx: mach.Audio.Opus,
fn init(
entities: *mach.Entities.Mod,
fn start(
core: *mach.Core.Mod,
audio: *mach.Audio.Mod,
app: *Mod,
) !void {
core.schedule(.init);
audio.schedule(.init);
app.schedule(.after_init);
app.schedule(.init);
}
fn init(
entities: *mach.Entities.Mod,
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);
// 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);
const bgm_fbs = std.io.fixedBufferStream(assets.bgm.bit_bit_loop);
const bgm_sound_stream = std.io.StreamSource{ .const_buffer = bgm_fbs };
@ -68,12 +81,6 @@ fn init(
core.schedule(.start);
}
fn afterInit(audio: *mach.Audio.Mod, app: *Mod) void {
// 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);
}
fn deinit(core: *mach.Core.Mod, audio: *mach.Audio.Mod) void {
audio.schedule(.deinit);
core.schedule(.deinit);

View file

@ -1,17 +1,25 @@
const std = @import("std");
const mach = @import("mach");
// The global list of Mach modules registered for use in our application.
// The global list of Mach modules our application may use.
pub const modules = .{
mach.Core,
mach.Audio,
@import("App.zig"),
};
// TODO(important): use standard entrypoint instead
// TODO: move this to a mach "entrypoint" zig module which handles nuances like WASM requires.
pub fn main() !void {
// Initialize mach.Core
try mach.core.initModule();
const allocator = std.heap.c_allocator;
// Main loop
while (try mach.core.tick()) {}
// Initialize module system
try mach.mods.init(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.
const stack_space = try allocator.alloc(u8, 8 * 1024 * 1024);
try mach.mods.dispatch(stack_space, .{});
}

View file

@ -38,9 +38,9 @@ pub const name = .app;
pub const Mod = mach.Mod(@This());
pub const systems = .{
.start = .{ .handler = start },
.init = .{ .handler = init },
.deinit = .{ .handler = deinit },
.after_init = .{ .handler = afterInit },
.tick = .{ .handler = tick },
.end_frame = .{ .handler = endFrame },
};
@ -53,23 +53,26 @@ fn deinit(
core.schedule(.deinit);
}
fn init(
fn start(
core: *mach.Core.Mod,
sprite_pipeline: *gfx.SpritePipeline.Mod,
game: *Mod,
app: *Mod,
) !void {
core.schedule(.init);
sprite_pipeline.schedule(.init);
game.schedule(.after_init);
app.schedule(.init);
}
fn afterInit(
fn init(
entities: *mach.Entities.Mod,
core: *mach.Core.Mod,
sprite: *gfx.Sprite.Mod,
sprite_pipeline: *gfx.SpritePipeline.Mod,
game: *Mod,
app: *Mod,
) !void {
core.state().on_tick = app.system(.tick);
core.state().on_exit = app.system(.deinit);
// 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
// type than the `.physics2d` module's `.location` component if you desire.
@ -88,7 +91,7 @@ fn afterInit(
try sprite.set(player, .pipeline, pipeline);
sprite.schedule(.update);
game.init(.{
app.init(.{
.timer = try mach.Timer.start(),
.spawn_timer = try mach.Timer.start(),
.player = player,
@ -109,13 +112,13 @@ fn tick(
core: *mach.Core.Mod,
sprite: *gfx.Sprite.Mod,
sprite_pipeline: *gfx.SpritePipeline.Mod,
game: *Mod,
app: *Mod,
) !void {
// TODO(important): event polling should occur in mach.Core module and get fired as ECS events.
// TODO(Core)
var iter = core.state().pollEvents();
var direction = game.state().direction;
var spawning = game.state().spawning;
var direction = app.state().direction;
var spawning = app.state().spawning;
while (iter.next()) |event| {
switch (event) {
.key_press => |ev| {
@ -142,30 +145,30 @@ fn tick(
else => {},
}
}
game.state().direction = direction;
game.state().spawning = spawning;
app.state().direction = direction;
app.state().spawning = spawning;
var player_transform = sprite.get(game.state().player, .transform).?;
var player_transform = sprite.get(app.state().player, .transform).?;
var player_pos = player_transform.translation();
if (spawning and game.state().spawn_timer.read() > 1.0 / 60.0) {
if (spawning and app.state().spawn_timer.read() > 1.0 / 60.0) {
// Spawn new entities
_ = game.state().spawn_timer.lap();
_ = app.state().spawn_timer.lap();
for (0..100) |_| {
var new_pos = player_pos;
new_pos.v[0] += game.state().rand.random().floatNorm(f32) * 25;
new_pos.v[1] += game.state().rand.random().floatNorm(f32) * 25;
new_pos.v[0] += app.state().rand.random().floatNorm(f32) * 25;
new_pos.v[1] += app.state().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, game.state().pipeline);
game.state().sprites += 1;
try sprite.set(new_entity, .pipeline, app.state().pipeline);
app.state().sprites += 1;
}
}
// Multiply by delta_time to ensure that movement is the same speed regardless of the frame rate.
const delta_time = game.state().timer.lap();
const delta_time = app.state().timer.lap();
// Rotate entities
var q = try entities.query(.{
@ -179,8 +182,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 * game.state().time));
transform = transform.mul(&Mat4x4.scaleScalar(@min(math.cos(game.state().time / 2.0), 0.5)));
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)));
entity_transform.* = transform;
}
}
@ -190,7 +193,7 @@ 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(game.state().player, .transform, Mat4x4.translate(player_pos));
try sprite.set(app.state().player, .transform, Mat4x4.translate(player_pos));
sprite.schedule(.update);
// Perform pre-render work
@ -198,7 +201,7 @@ fn tick(
// Create a command encoder for this frame
const label = @tagName(name) ++ ".tick";
game.state().frame_encoder = core.state().device.createCommandEncoder(&.{ .label = label });
app.state().frame_encoder = core.state().device.createCommandEncoder(&.{ .label = label });
// Grab the back buffer of the swapchain
// TODO(Core)
@ -213,46 +216,46 @@ fn tick(
.load_op = .clear,
.store_op = .store,
}};
game.state().frame_render_pass = game.state().frame_encoder.beginRenderPass(&gpu.RenderPassDescriptor.init(.{
app.state().frame_render_pass = app.state().frame_encoder.beginRenderPass(&gpu.RenderPassDescriptor.init(.{
.label = label,
.color_attachments = &color_attachments,
}));
// Render our sprite batch
sprite_pipeline.state().render_pass = game.state().frame_render_pass;
sprite_pipeline.state().render_pass = app.state().frame_render_pass;
sprite_pipeline.schedule(.render);
// Finish the frame once rendering is done.
game.schedule(.end_frame);
app.schedule(.end_frame);
game.state().time += delta_time;
app.state().time += delta_time;
}
fn endFrame(game: *Mod, core: *mach.Core.Mod) !void {
fn endFrame(app: *Mod, core: *mach.Core.Mod) !void {
// Finish render pass
game.state().frame_render_pass.end();
app.state().frame_render_pass.end();
const label = @tagName(name) ++ ".endFrame";
var command = game.state().frame_encoder.finish(&.{ .label = label });
var command = app.state().frame_encoder.finish(&.{ .label = label });
core.state().queue.submit(&[_]*gpu.CommandBuffer{command});
command.release();
game.state().frame_encoder.release();
game.state().frame_render_pass.release();
app.state().frame_encoder.release();
app.state().frame_render_pass.release();
// Present the frame
core.schedule(.present_frame);
// Every second, update the window title with the FPS
if (game.state().fps_timer.read() >= 1.0) {
if (app.state().fps_timer.read() >= 1.0) {
try core.state().printTitle(
core.state().main_window,
"sprite [ FPS: {d} ] [ Sprites: {d} ]",
.{ game.state().frame_count, game.state().sprites },
.{ app.state().frame_count, app.state().sprites },
);
core.schedule(.update);
game.state().fps_timer.reset();
game.state().frame_count = 0;
app.state().fps_timer.reset();
app.state().frame_count = 0;
}
game.state().frame_count += 1;
app.state().frame_count += 1;
}
// TODO: move this helper into gfx module

View file

@ -1,17 +1,25 @@
const std = @import("std");
const mach = @import("mach");
// The global list of Mach modules registered for use in our application.
// The global list of Mach modules our application may use.
pub const modules = .{
mach.Core,
mach.gfx.sprite_modules,
@import("App.zig"),
};
// TODO(important): use standard entrypoint instead
// TODO: move this to a mach "entrypoint" zig module which handles nuances like WASM requires.
pub fn main() !void {
// Initialize mach.Core
try mach.core.initModule();
const allocator = std.heap.c_allocator;
// Main loop
while (try mach.core.tick()) {}
// Initialize module system
try mach.mods.init(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.
const stack_space = try allocator.alloc(u8, 8 * 1024 * 1024);
try mach.mods.dispatch(stack_space, .{});
}

View file

@ -34,9 +34,9 @@ pub const name = .app;
pub const Mod = mach.Mod(@This());
pub const systems = .{
.start = .{ .handler = start },
.init = .{ .handler = init },
.deinit = .{ .handler = deinit },
.after_init = .{ .handler = afterInit },
.tick = .{ .handler = tick },
.end_frame = .{ .handler = endFrame },
};
@ -59,26 +59,29 @@ fn deinit(
core.schedule(.deinit);
}
fn init(
fn start(
core: *mach.Core.Mod,
text: *gfx.Text.Mod,
text_pipeline: *gfx.TextPipeline.Mod,
game: *Mod,
app: *Mod,
) !void {
core.schedule(.init);
text.schedule(.init);
text_pipeline.schedule(.init);
game.schedule(.after_init);
app.schedule(.init);
}
fn afterInit(
fn init(
entities: *mach.Entities.Mod,
core: *mach.Core.Mod,
text: *gfx.Text.Mod,
text_pipeline: *gfx.TextPipeline.Mod,
text_style: *gfx.TextStyle.Mod,
game: *Mod,
app: *Mod,
) !void {
core.state().on_tick = app.system(.tick);
core.state().on_exit = app.system(.deinit);
// 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.)
const style1 = try entities.new();
@ -100,7 +103,7 @@ fn afterInit(
, .{});
text.schedule(.update);
game.init(.{
app.init(.{
.timer = try mach.Timer.start(),
.spawn_timer = try mach.Timer.start(),
.player = player,
@ -120,13 +123,13 @@ fn tick(
core: *mach.Core.Mod,
text: *gfx.Text.Mod,
text_pipeline: *gfx.TextPipeline.Mod,
game: *Mod,
app: *Mod,
) !void {
// TODO(important): event polling should occur in mach.Core module and get fired as ECS events.
// TODO(Core)
var iter = core.state().pollEvents();
var direction = game.state().direction;
var spawning = game.state().spawning;
var direction = app.state().direction;
var spawning = app.state().spawning;
while (iter.next()) |event| {
switch (event) {
.key_press => |ev| {
@ -153,29 +156,29 @@ fn tick(
else => {},
}
}
game.state().direction = direction;
game.state().spawning = spawning;
app.state().direction = direction;
app.state().spawning = spawning;
var player_transform = text.get(game.state().player, .transform).?;
var player_transform = text.get(app.state().player, .transform).?;
var player_pos = player_transform.translation().divScalar(upscale);
if (spawning and game.state().spawn_timer.read() > (1.0 / 60.0)) {
if (spawning and app.state().spawn_timer.read() > (1.0 / 60.0)) {
// Spawn new entities
_ = game.state().spawn_timer.lap();
_ = app.state().spawn_timer.lap();
for (0..10) |_| {
var new_pos = player_pos;
new_pos.v[0] += game.state().rand.random().floatNorm(f32) * 50;
new_pos.v[1] += game.state().rand.random().floatNorm(f32) * 50;
new_pos.v[0] += app.state().rand.random().floatNorm(f32) * 50;
new_pos.v[1] += app.state().rand.random().floatNorm(f32) * 50;
// Create some text
const new_entity = try entities.new();
try text.set(new_entity, .pipeline, game.state().pipeline);
try text.set(new_entity, .pipeline, app.state().pipeline);
try text.set(new_entity, .transform, Mat4x4.scaleScalar(upscale).mul(&Mat4x4.translate(new_pos)));
try gfx.Text.allocPrintText(text, new_entity, game.state().style1, "?!$", .{});
try gfx.Text.allocPrintText(text, new_entity, app.state().style1, "?!$", .{});
}
}
// Multiply by delta_time to ensure that movement is the same speed regardless of the frame rate.
const delta_time = game.state().timer.lap();
const delta_time = app.state().timer.lap();
// Rotate entities
var q = try entities.query(.{
@ -189,8 +192,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 * game.state().time));
transform = transform.mul(&Mat4x4.scaleScalar(@min(math.cos(game.state().time / 2.0), 0.5)));
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)));
entity_transform.* = transform;
}
}
@ -200,8 +203,8 @@ 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(game.state().player, .transform, Mat4x4.scaleScalar(upscale).mul(&Mat4x4.translate(player_pos)));
try text.set(game.state().player, .dirty, true);
try text.set(app.state().player, .transform, Mat4x4.scaleScalar(upscale).mul(&Mat4x4.translate(player_pos)));
try text.set(app.state().player, .dirty, true);
text.schedule(.update);
// Perform pre-render work
@ -209,7 +212,7 @@ fn tick(
// Create a command encoder for this frame
const label = @tagName(name) ++ ".tick";
game.state().frame_encoder = core.state().device.createCommandEncoder(&.{ .label = label });
app.state().frame_encoder = core.state().device.createCommandEncoder(&.{ .label = label });
// Grab the back buffer of the swapchain
// TODO(Core)
@ -224,40 +227,40 @@ fn tick(
.load_op = .clear,
.store_op = .store,
}};
game.state().frame_render_pass = game.state().frame_encoder.beginRenderPass(&gpu.RenderPassDescriptor.init(.{
app.state().frame_render_pass = app.state().frame_encoder.beginRenderPass(&gpu.RenderPassDescriptor.init(.{
.label = label,
.color_attachments = &color_attachments,
}));
// Render our text batch
text_pipeline.state().render_pass = game.state().frame_render_pass;
text_pipeline.state().render_pass = app.state().frame_render_pass;
text_pipeline.schedule(.render);
// Finish the frame once rendering is done.
game.schedule(.end_frame);
app.schedule(.end_frame);
game.state().time += delta_time;
app.state().time += delta_time;
}
fn endFrame(
entities: *mach.Entities.Mod,
game: *Mod,
app: *Mod,
core: *mach.Core.Mod,
) !void {
// Finish render pass
game.state().frame_render_pass.end();
app.state().frame_render_pass.end();
const label = @tagName(name) ++ ".endFrame";
var command = game.state().frame_encoder.finish(&.{ .label = label });
var command = app.state().frame_encoder.finish(&.{ .label = label });
core.state().queue.submit(&[_]*gpu.CommandBuffer{command});
command.release();
game.state().frame_encoder.release();
game.state().frame_render_pass.release();
app.state().frame_encoder.release();
app.state().frame_render_pass.release();
// Present the frame
core.schedule(.present_frame);
// Every second, update the window title with the FPS
if (game.state().fps_timer.read() >= 1.0) {
if (app.state().fps_timer.read() >= 1.0) {
// Gather some text rendering stats
var num_texts: u32 = 0;
var num_glyphs: usize = 0;
@ -274,11 +277,11 @@ fn endFrame(
try core.state().printTitle(
core.state().main_window,
"text [ FPS: {d} ] [ Texts: {d} ] [ Glyphs: {d} ]",
.{ game.state().frame_count, num_texts, num_glyphs },
.{ app.state().frame_count, num_texts, num_glyphs },
);
core.schedule(.update);
game.state().fps_timer.reset();
game.state().frame_count = 0;
app.state().fps_timer.reset();
app.state().frame_count = 0;
}
game.state().frame_count += 1;
app.state().frame_count += 1;
}

View file

@ -1,17 +1,25 @@
const std = @import("std");
const mach = @import("mach");
// The global list of Mach modules registered for use in our application.
// The global list of Mach modules our application may use.
pub const modules = .{
mach.Core,
mach.gfx.text_modules,
@import("App.zig"),
};
// TODO(important): use standard entrypoint instead
// TODO: move this to a mach "entrypoint" zig module which handles nuances like WASM requires.
pub fn main() !void {
// Initialize mach.Core
try mach.core.initModule();
const allocator = std.heap.c_allocator;
// Main loop
while (try mach.core.tick()) {}
// Initialize module system
try mach.mods.init(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.
const stack_space = try allocator.alloc(u8, 8 * 1024 * 1024);
try mach.mods.dispatch(stack_space, .{});
}

View file

@ -21,13 +21,40 @@ pub const Platform = switch (build_options.core_platform) {
.null => @import("core/Null.zig"),
};
// 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) {
.win32 => true,
.x11 => true,
.wayland => true,
.web => false,
.darwin => false,
.null => false,
};
/// Set this to true if you intend to drive the main loop yourself.
///
/// A panic will occur if `supports_non_blocking == false` for the platform.
pub var non_blocking = false;
pub const name = .mach_core;
pub const Mod = mach.Mod(@This());
pub const systems = .{
.init = .{ .handler = init, .description =
\\ Send this once you've configured any options you want on e.g. the core.state().main_window
\\ Initialize mach.Core
},
.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.
},
.update = .{ .handler = update, .description =
\\ TODO
},
.present_frame = .{ .handler = presentFrame, .description =
@ -37,14 +64,27 @@ pub const systems = .{
.exit = .{ .handler = exit, .description =
\\ Send this when you would like to exit the application.
\\
\\ When the next .present_frame occurs, then .app.deinit will be sent giving your app a chance
\\ to deinitialize itself and .app.tick will no longer be sent. Once your app is done with
\\ deinitialization, you should send the final .mach_core.deinit event which will cause the
\\ application to finish.
\\ 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.
},
.deinit = .{ .handler = deinit, .description =
\\ Send this once your app is fully deinitialized and ready to exit for good.
\\ Send this once your app is fully deinitialized and you are ready for mach.Core to exit for
\\ good.
},
.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.
},
.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.
},
};
@ -85,11 +125,20 @@ pub const components = .{
},
};
// Callback systems
on_tick: ?mach.AnySystem = null,
on_exit: ?mach.AnySystem = null,
allocator: std.mem.Allocator,
main_window: mach.EntityID,
platform: Platform,
title: [256:0]u8 = undefined,
should_close: bool = false,
state: enum {
running,
exiting,
deinitializing,
exited,
} = .running,
linux_gamemode: ?bool = null,
frame: Frequency,
@ -114,6 +163,7 @@ pub const EventIterator = struct {
}
};
// TODO: this needs to be removed.
pub const InitOptions = struct {
allocator: std.mem.Allocator,
is_app: bool = false,
@ -130,24 +180,53 @@ pub const InitOptions = struct {
},
};
fn init(core: *Mod, entities: *mach.Entities.Mod, options: InitOptions) !void {
fn update(core: *Mod, entities: *mach.Entities.Mod) !void {
_ = core; // autofix
_ = entities; // autofix
}
fn init(core: *Mod, entities: *mach.Entities.Mod) !void {
// TODO: this needs to be removed.
const options: InitOptions = .{
.allocator = std.heap.c_allocator,
};
const allocator = options.allocator;
// TODO: fix all leaks and use options.allocator
try mach.sysgpu.Impl.init(std.heap.c_allocator, .{});
try mach.sysgpu.Impl.init(allocator, .{});
const state = core.state();
state.allocator = options.allocator;
state.main_window = try entities.new();
try core.set(state.main_window, .fullscreen, false);
try core.set(state.main_window, .width, 1920 / 2);
try core.set(state.main_window, .height, 1080 / 2);
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);
// Copy window title into owned buffer.
if (options.title.len < state.title.len) {
@memcpy(state.title[0..options.title.len], options.title);
state.title[options.title.len] = 0;
var title: [256:0]u8 = undefined;
if (options.title.len < title.len) {
@memcpy(title[0..options.title.len], options.title);
title[options.title.len] = 0;
}
core.init(.{
.allocator = allocator,
.main_window = main_window,
// TODO: remove undefined initialization (disgusting!)
.platform = undefined,
// TODO: these should not be state, they should be components.
.title = title,
.frame = undefined,
.input = undefined,
.instance = undefined,
.adapter = undefined,
.device = undefined,
.queue = undefined,
.surface = undefined,
.swap_chain = undefined,
.descriptor = undefined,
});
const state = core.state();
try Platform.init(&state.platform, options);
state.instance = gpu.createInstance(null) orelse {
@ -227,8 +306,63 @@ fn init(core: *Mod, entities: *mach.Entities.Mod, options: InitOptions) !void {
try state.input.start();
}
pub inline fn deinit(entities: *mach.Entities.Mod, core: *Mod) !void {
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");
// Signal that mach.Core has started.
core.schedule(.started);
// Schedule the next app tick to run.
core.scheduleAny(core.state().on_tick.?);
// 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
// control to us later.
if (non_blocking) {
if (!supports_non_blocking) std.debug.panic(
"mach.Core: platform {s} does not support non_blocking=true mode.",
.{@tagName(build_options.core_platform)},
);
return;
}
// The user wants mach.Core to take control of the main loop.
// TODO: we already have stack space since we are an executing system, so in theory we could
// deduplicate this allocation and just use 'our current stack space' - but accessing it from
// the dispatcher is tricky.
const stack_space = try core.state().allocator.alloc(u8, 8 * 1024 * 1024);
if (supports_non_blocking) {
while (mach.mods.mod.mach_core.state != .exited) {
try mach.mods.dispatchUntil(stack_space, .mach_core, .frame_finished);
}
// Don't return, because Platform.run wouldn't either (marked noreturn due to underlying
// platform APIs never returning.)
std.process.exit(1);
} else {
// Platform drives the main loop.
Platform.run(platform_update_callback, .{ &mach.mods.mod.mach_core, stack_space });
// 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
// good measure.
std.process.exit(1);
}
}
fn platform_update_callback(core: *Mod, stack_space: []u8) !bool {
// Execute systems until .mach_core.frame_finished is dispatched, signalling a frame was
// finished.
try mach.mods.dispatchUntil(stack_space, .mach_core, .frame_finished);
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),
@ -806,6 +940,20 @@ fn presentFrame(core: *Mod, entities: *mach.Entities.Mod) !void {
try core.set(state.main_window, .width, state.platform.size.width);
try core.set(state.main_window, .height, state.platform.size.height);
// Signal that the frame was finished.
core.schedule(.frame_finished);
switch (core.state().state) {
.running => core.scheduleAny(core.state().on_tick.?),
.exiting => {
core.scheduleAny(core.state().on_exit.?);
core.state().state = .deinitializing;
},
.deinitializing => {},
.exited => @panic("application not running"),
}
// Record to frame rate frequency monitor that a frame was finished.
state.frame.tick();
}
@ -834,7 +982,7 @@ pub fn printTitle(
}
fn exit(core: *Mod) void {
core.state().should_close = true;
core.state().state = .exiting;
}
pub const RequestAdapterResponse = struct {

View file

@ -45,53 +45,8 @@ pub const Entities = @import("module/main.zig").Entities;
pub const is_debug = builtin.mode == .Debug;
pub const core = struct {
var mods: Modules = undefined;
var stack_space: [8 * 1024 * 1024]u8 = undefined;
pub fn initModule() !void {
try mods.init(std.heap.c_allocator); // TODO: allocator
// TODO: this is a hack
mods.mod.mach_core.init(undefined);
mods.scheduleWithArgs(.mach_core, .init, .{.{ .allocator = std.heap.c_allocator }});
mods.schedule(.app, .init);
}
pub fn tick() !bool {
if (comptime builtin.target.isDarwin()) {
// TODO: tick() should never block, but we should have a way to block for other platforms.
Core.Platform.run(on_each_update, .{});
} else {
return try on_each_update();
}
return false;
}
// TODO: support deinitialization
// pub fn deinit() void {
// mods.deinit(std.heap.c_allocator); // TODO: allocator
// }
fn on_each_update() !bool {
// TODO: this should not exist here
if (mods.mod.mach_core.state().should_close) {
// Final Dispatch to deinitalize resources
mods.schedule(.app, .deinit);
try mods.dispatch(&stack_space, .{});
mods.schedule(.mach_core, .deinit);
try mods.dispatch(&stack_space, .{});
return false;
}
// Dispatch events until queue is empty
try mods.dispatch(&stack_space, .{});
// Run `update` when `init` and all other systems are executed
mods.schedule(.app, .update);
return true;
}
};
// The global set of all Mach modules that may be used in the program.
pub var mods: Modules = undefined;
test {
// TODO: refactor code so we can use this here: