module: event handlers are defined ahead of time

Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
This commit is contained in:
Stephen Gutekanst 2024-03-24 17:31:12 -07:00 committed by Stephen Gutekanst
parent 0fc3bf6545
commit 3bfafe102d
12 changed files with 1099 additions and 967 deletions

View file

@ -20,6 +20,11 @@ pub const components = struct {
pub const follower = void; pub const follower = void;
}; };
pub const events = .{
.{ .global = .init, .handler = init },
.{ .global = .tick, .handler = tick },
};
// Each module must have a globally unique name declared, it is impossible to use two modules with // Each module must have a globally unique name declared, it is impossible to use two modules with
// the same name in a program. To avoid name conflicts, we follow naming conventions: // the same name in a program. To avoid name conflicts, we follow naming conventions:
// //
@ -33,7 +38,8 @@ pub const components = struct {
pub const name = .game; pub const name = .game;
pub const Mod = mach.Mod(@This()); pub const Mod = mach.Mod(@This());
pub fn init( // TODO(engine): remove need for returning an error here
fn init(
engine: *mach.Engine.Mod, engine: *mach.Engine.Mod,
renderer: *Renderer.Mod, renderer: *Renderer.Mod,
game: *Mod, game: *Mod,
@ -56,7 +62,8 @@ pub fn init(
}; };
} }
pub fn tick( // TODO(engine): remove need for returning an error here
fn tick(
engine: *mach.Engine.Mod, engine: *mach.Engine.Mod,
renderer: *Renderer.Mod, renderer: *Renderer.Mod,
game: *Mod, game: *Mod,

View file

@ -26,13 +26,19 @@ pub const components = struct {
pub const scale = f32; pub const scale = f32;
}; };
pub const events = .{
.{ .global = .init, .handler = init },
.{ .global = .deinit, .handler = deinit },
.{ .global = .tick, .handler = tick },
};
// TODO: this shouldn't be a packed struct, it should be extern. // TODO: this shouldn't be a packed struct, it should be extern.
const UniformBufferObject = packed struct { const UniformBufferObject = packed struct {
offset: Vec3.Vector, offset: Vec3.Vector,
scale: f32, scale: f32,
}; };
pub fn init( fn init(
engine: *mach.Engine.Mod, engine: *mach.Engine.Mod,
renderer: *Mod, renderer: *Mod,
) !void { ) !void {
@ -97,7 +103,7 @@ pub fn init(
shader_module.release(); shader_module.release();
} }
pub fn deinit( fn deinit(
renderer: *Mod, renderer: *Mod,
) !void { ) !void {
renderer.state.pipeline.release(); renderer.state.pipeline.release();
@ -106,7 +112,7 @@ pub fn deinit(
renderer.state.uniform_buffer.release(); renderer.state.uniform_buffer.release();
} }
pub fn tick( fn tick(
engine: *mach.Engine.Mod, engine: *mach.Engine.Mod,
renderer: *Mod, renderer: *Mod,
) !void { ) !void {

View file

@ -40,12 +40,17 @@ const d0 = 0.000001;
pub const name = .game; pub const name = .game;
pub const Mod = mach.Mod(@This()); pub const Mod = mach.Mod(@This());
pub const events = .{
.{ .global = .init, .handler = init },
.{ .global = .tick, .handler = tick },
};
pub const Pipeline = enum(u32) { pub const Pipeline = enum(u32) {
default, default,
text, text,
}; };
pub fn init( fn init(
engine: *mach.Engine.Mod, engine: *mach.Engine.Mod,
sprite_mod: *Sprite.Mod, sprite_mod: *Sprite.Mod,
text_mod: *Text.Mod, text_mod: *Text.Mod,
@ -91,7 +96,7 @@ pub fn init(
}; };
} }
pub fn tick( fn tick(
engine: *mach.Engine.Mod, engine: *mach.Engine.Mod,
sprite_mod: *Sprite.Mod, sprite_mod: *Sprite.Mod,
text_mod: *Text.Mod, text_mod: *Text.Mod,

View file

@ -8,6 +8,12 @@ const assets = @import("assets");
pub const name = .game_text; pub const name = .game_text;
pub const Mod = mach.Mod(@This()); pub const Mod = mach.Mod(@This());
pub const events = .{
.{ .global = .deinit, .handler = deinit },
.{ .local = .init, .handler = init },
.{ .local = .prepare, .handler = prepare },
};
const RegionMap = std.AutoArrayHashMapUnmanaged(u21, mach.gfx.Atlas.Region); const RegionMap = std.AutoArrayHashMapUnmanaged(u21, mach.gfx.Atlas.Region);
texture_atlas: mach.gfx.Atlas, texture_atlas: mach.gfx.Atlas,
@ -16,7 +22,7 @@ ft: ft.Library,
face: ft.Face, face: ft.Face,
regions: RegionMap = .{}, regions: RegionMap = .{},
pub fn deinit( fn deinit(
engine: *mach.Engine.Mod, engine: *mach.Engine.Mod,
text_mod: *Mod, text_mod: *Mod,
) !void { ) !void {
@ -27,11 +33,10 @@ pub fn deinit(
text_mod.state.regions.deinit(engine.allocator); text_mod.state.regions.deinit(engine.allocator);
} }
pub const local = struct { fn init(
pub fn init(
engine: *mach.Engine.Mod, engine: *mach.Engine.Mod,
text_mod: *Mod, text_mod: *Mod,
) !void { ) !void {
const device = engine.state.device; const device = engine.state.device;
// rgba32_pixels // rgba32_pixels
@ -63,13 +68,13 @@ pub const local = struct {
s.face = try s.ft.createFaceMemory(assets.roboto_medium_ttf, 0); s.face = try s.ft.createFaceMemory(assets.roboto_medium_ttf, 0);
text_mod.send(.prepare, .{&[_]u21{ '?', '!', 'a', 'b', '#', '@', '%', '$', '&', '^', '*', '+', '=', '<', '>', '/', ':', ';', 'Q', '~' }}); text_mod.send(.prepare, .{&[_]u21{ '?', '!', 'a', 'b', '#', '@', '%', '$', '&', '^', '*', '+', '=', '<', '>', '/', ':', ';', 'Q', '~' }});
} }
pub fn prepare( fn prepare(
engine: *mach.Engine.Mod, engine: *mach.Engine.Mod,
text_mod: *Mod, text_mod: *Mod,
codepoints: []const u21, codepoints: []const u21,
) !void { ) !void {
const device = engine.state.device; const device = engine.state.device;
const queue = device.getQueue(); const queue = device.getQueue();
var s = &text_mod.state; var s = &text_mod.state;
@ -119,5 +124,4 @@ pub const local = struct {
.rows_per_image = @as(u32, @intCast(img_size.height)), .rows_per_image = @as(u32, @intCast(img_size.height)),
}; };
queue.writeTexture(&.{ .texture = s.texture }, &data_layout, &img_size, s.texture_atlas.data); queue.writeTexture(&.{ .texture = s.texture }, &data_layout, &img_size, s.texture_atlas.data);
} }
};

View file

@ -41,11 +41,16 @@ const d0 = 0.000001;
pub const name = .game; pub const name = .game;
pub const Mod = mach.Mod(@This()); pub const Mod = mach.Mod(@This());
pub const events = .{
.{ .global = .init, .handler = init },
.{ .global = .tick, .handler = tick },
};
pub const Pipeline = enum(u32) { pub const Pipeline = enum(u32) {
default, default,
}; };
pub fn init( fn init(
engine: *mach.Engine.Mod, engine: *mach.Engine.Mod,
sprite_mod: *Sprite.Mod, sprite_mod: *Sprite.Mod,
game: *Mod, game: *Mod,
@ -82,7 +87,7 @@ pub fn init(
}; };
} }
pub fn tick( fn tick(
engine: *mach.Engine.Mod, engine: *mach.Engine.Mod,
sprite_mod: *Sprite.Mod, sprite_mod: *Sprite.Mod,
game: *Mod, game: *Mod,

View file

@ -44,6 +44,12 @@ const d0 = 0.000001;
pub const name = .game; pub const name = .game;
pub const Mod = mach.Mod(@This()); pub const Mod = mach.Mod(@This());
pub const events = .{
.{ .global = .init, .handler = init },
.{ .global = .deinit, .handler = deinit },
.{ .global = .tick, .handler = tick },
};
pub const Pipeline = enum(u32) { pub const Pipeline = enum(u32) {
default, default,
}; };
@ -58,7 +64,7 @@ const text1: []const []const u8 = &.{
const text2: []const []const u8 = &.{"!$?😊"}; const text2: []const []const u8 = &.{"!$?😊"};
pub fn init( fn init(
engine: *mach.Engine.Mod, engine: *mach.Engine.Mod,
text_mod: *Text.Mod, text_mod: *Text.Mod,
game: *Mod, game: *Mod,
@ -122,11 +128,11 @@ pub fn init(
}; };
} }
pub fn deinit(engine: *mach.Engine.Mod) !void { fn deinit(engine: *mach.Engine.Mod) !void {
_ = engine; _ = engine;
} }
pub fn tick( fn tick(
engine: *mach.Engine.Mod, engine: *mach.Engine.Mod,
text_mod: *Text.Mod, text_mod: *Text.Mod,
game: *Mod, game: *Mod,

View file

@ -40,39 +40,47 @@ test "inclusion" {
test "example" { test "example" {
const allocator = testing.allocator; const allocator = testing.allocator;
comptime var Renderer = type; const root = struct {
comptime var Physics = type; pub const modules = .{ Renderer, Physics };
Physics = mach.Module(struct {
const Physics = struct {
pointer: u8, pointer: u8,
pub const name = .physics; pub const name = .physics;
pub const components = struct { pub const components = struct {
pub const id = u32; pub const id = u32;
}; };
pub const events = .{
.{ .global = .tick, .handler = tick },
};
pub fn tick(physics: *World(.{ Renderer, Physics }).Mod(Physics)) void { fn tick(physics: *World(modules).Mod(Physics)) void {
_ = physics; _ = physics;
} }
}); };
Renderer = mach.Module(struct { const Renderer = struct {
pub const name = .renderer; pub const name = .renderer;
pub const components = struct { pub const components = struct {
pub const id = u16; pub const id = u16;
}; };
pub const events = .{
.{ .global = .tick, .handler = tick },
};
pub fn tick( fn tick(
physics: *World(.{ Renderer, Physics }).Mod(Physics), physics: *World(modules).Mod(Physics),
renderer: *World(.{ Renderer, Physics }).Mod(Renderer), renderer: *World(modules).Mod(Renderer),
) void { ) void {
_ = renderer; _ = renderer;
_ = physics; _ = physics;
} }
}); };
};
//------------------------------------------------------------------------- //-------------------------------------------------------------------------
// Create a world. // Create a world.
var world: World(.{ Renderer, Physics }) = undefined; var world: World(root.modules) = undefined;
try world.init(allocator); try world.init(allocator);
defer world.deinit(); defer world.deinit();

View file

@ -6,6 +6,7 @@ const mach = @import("../main.zig");
const Entities = @import("entities.zig").Entities; const Entities = @import("entities.zig").Entities;
const EntityID = @import("entities.zig").EntityID; const EntityID = @import("entities.zig").EntityID;
const comp = @import("comptime.zig"); const comp = @import("comptime.zig");
const Module = @import("../module.zig").Module;
pub fn World(comptime mods: anytype) type { pub fn World(comptime mods: anytype) type {
const StateT = NamespacedState(mods); const StateT = NamespacedState(mods);
@ -21,8 +22,8 @@ pub fn World(comptime mods: anytype) type {
pub const IsInjectedArgument = void; pub const IsInjectedArgument = void;
const WorldT = @This(); const WorldT = @This();
pub fn Mod(comptime Module: anytype) type { pub fn Mod(comptime M: anytype) type {
const module_tag = Module.name; const module_tag = M.name;
const State = @TypeOf(@field(@as(StateT, undefined), @tagName(module_tag))); const State = @TypeOf(@field(@as(StateT, undefined), @tagName(module_tag)));
const components = @field(ns_components, @tagName(module_tag)); const components = @field(ns_components, @tagName(module_tag));
return struct { return struct {
@ -212,6 +213,8 @@ fn NamespacedComponents(comptime modules: anytype) type {
fn NamespacedState(comptime modules: anytype) type { fn NamespacedState(comptime modules: anytype) type {
var fields: []const std.builtin.Type.StructField = &[0]std.builtin.Type.StructField{}; var fields: []const std.builtin.Type.StructField = &[0]std.builtin.Type.StructField{};
inline for (modules) |M| { inline for (modules) |M| {
// TODO: can't verify module here because it would introduce a dependency loop
// _ = Module(M);
const state_fields = std.meta.fields(M); const state_fields = std.meta.fields(M);
const State = if (state_fields.len > 0) @Type(.{ const State = if (state_fields.len > 0) @Type(.{
.Struct = .{ .Struct = .{

View file

@ -11,23 +11,31 @@ const allocator = gpa.allocator();
pub const Engine = struct { pub const Engine = struct {
device: *gpu.Device, device: *gpu.Device,
queue: *gpu.Queue, queue: *gpu.Queue,
exit: bool, should_exit: bool,
pass: *gpu.RenderPassEncoder, pass: *gpu.RenderPassEncoder,
encoder: *gpu.CommandEncoder, encoder: *gpu.CommandEncoder,
pub const name = .engine; pub const name = .engine;
pub const Mod = World.Mod(@This()); pub const Mod = World.Mod(@This());
pub const exit = fn () void; pub const events = .{
.{ .local = .init, .handler = init },
.{ .local = .deinit, .handler = deinit },
.{ .local = .exit, .handler = exit },
.{ .local = .beginPass, .handler = beginPass },
.{ .local = .endPass, .handler = endPass },
.{ .local = .present, .handler = present },
.{ .global = .tick, .handler = fn () void },
.{ .global = .exit, .handler = fn () void },
};
pub const local = struct { fn init(world: *World) !void {
pub fn init(world: *World) !void {
core.allocator = allocator; core.allocator = allocator;
try core.init(.{}); try core.init(.{});
const state = &world.mod.engine.state; const state = &world.mod.engine.state;
state.device = core.device; state.device = core.device;
state.queue = core.device.getQueue(); state.queue = core.device.getQueue();
state.exit = false; state.should_exit = false;
state.encoder = state.device.createCommandEncoder(&gpu.CommandEncoder.Descriptor{ state.encoder = state.device.createCommandEncoder(&gpu.CommandEncoder.Descriptor{
.label = "engine.state.encoder", .label = "engine.state.encoder",
}); });
@ -35,7 +43,7 @@ pub const Engine = struct {
world.modules.send(.init, .{}); world.modules.send(.init, .{});
} }
pub fn deinit(world: *World, engine: *Mod) void { fn deinit(world: *World, engine: *Mod) void {
// TODO: this triggers a device loss error, which we should handle correctly // TODO: this triggers a device loss error, which we should handle correctly
// engine.state.device.release(); // engine.state.device.release();
engine.state.queue.release(); engine.state.queue.release();
@ -46,12 +54,12 @@ pub const Engine = struct {
} }
// Engine module's exit handler // Engine module's exit handler
pub fn exit(world: *World) void { fn exit(world: *World) void {
world.modules.send(.exit, .{}); world.modules.send(.exit, .{});
world.mod.engine.state.exit = true; world.mod.engine.state.should_exit = true;
} }
pub fn beginPass(engine: *Mod, clear_color: gpu.Color) void { fn beginPass(engine: *Mod, clear_color: gpu.Color) void {
const back_buffer_view = core.swap_chain.getCurrentTextureView().?; const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
defer back_buffer_view.release(); defer back_buffer_view.release();
@ -69,7 +77,7 @@ pub const Engine = struct {
engine.state.pass = engine.state.encoder.beginRenderPass(&pass_info); engine.state.pass = engine.state.encoder.beginRenderPass(&pass_info);
} }
pub fn endPass(engine: *Mod) void { fn endPass(engine: *Mod) void {
// End this pass // End this pass
engine.state.pass.end(); engine.state.pass.end();
engine.state.pass.release(); engine.state.pass.release();
@ -85,10 +93,9 @@ pub const Engine = struct {
}); });
} }
pub fn present() void { fn present() void {
core.swap_chain.present(); core.swap_chain.present();
} }
};
}; };
pub const App = struct { pub const App = struct {
@ -110,7 +117,7 @@ pub const App = struct {
app.world.modules.send(.tick, .{}); app.world.modules.send(.tick, .{});
try app.world.dispatch(); // dispatch .tick try app.world.dispatch(); // dispatch .tick
try app.world.dispatch(); // dispatch any events produced by .tick try app.world.dispatch(); // dispatch any events produced by .tick
return app.world.mod.engine.state.exit; return app.world.mod.engine.state.should_exit;
} }
}; };

View file

@ -39,6 +39,15 @@ pub const components = struct {
pub const size = Vec2; pub const size = Vec2;
}; };
pub const events = .{
.{ .global = .deinit, .handler = deinit },
.{ .local = .init, .handler = init },
.{ .local = .initPipeline, .handler = initPipeline },
.{ .local = .updated, .handler = updated },
.{ .local = .preRender, .handler = preRender },
.{ .local = .render, .handler = render },
};
const Uniforms = extern struct { const Uniforms = extern struct {
// WebGPU requires that the size of struct fields are multiples of 16 // WebGPU requires that the size of struct fields are multiples of 16
// So we use align(16) and 'extern' to maintain field order // So we use align(16) and 'extern' to maintain field order
@ -121,26 +130,25 @@ pub const PipelineOptions = struct {
pipeline_layout: ?*gpu.PipelineLayout = null, pipeline_layout: ?*gpu.PipelineLayout = null,
}; };
pub fn deinit(sprite_mod: *Mod) !void { fn deinit(sprite_mod: *Mod) !void {
for (sprite_mod.state.pipelines.entries.items(.value)) |*pipeline| pipeline.deinit(); for (sprite_mod.state.pipelines.entries.items(.value)) |*pipeline| pipeline.deinit();
sprite_mod.state.pipelines.deinit(sprite_mod.allocator); sprite_mod.state.pipelines.deinit(sprite_mod.allocator);
} }
pub const local = struct { fn init(
pub fn init(
sprite_mod: *Mod, sprite_mod: *Mod,
) !void { ) !void {
sprite_mod.state = .{ sprite_mod.state = .{
// TODO: struct default value initializers don't work // TODO: struct default value initializers don't work
.pipelines = .{}, .pipelines = .{},
}; };
} }
pub fn initPipeline( fn initPipeline(
engine: *Engine.Mod, engine: *Engine.Mod,
sprite_mod: *Mod, sprite_mod: *Mod,
opt: PipelineOptions, opt: PipelineOptions,
) !void { ) !void {
const device = engine.state.device; const device = engine.state.device;
const pipeline = try sprite_mod.state.pipelines.getOrPut(engine.allocator, opt.pipeline); const pipeline = try sprite_mod.state.pipelines.getOrPut(engine.allocator, opt.pipeline);
@ -274,13 +282,13 @@ pub const local = struct {
.sizes = sizes, .sizes = sizes,
}; };
pipeline.value_ptr.reference(); pipeline.value_ptr.reference();
} }
pub fn updated( fn updated(
engine: *Engine.Mod, engine: *Engine.Mod,
sprite_mod: *Mod, sprite_mod: *Mod,
pipeline_id: u32, pipeline_id: u32,
) !void { ) !void {
const pipeline = sprite_mod.state.pipelines.getPtr(pipeline_id).?; const pipeline = sprite_mod.state.pipelines.getPtr(pipeline_id).?;
const device = engine.state.device; const device = engine.state.device;
@ -323,13 +331,13 @@ pub const local = struct {
defer command.release(); defer command.release();
engine.state.queue.submit(&[_]*gpu.CommandBuffer{command}); engine.state.queue.submit(&[_]*gpu.CommandBuffer{command});
} }
pub fn preRender( fn preRender(
engine: *Engine.Mod, engine: *Engine.Mod,
sprite_mod: *Mod, sprite_mod: *Mod,
pipeline_id: u32, pipeline_id: u32,
) !void { ) !void {
const pipeline = sprite_mod.state.pipelines.get(pipeline_id).?; const pipeline = sprite_mod.state.pipelines.get(pipeline_id).?;
// Update uniform buffer // Update uniform buffer
@ -351,13 +359,13 @@ pub const local = struct {
}; };
engine.state.encoder.writeBuffer(pipeline.uniforms, 0, &[_]Uniforms{uniforms}); engine.state.encoder.writeBuffer(pipeline.uniforms, 0, &[_]Uniforms{uniforms});
} }
pub fn render( fn render(
engine: *Engine.Mod, engine: *Engine.Mod,
sprite_mod: *Mod, sprite_mod: *Mod,
pipeline_id: u32, pipeline_id: u32,
) !void { ) !void {
const pipeline = sprite_mod.state.pipelines.get(pipeline_id).?; const pipeline = sprite_mod.state.pipelines.get(pipeline_id).?;
// Draw the sprite batch // Draw the sprite batch
@ -367,5 +375,4 @@ pub const local = struct {
// TODO: remove dynamic offsets? // TODO: remove dynamic offsets?
pass.setBindGroup(0, pipeline.bind_group, &.{}); pass.setBindGroup(0, pipeline.bind_group, &.{});
pass.draw(total_vertices, 1, 0, 0); pass.draw(total_vertices, 1, 0, 0);
} }
};

View file

@ -64,6 +64,15 @@ pub const components = struct {
pub const color = Vec4; // e.g. vec4(0, 0, 0, 1.0), pub const color = Vec4; // e.g. vec4(0, 0, 0, 1.0),
}; };
pub const events = .{
.{ .global = .deinit, .handler = deinit },
.{ .local = .init, .handler = init },
.{ .local = .initPipeline, .handler = initPipeline },
.{ .local = .updated, .handler = updated },
.{ .local = .preRender, .handler = preRender },
.{ .local = .render, .handler = render },
};
const Uniforms = extern struct { const Uniforms = extern struct {
// WebGPU requires that the size of struct fields are multiples of 16 // WebGPU requires that the size of struct fields are multiples of 16
// So we use align(16) and 'extern' to maintain field order // So we use align(16) and 'extern' to maintain field order
@ -171,26 +180,25 @@ pub const PipelineOptions = struct {
pipeline_layout: ?*gpu.PipelineLayout = null, pipeline_layout: ?*gpu.PipelineLayout = null,
}; };
pub fn deinit(text_mod: *Mod) !void { fn deinit(text_mod: *Mod) !void {
for (text_mod.state.pipelines.entries.items(.value)) |*pipeline| pipeline.deinit(text_mod.allocator); for (text_mod.state.pipelines.entries.items(.value)) |*pipeline| pipeline.deinit(text_mod.allocator);
text_mod.state.pipelines.deinit(text_mod.allocator); text_mod.state.pipelines.deinit(text_mod.allocator);
} }
pub const local = struct { fn init(
pub fn init(
text_mod: *Mod, text_mod: *Mod,
) !void { ) !void {
text_mod.state = .{ text_mod.state = .{
// TODO: struct default value initializers don't work // TODO: struct default value initializers don't work
.pipelines = .{}, .pipelines = .{},
}; };
} }
pub fn initPipeline( fn initPipeline(
engine: *Engine.Mod, engine: *Engine.Mod,
text_mod: *Mod, text_mod: *Mod,
opt: PipelineOptions, opt: PipelineOptions,
) !void { ) !void {
const device = engine.state.device; const device = engine.state.device;
const pipeline = try text_mod.state.pipelines.getOrPut(engine.allocator, opt.pipeline); const pipeline = try text_mod.state.pipelines.getOrPut(engine.allocator, opt.pipeline);
@ -344,13 +352,13 @@ pub const local = struct {
.glyphs = glyphs, .glyphs = glyphs,
}; };
pipeline.value_ptr.reference(); pipeline.value_ptr.reference();
} }
pub fn updated( fn updated(
engine: *Engine.Mod, engine: *Engine.Mod,
text_mod: *Mod, text_mod: *Mod,
pipeline_id: u32, pipeline_id: u32,
) !void { ) !void {
const pipeline = text_mod.state.pipelines.getPtr(pipeline_id).?; const pipeline = text_mod.state.pipelines.getPtr(pipeline_id).?;
const device = engine.state.device; const device = engine.state.device;
@ -505,13 +513,13 @@ pub const local = struct {
defer command.release(); defer command.release();
engine.state.queue.submit(&[_]*gpu.CommandBuffer{command}); engine.state.queue.submit(&[_]*gpu.CommandBuffer{command});
} }
pub fn preRender( fn preRender(
engine: *Engine.Mod, engine: *Engine.Mod,
text_mod: *Mod, text_mod: *Mod,
pipeline_id: u32, pipeline_id: u32,
) !void { ) !void {
const pipeline = text_mod.state.pipelines.get(pipeline_id).?; const pipeline = text_mod.state.pipelines.get(pipeline_id).?;
// Update uniform buffer // Update uniform buffer
@ -533,13 +541,13 @@ pub const local = struct {
}; };
engine.state.encoder.writeBuffer(pipeline.uniforms, 0, &[_]Uniforms{uniforms}); engine.state.encoder.writeBuffer(pipeline.uniforms, 0, &[_]Uniforms{uniforms});
} }
pub fn render( fn render(
engine: *Engine.Mod, engine: *Engine.Mod,
text_mod: *Mod, text_mod: *Mod,
pipeline_id: u32, pipeline_id: u32,
) !void { ) !void {
const pipeline = text_mod.state.pipelines.get(pipeline_id).?; const pipeline = text_mod.state.pipelines.get(pipeline_id).?;
// Draw the text batch // Draw the text batch
@ -549,5 +557,4 @@ pub const local = struct {
// TODO: remove dynamic offsets? // TODO: remove dynamic offsets?
pass.setBindGroup(0, pipeline.bind_group, &.{}); pass.setBindGroup(0, pipeline.bind_group, &.{});
pass.draw(total_vertices, 1, 0, 0); pass.draw(total_vertices, 1, 0, 0);
} }
};

View file

@ -2,17 +2,21 @@ const builtin = @import("builtin");
const std = @import("std"); const std = @import("std");
const testing = @import("testing.zig"); const testing = @import("testing.zig");
/// Verifies that T matches the basic layout of a Mach module /// Verifies that M matches the basic layout of a Mach module
pub fn Module(comptime T: type) type { pub fn Module(comptime M: type) type {
if (@typeInfo(T) != .Struct) @compileError("Module must be a struct type. Found:" ++ @typeName(T)); if (@typeInfo(M) != .Struct) @compileError("mach: expected module struct, found: " ++ @typeName(M));
if (!@hasDecl(T, "name")) @compileError("Module must have `pub const name = .foobar;`"); if (!@hasDecl(M, "name")) @compileError("mach: module must have `pub const name = .foobar;`");
if (@typeInfo(@TypeOf(T.name)) != .EnumLiteral) @compileError("Module must have `pub const name = .foobar;`, found type:" ++ @typeName(T.name)); if (@typeInfo(@TypeOf(M.name)) != .EnumLiteral) @compileError("mach: module must have `pub const name = .foobar;`, found type:" ++ @typeName(M.name));
const prefix = "mach: module ." ++ @tagName(M.name) ++ " ";
if (!@hasDecl(M, "events")) @compileError(prefix ++ "must have `pub const events = .{};`");
validateEvents("mach: module ." ++ @tagName(M.name) ++ " ", M.events);
// TODO: move this to ecs // TODO: move this to ecs
if (@hasDecl(T, "components")) { if (@hasDecl(M, "components")) {
if (@typeInfo(T.components) != .Struct) @compileError("Module.components must be `pub const components = struct { ... };`, found type:" ++ @typeName(T.components)); if (@typeInfo(M.components) != .Struct) @compileError("Module.components must be `pub const components = struct { ... };`, found type:" ++ @typeName(M.components));
} }
return T; return M;
} }
// TODO: implement serialization constraints // TODO: implement serialization constraints
@ -65,49 +69,63 @@ pub fn Modules(comptime mods: anytype) type {
/// Returns an args tuple representing the standard, uninjected, arguments which the given /// Returns an args tuple representing the standard, uninjected, arguments which the given
/// local event handler requires. /// local event handler requires.
fn LocalArgs(module_name: ModuleName(mods), event_name: EventName(mods)) type { fn LocalArgs(module_name: ModuleName(mods), event_name: LocalEventEnum(mods)) type {
inline for (modules) |M| { inline for (modules) |M| {
_ = Module(M); // Validate the module
if (M.name != module_name) continue; if (M.name != module_name) continue;
if (!@hasDecl(M, "local")) @compileError("Module " ++ @tagName(module_name) ++ " has no `pub const local = struct { ... };` event handlers"); inline for (M.events) |event| {
if (!@hasDecl(M.local, @tagName(event_name))) @compileError("Module " ++ @tagName(module_name) ++ ".local has no event handler named: " ++ @tagName(event_name)); const Ev = @TypeOf(event);
const handler = @field(M.local, @tagName(event_name)); const name_tag = if (@hasField(Ev, "local")) event.local else continue;
switch (@typeInfo(@TypeOf(handler))) { if (name_tag != event_name) continue;
const Handler = switch (@typeInfo(@TypeOf(event.handler))) {
.Fn => @TypeOf(event.handler),
.Type => |t| switch (@typeInfo(t)) {
.Fn => event.handler,
else => unreachable,
},
else => unreachable,
};
// TODO: passing std.meta.Tuple here instead of TupleHACK results in a compiler // TODO: passing std.meta.Tuple here instead of TupleHACK results in a compiler
// segfault. The only difference is that TupleHACk does not produce a real tuple, // segfault. The only difference is that TupleHACk does not produce a real tuple,
// `@Type(.{.Struct = .{ .is_tuple = false }})` instead of `.is_tuple = true`. // `@Type(.{.Struct = .{ .is_tuple = false }})` instead of `.is_tuple = true`.
.Fn => return UninjectedArgsTuple(TupleHACK, @TypeOf(handler)), return UninjectedArgsTuple(TupleHACK, Handler);
// Note: This means the module does have some other field by the same name, but it is not a function.
// TODO: allow pre-declarations
else => @compileError("Module " ++ @tagName(module_name) ++ ".local." ++ @tagName(event_name) ++ " is not a function"),
} }
@compileError("mach: module ." ++ @tagName(M.name) ++ " has no .local event handler for ." ++ @tagName(event_name));
} }
} }
/// Returns an args tuple representing the standard, uninjected, arguments which the given /// Returns an args tuple representing the standard, uninjected, arguments which the given
/// global event handler requires. /// global event handler requires.
fn Args(event_name: EventName(mods)) type { fn Args(event_name: GlobalEventEnum(mods)) type {
inline for (modules) |M| { inline for (modules) |M| {
// TODO: enforce any defined event handlers of the same name have the same argument types _ = Module(M); // Validate the module
if (@hasDecl(M, @tagName(event_name))) {
const Handler = switch (@typeInfo(@TypeOf(@field(M, @tagName(event_name))))) { inline for (M.events) |event| {
.Fn => @TypeOf(@field(M, @tagName(event_name))), const Ev = @TypeOf(event);
.Type => switch (@typeInfo(@field(M, @tagName(event_name)))) { const name_tag = if (@hasField(Ev, "global")) event.global else continue;
.Fn => @field(M, @tagName(event_name)), if (name_tag != event_name) continue;
else => continue,
const Handler = switch (@typeInfo(@TypeOf(event.handler))) {
.Fn => @TypeOf(event.handler),
.Type => switch (@typeInfo(event.handler)) {
.Fn => event.handler,
else => unreachable,
}, },
else => continue, else => unreachable,
}; };
return UninjectedArgsTuple(std.meta.Tuple, Handler); return UninjectedArgsTuple(std.meta.Tuple, Handler);
} }
} }
@compileError("No global event handler " ++ @tagName(event_name) ++ " is defined in any module."); @compileError("No global event handler ." ++ @tagName(event_name) ++ " is defined in any module.");
} }
/// Send a global event /// Send a global event
pub fn send( pub fn send(
m: *@This(), m: *@This(),
// TODO: is a variant of this function where event_name is not comptime known, but asserted to be a valid enum, useful? // TODO: is a variant of this function where event_name is not comptime known, but asserted to be a valid enum, useful?
comptime event_name: EventName(mods), comptime event_name: GlobalEventEnum(mods),
args: Args(event_name), args: Args(event_name),
) void { ) void {
// TODO: comptime safety/debugging // TODO: comptime safety/debugging
@ -119,7 +137,7 @@ pub fn Modules(comptime mods: anytype) type {
m: *@This(), m: *@This(),
// TODO: is a variant of this function where module_name/event_name is not comptime known, but asserted to be a valid enum, useful? // TODO: is a variant of this function where module_name/event_name is not comptime known, but asserted to be a valid enum, useful?
comptime module_name: ModuleName(mods), comptime module_name: ModuleName(mods),
comptime event_name: EventName(mods), comptime event_name: LocalEventEnum(mods),
args: LocalArgs(module_name, event_name), args: LocalArgs(module_name, event_name),
) void { ) void {
// TODO: comptime safety/debugging // TODO: comptime safety/debugging
@ -187,23 +205,29 @@ pub fn Modules(comptime mods: anytype) type {
try @This().callLocal(@enumFromInt(module_name), @enumFromInt(ev.event_name), ev.args_slice, injectable); try @This().callLocal(@enumFromInt(module_name), @enumFromInt(ev.event_name), ev.args_slice, injectable);
} else { } else {
// TODO: dispatch arguments // TODO: dispatch arguments
try @This().call(@enumFromInt(ev.event_name), ev.args_slice, injectable); try @This().callGlobal(@enumFromInt(ev.event_name), ev.args_slice, injectable);
} }
} }
} }
/// Call global event handler with the specified name in all modules /// Call global event handler with the specified name in all modules
inline fn call(event_name: EventName(mods), args: []u8, injectable: anytype) !void { inline fn callGlobal(event_name: GlobalEventEnum(mods), args: []u8, injectable: anytype) !void {
if (@typeInfo(@TypeOf(event_name)).Enum.fields.len == 0) return;
switch (event_name) { switch (event_name) {
inline else => |name| { inline else => |ev_name| {
inline for (modules) |M| { inline for (modules) |M| {
if (@hasDecl(M, @tagName(name))) { _ = Module(M); // Validate the module
switch (@typeInfo(@TypeOf(@field(M, @tagName(name))))) { inline for (M.events) |event| {
.Fn => { const Ev = @TypeOf(event);
const handler = @field(M, @tagName(name)); const name_tag = if (@hasField(Ev, "global")) event.global else continue;
try callHandler(handler, args, injectable); if (name_tag != ev_name) continue;
switch (@typeInfo(@TypeOf(event.handler))) {
.Fn => try callHandler(event.handler, args, injectable),
.Type => switch (@typeInfo(event.handler)) {
.Fn => {}, // Pre-declaration of what args an event has, nothing to run.
else => unreachable,
}, },
else => {}, else => unreachable,
} }
} }
} }
@ -212,22 +236,29 @@ pub fn Modules(comptime mods: anytype) type {
} }
/// Call local event handler with the specified name in the specified module /// Call local event handler with the specified name in the specified module
inline fn callLocal(module_name: ModuleName(mods), event_name: EventName(mods), args: []u8, injectable: anytype) !void { inline fn callLocal(module_name: ModuleName(mods), event_name: LocalEventEnum(mods), args: []u8, injectable: anytype) !void {
if (@typeInfo(@TypeOf(event_name)).Enum.fields.len == 0) return;
// TODO: invert switch case for hypothetically better branch prediction // TODO: invert switch case for hypothetically better branch prediction
switch (module_name) { switch (module_name) {
inline else => |mod_name| { inline else => |mod_name| {
switch (event_name) { switch (event_name) {
inline else => |ev_name| { inline else => |ev_name| {
const M = @field(NamespacedModules(@This().modules){}, @tagName(mod_name)); const M = @field(NamespacedModules(@This().modules){}, @tagName(mod_name));
// TODO: no need for hasDecl, assertion should be event can be sent at send() time. _ = Module(M); // Validate the module
if (@hasDecl(M, "local") and @hasDecl(M.local, @tagName(ev_name))) {
const handler = @field(M.local, @tagName(ev_name)); inline for (M.events) |event| {
switch (@typeInfo(@TypeOf(handler))) { const Ev = @TypeOf(event);
.Fn => { const name_tag = if (@hasField(Ev, "local")) event.local else continue;
try callHandler(handler, args, injectable); if (name_tag != ev_name) continue;
switch (@typeInfo(@TypeOf(event.handler))) {
.Fn => try callHandler(event.handler, args, injectable),
.Type => switch (@typeInfo(event.handler)) {
.Fn => {}, // Pre-declaration of what args an event has, nothing to run.
else => unreachable,
}, },
else => {}, else => unreachable,
} }
break;
} }
}, },
} }
@ -336,37 +367,29 @@ fn UninjectedArgsTuple(
return Tuple(std_args); return Tuple(std_args);
} }
/// enum describing every possible comptime-known global event name. /// enum describing every possible comptime-known local event name
fn GlobalEvent(comptime mods: anytype) type { fn LocalEventEnum(comptime mods: anytype) type {
var enum_fields: []const std.builtin.Type.EnumField = &[0]std.builtin.Type.EnumField{}; var enum_fields: []const std.builtin.Type.EnumField = &[0]std.builtin.Type.EnumField{};
var i: u32 = 0; var i: u32 = 0;
for (mods) |M| { for (mods) |M| {
// Global event handlers _ = Module(M); // Validate the module
for (@typeInfo(M).Struct.decls) |decl| { inline for (M.events) |event| {
const is_event_handler = switch (@typeInfo(@TypeOf(@field(M, decl.name)))) { const Event = @TypeOf(event);
.Fn => true, const name_tag = if (@hasField(Event, "local")) event.local else continue;
.Type => switch (@typeInfo(@field(M, decl.name))) {
.Fn => true, const exists_already = blk: {
else => false, for (enum_fields) |existing| if (std.mem.eql(u8, existing.name, @tagName(name_tag))) break :blk true;
}, break :blk false;
else => false,
};
if (is_event_handler) {
const exists_already = blk2: {
for (enum_fields) |existing| if (std.mem.eql(u8, existing.name, decl.name)) break :blk2 true;
break :blk2 false;
}; };
if (!exists_already) { if (!exists_already) {
enum_fields = enum_fields ++ [_]std.builtin.Type.EnumField{.{ .name = decl.name, .value = i }}; enum_fields = enum_fields ++ [_]std.builtin.Type.EnumField{.{ .name = @tagName(name_tag), .value = i }};
i += 1; i += 1;
} }
} }
} }
}
return @Type(.{ return @Type(.{
.Enum = .{ .Enum = .{
.tag_type = std.math.IntFittingRange(0, enum_fields.len - 1), .tag_type = if (enum_fields.len > 0) std.math.IntFittingRange(0, enum_fields.len - 1) else u0,
.fields = enum_fields, .fields = enum_fields,
.decls = &[_]std.builtin.Type.Declaration{}, .decls = &[_]std.builtin.Type.Declaration{},
.is_exhaustive = true, .is_exhaustive = true,
@ -374,55 +397,29 @@ fn GlobalEvent(comptime mods: anytype) type {
}); });
} }
/// enum describing every possible comptime-known event name /// enum describing every possible comptime-known global event name
fn EventName(comptime mods: anytype) type { fn GlobalEventEnum(comptime mods: anytype) type {
var enum_fields: []const std.builtin.Type.EnumField = &[0]std.builtin.Type.EnumField{}; var enum_fields: []const std.builtin.Type.EnumField = &[0]std.builtin.Type.EnumField{};
var i: u32 = 0; var i: u32 = 0;
for (mods) |M| { for (mods) |M| {
// Global event handlers _ = Module(M); // Validate the module
for (@typeInfo(M).Struct.decls) |decl| { inline for (M.events) |event| {
const is_event_handler = switch (@typeInfo(@TypeOf(@field(M, decl.name)))) { const Event = @TypeOf(event);
.Fn => true, const name_tag = if (@hasField(Event, "global")) event.global else continue;
.Type => switch (@typeInfo(@field(M, decl.name))) {
.Fn => true,
else => false,
},
else => false,
};
if (is_event_handler) {
const exists_already = blk2: {
for (enum_fields) |existing| if (std.mem.eql(u8, existing.name, decl.name)) break :blk2 true;
break :blk2 false;
};
if (!exists_already) {
enum_fields = enum_fields ++ [_]std.builtin.Type.EnumField{.{ .name = decl.name, .value = i }};
i += 1;
}
}
}
// Local event handlers const exists_already = blk: {
if (@hasDecl(M, "local")) { for (enum_fields) |existing| if (std.mem.eql(u8, existing.name, @tagName(name_tag))) break :blk true;
for (@typeInfo(M.local).Struct.decls) |decl| { break :blk false;
switch (@typeInfo(@TypeOf(@field(M.local, decl.name)))) {
.Fn => {
const exists_already = blk2: {
for (enum_fields) |existing| if (std.mem.eql(u8, existing.name, decl.name)) break :blk2 true;
break :blk2 false;
}; };
if (!exists_already) { if (!exists_already) {
enum_fields = enum_fields ++ [_]std.builtin.Type.EnumField{.{ .name = decl.name, .value = i }}; enum_fields = enum_fields ++ [_]std.builtin.Type.EnumField{.{ .name = @tagName(name_tag), .value = i }};
i += 1; i += 1;
} }
},
else => {},
}
}
} }
} }
return @Type(.{ return @Type(.{
.Enum = .{ .Enum = .{
.tag_type = std.math.IntFittingRange(0, enum_fields.len - 1), .tag_type = if (enum_fields.len > 0) std.math.IntFittingRange(0, enum_fields.len - 1) else u0,
.fields = enum_fields, .fields = enum_fields,
.decls = &[_]std.builtin.Type.Declaration{}, .decls = &[_]std.builtin.Type.Declaration{},
.is_exhaustive = true, .is_exhaustive = true,
@ -468,6 +465,48 @@ fn NamespacedModules(comptime modules: anytype) type {
}); });
} }
fn validateEvents(comptime error_prefix: anytype, comptime events: anytype) void {
if (@typeInfo(@TypeOf(events)) != .Struct or !@typeInfo(@TypeOf(events)).Struct.is_tuple) {
@compileError(error_prefix ++ "expected a tuple of structs, found: " ++ @typeName(@TypeOf(events)));
}
inline for (events, 0..) |event, i| {
const Event = @TypeOf(event);
if (@typeInfo(Event) != .Struct) @compileError(std.fmt.comptimePrint(
error_prefix ++ "expected a tuple of structs, found tuple element ({}): {s}",
.{ i, @typeName(Event) },
));
// Verify .global = .foo, or .local = .foo, event handler name field
const name_tag = if (@hasField(Event, "global")) event.global else if (@hasField(Event, "local")) event.local else @compileError(std.fmt.comptimePrint(
error_prefix ++ "tuple element ({}) missing field `.global = .foo` or `.local = .foo` (event handler kind / name)",
.{i},
));
const is_global = if (@hasField(Event, "global")) true else false;
if (@typeInfo(@TypeOf(name_tag)) != .EnumLiteral) @compileError(std.fmt.comptimePrint(
error_prefix ++ "tuple element ({}) expected field `.{s} = .foo`, found: {s}",
.{ i, if (is_global) "global" else "local", @typeName(@TypeOf(name_tag)) },
));
// Verify .handler = fn, field
if (!@hasField(Event, "handler")) @compileError(std.fmt.comptimePrint(
error_prefix ++ "tuple element ({}) missing field `.handler = fn`",
.{i},
));
const valid_handler_type = switch (@typeInfo(@TypeOf(event.handler))) {
.Fn => true,
.Type => switch (@typeInfo(event.handler)) {
.Fn => true,
else => false,
},
else => false,
};
if (!valid_handler_type) @compileError(std.fmt.comptimePrint(
error_prefix ++ "tuple element ({}) expected field `.handler = fn`, found: {s}",
.{ i, @typeName(@TypeOf(event.handler)) },
));
}
}
test { test {
testing.refAllDeclsRecursive(@This()); testing.refAllDeclsRecursive(@This());
} }
@ -486,7 +525,11 @@ test Module {
pub const location = @Vector(3, f32); pub const location = @Vector(3, f32);
}; };
pub fn tick() !void {} pub const events = .{
.{ .global = .tick, .handler = tick },
};
fn tick() !void {}
}); });
} }
@ -504,20 +547,28 @@ test Modules {
pub const location = @Vector(3, f32); pub const location = @Vector(3, f32);
}; };
pub fn tick() !void {} pub const events = .{
.{ .global = .tick, .handler = tick },
};
fn tick() !void {}
}); });
const Renderer = Module(struct { const Renderer = Module(struct {
pub const name = .engine_renderer; pub const name = .engine_renderer;
pub const events = .{
.{ .global = .tick, .handler = tick },
};
/// Renderer module components /// Renderer module components
pub const components = struct {}; pub const components = struct {};
pub fn tick() !void {} fn tick() !void {}
}); });
const Sprite2D = Module(struct { const Sprite2D = Module(struct {
pub const name = .engine_sprite2d; pub const name = .engine_sprite2d;
pub const events = .{};
}); });
var modules: Modules(.{ var modules: Modules(.{
@ -532,39 +583,48 @@ test Modules {
testing.refAllDeclsRecursive(Sprite2D); testing.refAllDeclsRecursive(Sprite2D);
} }
test EventName { test "event name" {
const Physics = Module(struct { const Physics = Module(struct {
pub const name = .engine_physics; pub const name = .engine_physics;
pub const components = struct {}; pub const components = struct {};
pub const events = .{
pub fn foo() !void {} .{ .global = .foo, .handler = foo },
pub fn bar() !void {} .{ .global = .bar, .handler = bar },
.{ .local = .baz, .handler = baz },
pub const local = struct { .{ .local = .bam, .handler = bam },
pub fn baz() !void {}
pub fn bam() !void {}
}; };
fn foo() !void {}
fn bar() !void {}
fn baz() !void {}
fn bam() !void {}
}); });
const Renderer = Module(struct { const Renderer = Module(struct {
pub const name = .engine_renderer; pub const name = .engine_renderer;
pub const components = struct {}; pub const components = struct {};
pub const events = .{
.{ .global = .foo_unused, .handler = fn (f32, i32) void },
.{ .global = .bar_unused, .handler = fn (i32, f32) void },
.{ .global = .tick, .handler = tick },
.{ .global = .foo, .handler = foo },
.{ .global = .bar, .handler = bar },
};
pub const fooUnused = fn (f32, i32) void; fn tick() !void {}
pub const barUnused = fn (i32, f32) void; fn foo() !void {} // same .foo name as .engine_physics.foo
fn bar() !void {} // same .bar name as .engine_physics.bar
pub fn tick() !void {}
pub fn foo() !void {} // same .foo name as .engine_physics.foo
pub fn bar() !void {} // same .bar name as .engine_physics.bar
}); });
const Sprite2D = Module(struct { const Sprite2D = Module(struct {
pub const name = .engine_sprite2d; pub const name = .engine_sprite2d;
pub const events = .{
pub fn tick() void {} // same .tick as .engine_renderer.tick .{ .global = .tick, .handler = tick },
pub const local = struct { .{ .global = .foobar, .handler = foobar },
pub fn foobar() void {}
}; };
fn tick() void {} // same .tick as .engine_renderer.tick
fn foobar() void {}
}); });
const Mods = Modules(.{ const Mods = Modules(.{
@ -572,38 +632,36 @@ test EventName {
Renderer, Renderer,
Sprite2D, Sprite2D,
}); });
const info = @typeInfo(EventName(Mods.modules)).Enum;
try testing.expect(type, u3).eql(info.tag_type); const locals = @typeInfo(LocalEventEnum(Mods.modules)).Enum;
try testing.expect(usize, 8).eql(info.fields.len); try testing.expect(type, u1).eql(locals.tag_type);
try testing.expect([]const u8, "foo").eql(info.fields[0].name); try testing.expect(usize, 2).eql(locals.fields.len);
try testing.expect([]const u8, "bar").eql(info.fields[1].name); try testing.expect([]const u8, "baz").eql(locals.fields[0].name);
try testing.expect([]const u8, "baz").eql(info.fields[2].name); try testing.expect([]const u8, "bam").eql(locals.fields[1].name);
try testing.expect([]const u8, "bam").eql(info.fields[3].name);
try testing.expect([]const u8, "fooUnused").eql(info.fields[4].name);
try testing.expect([]const u8, "barUnused").eql(info.fields[5].name);
try testing.expect([]const u8, "tick").eql(info.fields[6].name);
try testing.expect([]const u8, "foobar").eql(info.fields[7].name);
const global_info = @typeInfo(GlobalEvent(Mods.modules)).Enum; const globals = @typeInfo(GlobalEventEnum(Mods.modules)).Enum;
try testing.expect(type, u3).eql(global_info.tag_type); try testing.expect(type, u3).eql(globals.tag_type);
try testing.expect(usize, 5).eql(global_info.fields.len); try testing.expect(usize, 6).eql(globals.fields.len);
try testing.expect([]const u8, "foo").eql(global_info.fields[0].name); try testing.expect([]const u8, "foo").eql(globals.fields[0].name);
try testing.expect([]const u8, "bar").eql(global_info.fields[1].name); try testing.expect([]const u8, "bar").eql(globals.fields[1].name);
try testing.expect([]const u8, "fooUnused").eql(global_info.fields[2].name); try testing.expect([]const u8, "foo_unused").eql(globals.fields[2].name);
try testing.expect([]const u8, "barUnused").eql(global_info.fields[3].name); try testing.expect([]const u8, "bar_unused").eql(globals.fields[3].name);
try testing.expect([]const u8, "tick").eql(global_info.fields[4].name); try testing.expect([]const u8, "tick").eql(globals.fields[4].name);
try testing.expect([]const u8, "foobar").eql(globals.fields[5].name);
} }
test ModuleName { test ModuleName {
const Physics = Module(struct { const Physics = Module(struct {
pub const name = .engine_physics; pub const name = .engine_physics;
pub const events = .{};
}); });
const Renderer = Module(struct { const Renderer = Module(struct {
pub const name = .engine_renderer; pub const name = .engine_renderer;
pub const events = .{};
}); });
const Sprite2D = Module(struct { const Sprite2D = Module(struct {
pub const name = .engine_sprite2d; pub const name = .engine_sprite2d;
pub const events = .{};
}); });
const Mods = Modules(.{ const Mods = Modules(.{
Physics, Physics,
@ -742,34 +800,39 @@ test "event name calling" {
const Physics = Module(struct { const Physics = Module(struct {
pub const name = .engine_physics; pub const name = .engine_physics;
pub const components = struct {}; pub const components = struct {};
pub const events = .{
.{ .global = .tick, .handler = tick },
.{ .local = .update, .handler = update },
.{ .local = .calc, .handler = calc },
};
pub fn tick() void { fn tick() void {
global.ticks += 1; global.ticks += 1;
} }
pub const local = struct { fn update() void {
pub fn update() void {
global.physics_updates += 1; global.physics_updates += 1;
} }
pub fn calc() void { fn calc() void {
global.physics_calc += 1; global.physics_calc += 1;
} }
};
}); });
const Renderer = Module(struct { const Renderer = Module(struct {
pub const name = .engine_renderer; pub const name = .engine_renderer;
pub const components = struct {}; pub const components = struct {};
pub const events = .{
.{ .global = .tick, .handler = tick },
.{ .local = .update, .handler = update },
};
pub fn tick() void { fn tick() void {
global.ticks += 1; global.ticks += 1;
} }
pub const local = struct { fn update() void {
pub fn update() void {
global.renderer_updates += 1; global.renderer_updates += 1;
} }
};
}); });
var modules: Modules(.{ var modules: Modules(.{
@ -779,52 +842,46 @@ test "event name calling" {
try modules.init(testing.allocator); try modules.init(testing.allocator);
defer modules.deinit(testing.allocator); defer modules.deinit(testing.allocator);
try @TypeOf(modules).call(.tick, &.{}, .{}); try @TypeOf(modules).callGlobal(.tick, &.{}, .{});
try testing.expect(usize, 2).eql(global.ticks); try testing.expect(usize, 2).eql(global.ticks);
// Check we can use .call() with a runtime-known event name. // Check we can use .callGlobal() with a runtime-known event name.
const alloc = try testing.allocator.create(u3); const alloc = try testing.allocator.create(u3);
defer testing.allocator.destroy(alloc); defer testing.allocator.destroy(alloc);
const E = EventName(@TypeOf(modules).modules); const GE = GlobalEventEnum(@TypeOf(modules).modules);
alloc.* = @intFromEnum(@as(E, .tick)); const LE = LocalEventEnum(@TypeOf(modules).modules);
alloc.* = @intFromEnum(@as(GE, .tick));
var event_name = @as(E, @enumFromInt(alloc.*)); const global_event_name = @as(GE, @enumFromInt(alloc.*));
try @TypeOf(modules).call(event_name, &.{}, .{}); try @TypeOf(modules).callGlobal(global_event_name, &.{}, .{});
try testing.expect(usize, 4).eql(global.ticks); try testing.expect(usize, 4).eql(global.ticks);
// Check call() behavior with a valid event name enum, but not a valid global event handler name
alloc.* = @intFromEnum(@as(E, .update));
event_name = @as(E, @enumFromInt(alloc.*));
try @TypeOf(modules).call(event_name, &.{}, .{});
try testing.expect(usize, 4).eql(global.ticks);
try testing.expect(usize, 0).eql(global.physics_updates);
try testing.expect(usize, 0).eql(global.renderer_updates);
// Check we can use .callLocal() with a runtime-known event and module name. // Check we can use .callLocal() with a runtime-known event and module name.
const m_alloc = try testing.allocator.create(u3); const m_alloc = try testing.allocator.create(u3);
defer testing.allocator.destroy(m_alloc); defer testing.allocator.destroy(m_alloc);
const M = ModuleName(@TypeOf(modules).modules); const M = ModuleName(@TypeOf(modules).modules);
m_alloc.* = @intFromEnum(@as(M, .engine_renderer)); m_alloc.* = @intFromEnum(@as(M, .engine_renderer));
alloc.* = @intFromEnum(@as(E, .update)); alloc.* = @intFromEnum(@as(LE, .update));
var module_name = @as(M, @enumFromInt(m_alloc.*)); var module_name = @as(M, @enumFromInt(m_alloc.*));
try @TypeOf(modules).callLocal(module_name, event_name, &.{}, .{}); var local_event_name = @as(LE, @enumFromInt(alloc.*));
try @TypeOf(modules).callLocal(module_name, event_name, &.{}, .{}); try @TypeOf(modules).callLocal(module_name, local_event_name, &.{}, .{});
try @TypeOf(modules).callLocal(module_name, local_event_name, &.{}, .{});
try testing.expect(usize, 4).eql(global.ticks); try testing.expect(usize, 4).eql(global.ticks);
try testing.expect(usize, 0).eql(global.physics_updates); try testing.expect(usize, 0).eql(global.physics_updates);
try testing.expect(usize, 2).eql(global.renderer_updates); try testing.expect(usize, 2).eql(global.renderer_updates);
m_alloc.* = @intFromEnum(@as(M, .engine_physics)); m_alloc.* = @intFromEnum(@as(M, .engine_physics));
alloc.* = @intFromEnum(@as(E, .update)); alloc.* = @intFromEnum(@as(LE, .update));
module_name = @as(M, @enumFromInt(m_alloc.*)); module_name = @as(M, @enumFromInt(m_alloc.*));
event_name = @as(E, @enumFromInt(alloc.*)); local_event_name = @as(LE, @enumFromInt(alloc.*));
try @TypeOf(modules).callLocal(module_name, event_name, &.{}, .{}); try @TypeOf(modules).callLocal(module_name, local_event_name, &.{}, .{});
try testing.expect(usize, 1).eql(global.physics_updates); try testing.expect(usize, 1).eql(global.physics_updates);
m_alloc.* = @intFromEnum(@as(M, .engine_physics)); m_alloc.* = @intFromEnum(@as(M, .engine_physics));
alloc.* = @intFromEnum(@as(E, .calc)); alloc.* = @intFromEnum(@as(LE, .calc));
module_name = @as(M, @enumFromInt(m_alloc.*)); module_name = @as(M, @enumFromInt(m_alloc.*));
event_name = @as(E, @enumFromInt(alloc.*)); local_event_name = @as(LE, @enumFromInt(alloc.*));
try @TypeOf(modules).callLocal(module_name, event_name, &.{}, .{}); try @TypeOf(modules).callLocal(module_name, local_event_name, &.{}, .{});
try testing.expect(usize, 4).eql(global.ticks); try testing.expect(usize, 4).eql(global.ticks);
try testing.expect(usize, 1).eql(global.physics_calc); try testing.expect(usize, 1).eql(global.physics_calc);
try testing.expect(usize, 1).eql(global.physics_updates); try testing.expect(usize, 1).eql(global.physics_updates);
@ -846,48 +903,57 @@ test "dispatch" {
}{}; }{};
const Minimal = Module(struct { const Minimal = Module(struct {
pub const name = .engine_minimal; pub const name = .engine_minimal;
pub const events = .{};
}); });
const Physics = Module(struct { const Physics = Module(struct {
pub const name = .engine_physics; pub const name = .engine_physics;
pub const components = struct {}; pub const components = struct {};
pub const events = .{
.{ .global = .tick, .handler = tick },
.{ .local = .update, .handler = update },
.{ .local = .calc, .handler = calc },
};
pub fn tick() void { fn tick() void {
global.ticks += 1; global.ticks += 1;
} }
pub const local = struct { fn update() void {
pub fn update() void {
global.physics_updates += 1; global.physics_updates += 1;
} }
pub fn calc() void { fn calc() void {
global.physics_calc += 1; global.physics_calc += 1;
} }
};
}); });
const Renderer = Module(struct { const Renderer = Module(struct {
pub const name = .engine_renderer; pub const name = .engine_renderer;
pub const components = struct {}; pub const components = struct {};
pub const events = .{
.{ .global = .tick, .handler = tick },
.{ .global = .frame_done, .handler = fn (i32) void },
.{ .local = .update, .handler = update },
.{ .local = .basic_args, .handler = basicArgs },
.{ .local = .injected_args, .handler = injectedArgs },
};
pub const frameDone = fn (i32) void; pub const frameDone = fn (i32) void;
pub fn tick() void { fn tick() void {
global.ticks += 1; global.ticks += 1;
} }
pub const local = struct { fn update() void {
pub fn update() void {
global.renderer_updates += 1; global.renderer_updates += 1;
} }
pub fn basicArgs(a: u32, b: u32) void { fn basicArgs(a: u32, b: u32) void {
global.basic_args_sum = a + b; global.basic_args_sum = a + b;
} }
pub fn injectedArgs(foo_ptr: *@TypeOf(foo), a: u32, b: u32) void { fn injectedArgs(foo_ptr: *@TypeOf(foo), a: u32, b: u32) void {
foo_ptr.*.injected_args_sum = a + b; foo_ptr.*.injected_args_sum = a + b;
} }
};
}); });
var modules: Modules(.{ var modules: Modules(.{
@ -898,7 +964,8 @@ test "dispatch" {
try modules.init(testing.allocator); try modules.init(testing.allocator);
defer modules.deinit(testing.allocator); defer modules.deinit(testing.allocator);
const E = EventName(@TypeOf(modules).modules); const GE = GlobalEventEnum(@TypeOf(modules).modules);
const LE = LocalEventEnum(@TypeOf(modules).modules);
const M = ModuleName(@TypeOf(modules).modules); const M = ModuleName(@TypeOf(modules).modules);
// Global events // Global events
@ -912,14 +979,14 @@ test "dispatch" {
try modules.dispatch(.{&foo}); try modules.dispatch(.{&foo});
try testing.expect(usize, 2).eql(global.ticks); try testing.expect(usize, 2).eql(global.ticks);
// TODO: make sendDynamic take an args type to avoid footguns with comptime values, etc. // TODO: make sendDynamic take an args type to avoid footguns with comptime values, etc.
modules.sendDynamic(@intFromEnum(@as(E, .tick)), .{}); modules.sendDynamic(@intFromEnum(@as(GE, .tick)), .{});
try modules.dispatch(.{&foo}); try modules.dispatch(.{&foo});
try testing.expect(usize, 4).eql(global.ticks); 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;` // Global events which are not handled by anyone yet can be written as `pub const fooBar = fn() void;`
// within a module, which allows pre-declaring that `fooBar` is a valid global event, and enables // within a module, which allows pre-declaring that `fooBar` is a valid global event, and enables
// its arguments to be inferred still like this: // its arguments to be inferred still like this:
modules.send(.frameDone, .{1337}); modules.send(.frame_done, .{1337});
// Local events // Local events
modules.sendToModule(.engine_renderer, .update, .{}); modules.sendToModule(.engine_renderer, .update, .{});
@ -928,7 +995,7 @@ test "dispatch" {
modules.sendToModule(.engine_physics, .update, .{}); modules.sendToModule(.engine_physics, .update, .{});
modules.sendToModuleDynamic( modules.sendToModuleDynamic(
@intFromEnum(@as(M, .engine_physics)), @intFromEnum(@as(M, .engine_physics)),
@intFromEnum(@as(E, .calc)), @intFromEnum(@as(LE, .calc)),
.{}, .{},
); );
try modules.dispatch(.{&foo}); try modules.dispatch(.{&foo});
@ -936,8 +1003,8 @@ test "dispatch" {
try testing.expect(usize, 1).eql(global.physics_calc); try testing.expect(usize, 1).eql(global.physics_calc);
// Local events // Local events
modules.sendToModule(.engine_renderer, .basicArgs, .{ .@"0" = @as(u32, 1), .@"1" = @as(u32, 2) }); // TODO: match arguments against fn ArgsTuple, for correctness and type inference modules.sendToModule(.engine_renderer, .basic_args, .{ .@"0" = @as(u32, 1), .@"1" = @as(u32, 2) }); // TODO: match arguments against fn ArgsTuple, for correctness and type inference
modules.sendToModule(.engine_renderer, .injectedArgs, .{ .@"0" = @as(u32, 1), .@"1" = @as(u32, 2) }); modules.sendToModule(.engine_renderer, .injected_args, .{ .@"0" = @as(u32, 1), .@"1" = @as(u32, 2) });
try modules.dispatch(.{&foo}); try modules.dispatch(.{&foo});
try testing.expect(usize, 3).eql(global.basic_args_sum); try testing.expect(usize, 3).eql(global.basic_args_sum);
try testing.expect(usize, 3).eql(foo.injected_args_sum); try testing.expect(usize, 3).eql(foo.injected_args_sum);