From af3c1e91557d807dc61884a10c31642d169fb6b3 Mon Sep 17 00:00:00 2001 From: Stephen Gutekanst Date: Sat, 6 Apr 2024 11:15:40 -0700 Subject: [PATCH] module: remove dispatchNoError, better loop implementation Signed-off-by: Stephen Gutekanst --- examples/glyphs/Game.zig | 19 +++++++-- examples/sprite/Game.zig | 2 +- examples/text/Game.zig | 3 +- src/ecs/main.zig | 2 +- src/engine.zig | 24 ++++++----- src/module.zig | 87 ++++++++++++++++++++++++++++------------ 6 files changed, 95 insertions(+), 42 deletions(-) diff --git a/examples/glyphs/Game.zig b/examples/glyphs/Game.zig index 03aaa9b1..3cfcc165 100644 --- a/examples/glyphs/Game.zig +++ b/examples/glyphs/Game.zig @@ -45,13 +45,16 @@ pub const global_events = .{ .tick = .{ .handler = tick }, }; +pub const local_events = .{ + .after_sprite_init = .{ .handler = afterSpriteInit }, +}; + pub const Pipeline = enum(u32) { default, text, }; fn init( - engine: *mach.Engine.Mod, sprite_mod: *Sprite.Mod, text_mod: *Text.Mod, game: *Mod, @@ -66,11 +69,21 @@ fn init( .texture = texture, }}); + // Run the rest of our init code after sprite_mod's .init_pipeline + // TODO(important): relying on this event ordering is not good + game.send(.after_sprite_init, .{}); +} + +fn afterSpriteInit( + engine: *mach.Engine.Mod, + sprite_mod: *Sprite.Mod, + text_mod: *Text.Mod, + game: *Mod, +) !void { // We can create entities, and set components on them. Note that components live in a module // namespace, e.g. the `Sprite` module could have a 3D `.location` component with a different // type than the `.physics2d` module's `.location` component if you desire. - engine.dispatchNoError(); // TODO: no dispatch in user code const r = text_mod.state().regions.get('?').?; const player = try engine.newEntity(); try sprite_mod.set(player, .transform, Mat4x4.translate(vec3(-0.02, 0, 0))); @@ -201,7 +214,7 @@ fn tick( engine.send(.begin_pass, .{gpu.Color{ .r = 1.0, .g = 1.0, .b = 1.0, .a = 1.0 }}); sprite_mod.send(.render, .{@intFromEnum(Pipeline.text)}); engine.send(.end_pass, .{}); - engine.send(.present, .{}); // Present the frame + engine.send(.frame_done, .{}); // Present the frame // Every second, update the window title with the FPS if (game.state().fps_timer.read() >= 1.0) { diff --git a/examples/sprite/Game.zig b/examples/sprite/Game.zig index aeaeb7a8..ade4229c 100644 --- a/examples/sprite/Game.zig +++ b/examples/sprite/Game.zig @@ -190,7 +190,7 @@ fn tick( engine.send(.begin_pass, .{gpu.Color{ .r = 1.0, .g = 1.0, .b = 1.0, .a = 1.0 }}); sprite_mod.send(.render, .{@intFromEnum(Pipeline.default)}); engine.send(.end_pass, .{}); - engine.send(.present, .{}); // Present the frame + engine.send(.frame_done, .{}); // Present the frame // Every second, update the window title with the FPS if (game.state().fps_timer.read() >= 1.0) { diff --git a/examples/text/Game.zig b/examples/text/Game.zig index 4075ae9e..6f238500 100644 --- a/examples/text/Game.zig +++ b/examples/text/Game.zig @@ -115,7 +115,6 @@ fn init( text_mod.send(.init_pipeline, .{Text.PipelineOptions{ .pipeline = @intFromEnum(Pipeline.default), }}); - engine.dispatchNoError(); // TODO: no dispatch in user code game.init(.{ .timer = try mach.Timer.start(), @@ -240,7 +239,7 @@ fn tick( engine.send(.begin_pass, .{gpu.Color{ .r = 1.0, .g = 1.0, .b = 1.0, .a = 1.0 }}); text_mod.send(.render, .{@intFromEnum(Pipeline.default)}); engine.send(.end_pass, .{}); - engine.send(.present, .{}); // Present the frame + engine.send(.frame_done, .{}); // Present the frame // Every second, update the window title with the FPS if (game.state().fps_timer.read() >= 1.0) { diff --git a/src/ecs/main.zig b/src/ecs/main.zig index 36e64185..7f8ce720 100644 --- a/src/ecs/main.zig +++ b/src/ecs/main.zig @@ -122,5 +122,5 @@ test "example" { //------------------------------------------------------------------------- // Send events to modules world.mod.renderer.sendGlobal(.tick, .{}); - try world.dispatch(); + try world.dispatch(.{}); } diff --git a/src/engine.zig b/src/engine.zig index 732e75c0..46f9e9bc 100644 --- a/src/engine.zig +++ b/src/engine.zig @@ -33,7 +33,8 @@ pub const Engine = struct { .exit = .{ .handler = exit }, .begin_pass = .{ .handler = beginPass }, .end_pass = .{ .handler = endPass }, - .present = .{ .handler = present }, + .frame_done = .{ .handler = frameDone }, + .tick_done = .{ .handler = fn () void }, }; fn init(engine: *Mod) !void { @@ -103,8 +104,9 @@ pub const Engine = struct { }); } - fn present() void { + fn frameDone(engine: *Mod) void { core.swap_chain.present(); + engine.send(.tick_done, .{}); } }; @@ -115,22 +117,26 @@ pub const App = struct { app.* = .{ .modules = undefined }; try app.modules.init(allocator); app.modules.mod.engine.send(.init, .{}); - try app.modules.dispatch(); + try app.modules.dispatch(.{}); } - pub fn deinit(app: *@This()) !void { + pub fn deinit(app: *@This()) void { app.modules.mod.engine.send(.deinit, .{}); - // TODO: dispatch until no remaining events - try app.modules.dispatch(); // dispatch .deinit + // TODO: could it be worth enforcing that deinit dispatch cannot return errors at event handler level? + app.modules.dispatch(.{}) catch |err| std.debug.panic("mach: error during dispatching final .deinit event: {s}", .{@errorName(err)}); app.modules.deinit(gpa.allocator()); _ = gpa.deinit(); } pub fn update(app: *@This()) !bool { - // TODO: better dispatch implementation + // Send .tick to anyone interested app.modules.mod.engine.sendGlobal(.tick, .{}); - try app.modules.dispatch(); // dispatch .tick - try app.modules.dispatch(); // dispatch any events produced by .tick + + // Wait until the .engine module sends a .tick_done event + try app.modules.dispatch(.{ .until = .{ + .module_name = app.modules.moduleNameToID(.engine), + .local_event = app.modules.localEventToID(.engine, .tick_done), + } }); return app.modules.mod.engine.state().should_exit; } diff --git a/src/module.zig b/src/module.zig index 7d25ea7f..5b4670df 100644 --- a/src/module.zig +++ b/src/module.zig @@ -255,8 +255,38 @@ pub fn Modules(comptime modules: anytype) type { }); } + // TODO: docs + pub fn moduleNameToID(m: *@This(), name: ModuleName(modules)) ModuleID { + _ = m; + return @intFromEnum(name); + } + + // TODO: docs + pub fn localEventToID( + m: *@This(), + comptime module_name: ModuleName(modules), + // TODO(important): cleanup comptime + local_event: LocalEventEnumM(@TypeOf(@field(m.mod, @tagName(module_name)).__state)), + ) EventID { + return @intFromEnum(local_event); + } + + pub const DispatchOptions = struct { + /// If specified, instructs that dispatching should occur until the specified local + /// event has been dispatched. + /// + /// If null, dispatching occurs until the event queue is completely empty. + until: ?struct { + module_name: ModuleID, + local_event: EventID, + } = null, + }; + /// Dispatches pending events, invoking their event handlers. - pub fn dispatch(m: *@This()) !void { + pub fn dispatch( + m: *@This(), + options: DispatchOptions, + ) !void { const Injectable = comptime blk: { var types: []const type = &[0]type{}; for (@typeInfo(ModsByName(modules)).Struct.fields) |field| { @@ -275,32 +305,46 @@ pub fn Modules(comptime modules: anytype) type { } @compileError("failed to initialize Injectable field (this is a bug): " ++ field.name ++ " " ++ @typeName(field.type)); } - return m.dispatchInternal(injectable); + return m.dispatchInternal(options, injectable); } - pub fn dispatchInternal(m: *@This(), injectable: anytype) !void { + pub fn dispatchInternal( + m: *@This(), + options: DispatchOptions, + injectable: anytype, + ) !void { // TODO: optimize to reduce send contention // TODO: parallel / multi-threaded dispatch // TODO: PGO - // TODO(important): this is wrong - defer { - m.events_mu.lock(); - m.args_queue.clearRetainingCapacity(); - m.events_mu.unlock(); - } - + var buf: [8 * 1024 * 1024]u8 = undefined; while (true) { + // Dequeue the next event m.events_mu.lock(); - const ev = m.events.readItem() orelse { + var ev = m.events.readItem() orelse { m.events_mu.unlock(); - break; + return; }; + + // Pop the arguments off the stack, so we can release args_slice space. + // Otherwise when we release m.events_mu someone may add more events' arguments + // to the buffer which would make it tricky to find a good point-in-time to release + // argument buffer space. + @memcpy(buf[0..ev.args_slice.len], ev.args_slice); + ev.args_slice = buf[0..ev.args_slice.len]; + m.args_queue.shrinkRetainingCapacity(m.args_queue.items.len - ev.args_slice.len); m.events_mu.unlock(); if (ev.module_name) |module_name| { + // Dispatch the local event try @This().callLocal(@enumFromInt(module_name), @enumFromInt(ev.event_name), ev.args_slice, injectable); + + // If we only wanted to dispatch until this event, then return. + if (options.until) |until| { + if (until.module_name == module_name and until.local_event == ev.event_name) return; + } } else { + // Dispatch the global event try @This().callGlobal(@enumFromInt(ev.event_name), ev.args_slice, injectable); } } @@ -509,15 +553,6 @@ pub fn ModSet(comptime modules: anytype) type { const mods = @fieldParentPtr(ModulesT, "mod", mod_ptr); mods.sendGlobal(module_tag, event_name, args); } - - // TODO: important! eliminate this - pub fn dispatchNoError(m: *@This()) void { - const ModulesT = Modules(modules); - const MByName = ModsByName(modules); - const mod_ptr: *MByName = @alignCast(@fieldParentPtr(MByName, @tagName(module_tag), m)); - const mods = @fieldParentPtr(ModulesT, "mod", mod_ptr); - mods.dispatch() catch |err| @panic(@errorName(err)); - } }; } }; @@ -1425,11 +1460,11 @@ test "dispatch" { // injected arguments. modules.sendGlobal(.engine_renderer, .tick, .{}); try testing.expect(usize, 0).eql(global.ticks); - try modules.dispatchInternal(.{&foo}); + try modules.dispatchInternal(.{}, .{&foo}); try testing.expect(usize, 2).eql(global.ticks); // TODO: make sendDynamic take an args type to avoid footguns with comptime values, etc. modules.sendGlobalDynamic(@intFromEnum(@as(GE, .tick)), .{}); - try modules.dispatchInternal(.{&foo}); + try modules.dispatchInternal(.{}, .{&foo}); try testing.expect(usize, 4).eql(global.ticks); // Global events which are not handled by anyone yet can be written as `pub const fooBar = fn() void;` @@ -1439,7 +1474,7 @@ test "dispatch" { // Local events modules.send(.engine_renderer, .update, .{}); - try modules.dispatchInternal(.{&foo}); + try modules.dispatchInternal(.{}, .{&foo}); try testing.expect(usize, 1).eql(global.renderer_updates); modules.send(.engine_physics, .update, .{}); modules.sendDynamic( @@ -1447,14 +1482,14 @@ test "dispatch" { @intFromEnum(@as(LE, .calc)), .{}, ); - try modules.dispatchInternal(.{&foo}); + try modules.dispatchInternal(.{}, .{&foo}); try testing.expect(usize, 1).eql(global.physics_updates); try testing.expect(usize, 1).eql(global.physics_calc); // Local events modules.send(.engine_renderer, .basic_args, .{ @as(u32, 1), @as(u32, 2) }); // TODO: match arguments against fn ArgsTuple, for correctness and type inference modules.send(.engine_renderer, .injected_args, .{ @as(u32, 1), @as(u32, 2) }); - try modules.dispatchInternal(.{&foo}); + try modules.dispatchInternal(.{}, .{&foo}); try testing.expect(usize, 3).eql(global.basic_args_sum); try testing.expect(usize, 3).eql(foo.injected_args_sum); }