module: remove dispatchNoError, better loop implementation

Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
This commit is contained in:
Stephen Gutekanst 2024-04-06 11:15:40 -07:00 committed by Stephen Gutekanst
parent 5737c62171
commit af3c1e9155
6 changed files with 95 additions and 42 deletions

View file

@ -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) {

View file

@ -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) {

View file

@ -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) {

View file

@ -122,5 +122,5 @@ test "example" {
//-------------------------------------------------------------------------
// Send events to modules
world.mod.renderer.sendGlobal(.tick, .{});
try world.dispatch();
try world.dispatch(.{});
}

View file

@ -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;
}

View file

@ -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);
}