diff --git a/examples/custom-renderer/Game.zig b/examples/custom-renderer/Game.zig index 8d860d91..e6d1ae25 100644 --- a/examples/custom-renderer/Game.zig +++ b/examples/custom-renderer/Game.zig @@ -20,6 +20,11 @@ pub const components = struct { 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 // 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 Mod = mach.Mod(@This()); -pub fn init( +// TODO(engine): remove need for returning an error here +fn init( engine: *mach.Engine.Mod, renderer: *Renderer.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, renderer: *Renderer.Mod, game: *Mod, diff --git a/examples/custom-renderer/Renderer.zig b/examples/custom-renderer/Renderer.zig index 85a3f6fc..d153137e 100644 --- a/examples/custom-renderer/Renderer.zig +++ b/examples/custom-renderer/Renderer.zig @@ -26,13 +26,19 @@ pub const components = struct { 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. const UniformBufferObject = packed struct { offset: Vec3.Vector, scale: f32, }; -pub fn init( +fn init( engine: *mach.Engine.Mod, renderer: *Mod, ) !void { @@ -97,7 +103,7 @@ pub fn init( shader_module.release(); } -pub fn deinit( +fn deinit( renderer: *Mod, ) !void { renderer.state.pipeline.release(); @@ -106,7 +112,7 @@ pub fn deinit( renderer.state.uniform_buffer.release(); } -pub fn tick( +fn tick( engine: *mach.Engine.Mod, renderer: *Mod, ) !void { diff --git a/examples/glyphs/Game.zig b/examples/glyphs/Game.zig index 024283c5..6a4937f4 100644 --- a/examples/glyphs/Game.zig +++ b/examples/glyphs/Game.zig @@ -40,12 +40,17 @@ const d0 = 0.000001; pub const name = .game; pub const Mod = mach.Mod(@This()); +pub const events = .{ + .{ .global = .init, .handler = init }, + .{ .global = .tick, .handler = tick }, +}; + pub const Pipeline = enum(u32) { default, text, }; -pub fn init( +fn init( engine: *mach.Engine.Mod, sprite_mod: *Sprite.Mod, text_mod: *Text.Mod, @@ -91,7 +96,7 @@ pub fn init( }; } -pub fn tick( +fn tick( engine: *mach.Engine.Mod, sprite_mod: *Sprite.Mod, text_mod: *Text.Mod, diff --git a/examples/glyphs/Text.zig b/examples/glyphs/Text.zig index 7e371c91..95c3dcc8 100644 --- a/examples/glyphs/Text.zig +++ b/examples/glyphs/Text.zig @@ -8,6 +8,12 @@ const assets = @import("assets"); pub const name = .game_text; 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); texture_atlas: mach.gfx.Atlas, @@ -16,7 +22,7 @@ ft: ft.Library, face: ft.Face, regions: RegionMap = .{}, -pub fn deinit( +fn deinit( engine: *mach.Engine.Mod, text_mod: *Mod, ) !void { @@ -27,97 +33,95 @@ pub fn deinit( text_mod.state.regions.deinit(engine.allocator); } -pub const local = struct { - pub fn init( - engine: *mach.Engine.Mod, - text_mod: *Mod, - ) !void { - const device = engine.state.device; +fn init( + engine: *mach.Engine.Mod, + text_mod: *Mod, +) !void { + const device = engine.state.device; - // rgba32_pixels - const img_size = gpu.Extent3D{ .width = 1024, .height = 1024 }; + // rgba32_pixels + const img_size = gpu.Extent3D{ .width = 1024, .height = 1024 }; - // Create a GPU texture - const texture = device.createTexture(&.{ - .size = img_size, - .format = .rgba8_unorm, - .usage = .{ - .texture_binding = true, - .copy_dst = true, - .render_attachment = true, - }, - }); + // Create a GPU texture + const texture = device.createTexture(&.{ + .size = img_size, + .format = .rgba8_unorm, + .usage = .{ + .texture_binding = true, + .copy_dst = true, + .render_attachment = true, + }, + }); - var s = &text_mod.state; - s.texture = texture; - s.texture_atlas = try mach.gfx.Atlas.init( - engine.allocator, - img_size.width, - .rgba, - ); + var s = &text_mod.state; + s.texture = texture; + s.texture_atlas = try mach.gfx.Atlas.init( + engine.allocator, + img_size.width, + .rgba, + ); - // TODO: state fields' default values do not work - s.regions = .{}; + // TODO: state fields' default values do not work + s.regions = .{}; - s.ft = try ft.Library.init(); - s.face = try s.ft.createFaceMemory(assets.roboto_medium_ttf, 0); + s.ft = try ft.Library.init(); + 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( - engine: *mach.Engine.Mod, - text_mod: *Mod, - codepoints: []const u21, - ) !void { - const device = engine.state.device; - const queue = device.getQueue(); - var s = &text_mod.state; +fn prepare( + engine: *mach.Engine.Mod, + text_mod: *Mod, + codepoints: []const u21, +) !void { + const device = engine.state.device; + const queue = device.getQueue(); + var s = &text_mod.state; - for (codepoints) |codepoint| { - const font_size = 48 * 1; - try s.face.setCharSize(font_size * 64, 0, 50, 0); - try s.face.loadChar(codepoint, .{ .render = true }); - const glyph = s.face.glyph(); - const metrics = glyph.metrics(); + for (codepoints) |codepoint| { + const font_size = 48 * 1; + try s.face.setCharSize(font_size * 64, 0, 50, 0); + try s.face.loadChar(codepoint, .{ .render = true }); + const glyph = s.face.glyph(); + const metrics = glyph.metrics(); - const glyph_bitmap = glyph.bitmap(); - const glyph_width = glyph_bitmap.width(); - const glyph_height = glyph_bitmap.rows(); + const glyph_bitmap = glyph.bitmap(); + const glyph_width = glyph_bitmap.width(); + const glyph_height = glyph_bitmap.rows(); - // Add 1 pixel padding to texture to avoid bleeding over other textures - const margin = 1; - const glyph_data = try engine.allocator.alloc([4]u8, (glyph_width + (margin * 2)) * (glyph_height + (margin * 2))); - defer engine.allocator.free(glyph_data); - const glyph_buffer = glyph_bitmap.buffer().?; - for (glyph_data, 0..) |*data, i| { - const x = i % (glyph_width + (margin * 2)); - const y = i / (glyph_width + (margin * 2)); - if (x < margin or x > (glyph_width + margin) or y < margin or y > (glyph_height + margin)) { - data.* = [4]u8{ 0, 0, 0, 0 }; - } else { - const alpha = glyph_buffer[((y - margin) * glyph_width + (x - margin)) % glyph_buffer.len]; - data.* = [4]u8{ 0, 0, 0, alpha }; - } + // Add 1 pixel padding to texture to avoid bleeding over other textures + const margin = 1; + const glyph_data = try engine.allocator.alloc([4]u8, (glyph_width + (margin * 2)) * (glyph_height + (margin * 2))); + defer engine.allocator.free(glyph_data); + const glyph_buffer = glyph_bitmap.buffer().?; + for (glyph_data, 0..) |*data, i| { + const x = i % (glyph_width + (margin * 2)); + const y = i / (glyph_width + (margin * 2)); + if (x < margin or x > (glyph_width + margin) or y < margin or y > (glyph_height + margin)) { + data.* = [4]u8{ 0, 0, 0, 0 }; + } else { + const alpha = glyph_buffer[((y - margin) * glyph_width + (x - margin)) % glyph_buffer.len]; + data.* = [4]u8{ 0, 0, 0, alpha }; } - var glyph_atlas_region = try s.texture_atlas.reserve(engine.allocator, glyph_width + (margin * 2), glyph_height + (margin * 2)); - s.texture_atlas.set(glyph_atlas_region, @as([*]const u8, @ptrCast(glyph_data.ptr))[0 .. glyph_data.len * 4]); - - glyph_atlas_region.x += margin; - glyph_atlas_region.y += margin; - glyph_atlas_region.width -= margin * 2; - glyph_atlas_region.height -= margin * 2; - - try s.regions.put(engine.allocator, codepoint, glyph_atlas_region); - _ = metrics; } + var glyph_atlas_region = try s.texture_atlas.reserve(engine.allocator, glyph_width + (margin * 2), glyph_height + (margin * 2)); + s.texture_atlas.set(glyph_atlas_region, @as([*]const u8, @ptrCast(glyph_data.ptr))[0 .. glyph_data.len * 4]); - // rgba32_pixels - const img_size = gpu.Extent3D{ .width = 1024, .height = 1024 }; - const data_layout = gpu.Texture.DataLayout{ - .bytes_per_row = @as(u32, @intCast(img_size.width * 4)), - .rows_per_image = @as(u32, @intCast(img_size.height)), - }; - queue.writeTexture(&.{ .texture = s.texture }, &data_layout, &img_size, s.texture_atlas.data); + glyph_atlas_region.x += margin; + glyph_atlas_region.y += margin; + glyph_atlas_region.width -= margin * 2; + glyph_atlas_region.height -= margin * 2; + + try s.regions.put(engine.allocator, codepoint, glyph_atlas_region); + _ = metrics; } -}; + + // rgba32_pixels + const img_size = gpu.Extent3D{ .width = 1024, .height = 1024 }; + const data_layout = gpu.Texture.DataLayout{ + .bytes_per_row = @as(u32, @intCast(img_size.width * 4)), + .rows_per_image = @as(u32, @intCast(img_size.height)), + }; + queue.writeTexture(&.{ .texture = s.texture }, &data_layout, &img_size, s.texture_atlas.data); +} diff --git a/examples/sprite/Game.zig b/examples/sprite/Game.zig index 19fc59ce..da4c173f 100644 --- a/examples/sprite/Game.zig +++ b/examples/sprite/Game.zig @@ -41,11 +41,16 @@ const d0 = 0.000001; pub const name = .game; pub const Mod = mach.Mod(@This()); +pub const events = .{ + .{ .global = .init, .handler = init }, + .{ .global = .tick, .handler = tick }, +}; + pub const Pipeline = enum(u32) { default, }; -pub fn init( +fn init( engine: *mach.Engine.Mod, sprite_mod: *Sprite.Mod, game: *Mod, @@ -82,7 +87,7 @@ pub fn init( }; } -pub fn tick( +fn tick( engine: *mach.Engine.Mod, sprite_mod: *Sprite.Mod, game: *Mod, diff --git a/examples/text/Game.zig b/examples/text/Game.zig index 57179c8f..f12b04d9 100644 --- a/examples/text/Game.zig +++ b/examples/text/Game.zig @@ -44,6 +44,12 @@ const d0 = 0.000001; pub const name = .game; 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) { default, }; @@ -58,7 +64,7 @@ const text1: []const []const u8 = &.{ const text2: []const []const u8 = &.{"!$?😊"}; -pub fn init( +fn init( engine: *mach.Engine.Mod, text_mod: *Text.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; } -pub fn tick( +fn tick( engine: *mach.Engine.Mod, text_mod: *Text.Mod, game: *Mod, diff --git a/src/ecs/main.zig b/src/ecs/main.zig index 2089cecb..8ecc3ca2 100644 --- a/src/ecs/main.zig +++ b/src/ecs/main.zig @@ -40,39 +40,47 @@ test "inclusion" { test "example" { const allocator = testing.allocator; - comptime var Renderer = type; - comptime var Physics = type; - Physics = mach.Module(struct { - pointer: u8, + const root = struct { + pub const modules = .{ Renderer, Physics }; - pub const name = .physics; - pub const components = struct { - pub const id = u32; + const Physics = struct { + pointer: u8, + + pub const name = .physics; + pub const components = struct { + pub const id = u32; + }; + pub const events = .{ + .{ .global = .tick, .handler = tick }, + }; + + fn tick(physics: *World(modules).Mod(Physics)) void { + _ = physics; + } }; - pub fn tick(physics: *World(.{ Renderer, Physics }).Mod(Physics)) void { - _ = physics; - } - }); + const Renderer = struct { + pub const name = .renderer; + pub const components = struct { + pub const id = u16; + }; + pub const events = .{ + .{ .global = .tick, .handler = tick }, + }; - Renderer = mach.Module(struct { - pub const name = .renderer; - pub const components = struct { - pub const id = u16; + fn tick( + physics: *World(modules).Mod(Physics), + renderer: *World(modules).Mod(Renderer), + ) void { + _ = renderer; + _ = physics; + } }; - - pub fn tick( - physics: *World(.{ Renderer, Physics }).Mod(Physics), - renderer: *World(.{ Renderer, Physics }).Mod(Renderer), - ) void { - _ = renderer; - _ = physics; - } - }); + }; //------------------------------------------------------------------------- // Create a world. - var world: World(.{ Renderer, Physics }) = undefined; + var world: World(root.modules) = undefined; try world.init(allocator); defer world.deinit(); diff --git a/src/ecs/systems.zig b/src/ecs/systems.zig index 74e7f07a..73ae8f5f 100644 --- a/src/ecs/systems.zig +++ b/src/ecs/systems.zig @@ -6,6 +6,7 @@ const mach = @import("../main.zig"); const Entities = @import("entities.zig").Entities; const EntityID = @import("entities.zig").EntityID; const comp = @import("comptime.zig"); +const Module = @import("../module.zig").Module; pub fn World(comptime mods: anytype) type { const StateT = NamespacedState(mods); @@ -21,8 +22,8 @@ pub fn World(comptime mods: anytype) type { pub const IsInjectedArgument = void; const WorldT = @This(); - pub fn Mod(comptime Module: anytype) type { - const module_tag = Module.name; + pub fn Mod(comptime M: anytype) type { + const module_tag = M.name; const State = @TypeOf(@field(@as(StateT, undefined), @tagName(module_tag))); const components = @field(ns_components, @tagName(module_tag)); return struct { @@ -212,6 +213,8 @@ fn NamespacedComponents(comptime modules: anytype) type { fn NamespacedState(comptime modules: anytype) type { var fields: []const std.builtin.Type.StructField = &[0]std.builtin.Type.StructField{}; 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 = if (state_fields.len > 0) @Type(.{ .Struct = .{ diff --git a/src/engine.zig b/src/engine.zig index b55f0be8..6cf4c14a 100644 --- a/src/engine.zig +++ b/src/engine.zig @@ -11,84 +11,91 @@ const allocator = gpa.allocator(); pub const Engine = struct { device: *gpu.Device, queue: *gpu.Queue, - exit: bool, + should_exit: bool, pass: *gpu.RenderPassEncoder, encoder: *gpu.CommandEncoder, pub const name = .engine; pub const Mod = World.Mod(@This()); - pub const exit = fn () void; - - pub const local = struct { - pub fn init(world: *World) !void { - core.allocator = allocator; - try core.init(.{}); - const state = &world.mod.engine.state; - state.device = core.device; - state.queue = core.device.getQueue(); - state.exit = false; - state.encoder = state.device.createCommandEncoder(&gpu.CommandEncoder.Descriptor{ - .label = "engine.state.encoder", - }); - - world.modules.send(.init, .{}); - } - - pub fn deinit(world: *World, engine: *Mod) void { - // TODO: this triggers a device loss error, which we should handle correctly - // engine.state.device.release(); - engine.state.queue.release(); - world.modules.send(.deinit, .{}); - core.deinit(); - world.deinit(); - _ = gpa.deinit(); - } - - // Engine module's exit handler - pub fn exit(world: *World) void { - world.modules.send(.exit, .{}); - world.mod.engine.state.exit = true; - } - - pub fn beginPass(engine: *Mod, clear_color: gpu.Color) void { - const back_buffer_view = core.swap_chain.getCurrentTextureView().?; - defer back_buffer_view.release(); - - // TODO: expose options - const color_attachment = gpu.RenderPassColorAttachment{ - .view = back_buffer_view, - .clear_value = clear_color, - .load_op = .clear, - .store_op = .store, - }; - const pass_info = gpu.RenderPassDescriptor.init(.{ - .color_attachments = &.{color_attachment}, - }); - - engine.state.pass = engine.state.encoder.beginRenderPass(&pass_info); - } - - pub fn endPass(engine: *Mod) void { - // End this pass - engine.state.pass.end(); - engine.state.pass.release(); - - var command = engine.state.encoder.finish(null); - defer command.release(); - engine.state.encoder.release(); - engine.state.queue.submit(&[_]*gpu.CommandBuffer{command}); - - // Prepare for next pass - engine.state.encoder = engine.state.device.createCommandEncoder(&gpu.CommandEncoder.Descriptor{ - .label = "engine.state.encoder", - }); - } - - pub fn present() void { - core.swap_chain.present(); - } + 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 }, }; + + fn init(world: *World) !void { + core.allocator = allocator; + try core.init(.{}); + const state = &world.mod.engine.state; + state.device = core.device; + state.queue = core.device.getQueue(); + state.should_exit = false; + state.encoder = state.device.createCommandEncoder(&gpu.CommandEncoder.Descriptor{ + .label = "engine.state.encoder", + }); + + world.modules.send(.init, .{}); + } + + fn deinit(world: *World, engine: *Mod) void { + // TODO: this triggers a device loss error, which we should handle correctly + // engine.state.device.release(); + engine.state.queue.release(); + world.modules.send(.deinit, .{}); + core.deinit(); + world.deinit(); + _ = gpa.deinit(); + } + + // Engine module's exit handler + fn exit(world: *World) void { + world.modules.send(.exit, .{}); + world.mod.engine.state.should_exit = true; + } + + fn beginPass(engine: *Mod, clear_color: gpu.Color) void { + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + defer back_buffer_view.release(); + + // TODO: expose options + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = clear_color, + .load_op = .clear, + .store_op = .store, + }; + const pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{color_attachment}, + }); + + engine.state.pass = engine.state.encoder.beginRenderPass(&pass_info); + } + + fn endPass(engine: *Mod) void { + // End this pass + engine.state.pass.end(); + engine.state.pass.release(); + + var command = engine.state.encoder.finish(null); + defer command.release(); + engine.state.encoder.release(); + engine.state.queue.submit(&[_]*gpu.CommandBuffer{command}); + + // Prepare for next pass + engine.state.encoder = engine.state.device.createCommandEncoder(&gpu.CommandEncoder.Descriptor{ + .label = "engine.state.encoder", + }); + } + + fn present() void { + core.swap_chain.present(); + } }; pub const App = struct { @@ -110,7 +117,7 @@ pub const App = struct { app.world.modules.send(.tick, .{}); try app.world.dispatch(); // dispatch .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; } }; diff --git a/src/gfx/Sprite.zig b/src/gfx/Sprite.zig index ea31516c..297601fa 100644 --- a/src/gfx/Sprite.zig +++ b/src/gfx/Sprite.zig @@ -39,6 +39,15 @@ pub const components = struct { 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 { // WebGPU requires that the size of struct fields are multiples of 16 // So we use align(16) and 'extern' to maintain field order @@ -121,251 +130,249 @@ pub const PipelineOptions = struct { 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(); sprite_mod.state.pipelines.deinit(sprite_mod.allocator); } -pub const local = struct { - pub fn init( - sprite_mod: *Mod, - ) !void { - sprite_mod.state = .{ - // TODO: struct default value initializers don't work - .pipelines = .{}, - }; +fn init( + sprite_mod: *Mod, +) !void { + sprite_mod.state = .{ + // TODO: struct default value initializers don't work + .pipelines = .{}, + }; +} + +fn initPipeline( + engine: *Engine.Mod, + sprite_mod: *Mod, + opt: PipelineOptions, +) !void { + const device = engine.state.device; + + const pipeline = try sprite_mod.state.pipelines.getOrPut(engine.allocator, opt.pipeline); + if (pipeline.found_existing) { + pipeline.value_ptr.*.deinit(); } - pub fn initPipeline( - engine: *Engine.Mod, - sprite_mod: *Mod, - opt: PipelineOptions, - ) !void { - const device = engine.state.device; + // Storage buffers + const sprite_buffer_cap = 1024 * 512; // TODO: allow user to specify preallocation + const transforms = device.createBuffer(&.{ + .usage = .{ .storage = true, .copy_dst = true }, + .size = @sizeOf(Mat4x4) * sprite_buffer_cap, + .mapped_at_creation = .false, + }); + const uv_transforms = device.createBuffer(&.{ + .usage = .{ .storage = true, .copy_dst = true }, + .size = @sizeOf(Mat3x3) * sprite_buffer_cap, + .mapped_at_creation = .false, + }); + const sizes = device.createBuffer(&.{ + .usage = .{ .storage = true, .copy_dst = true }, + .size = @sizeOf(Vec2) * sprite_buffer_cap, + .mapped_at_creation = .false, + }); - const pipeline = try sprite_mod.state.pipelines.getOrPut(engine.allocator, opt.pipeline); - if (pipeline.found_existing) { - pipeline.value_ptr.*.deinit(); - } - - // Storage buffers - const sprite_buffer_cap = 1024 * 512; // TODO: allow user to specify preallocation - const transforms = device.createBuffer(&.{ - .usage = .{ .storage = true, .copy_dst = true }, - .size = @sizeOf(Mat4x4) * sprite_buffer_cap, - .mapped_at_creation = .false, - }); - const uv_transforms = device.createBuffer(&.{ - .usage = .{ .storage = true, .copy_dst = true }, - .size = @sizeOf(Mat3x3) * sprite_buffer_cap, - .mapped_at_creation = .false, - }); - const sizes = device.createBuffer(&.{ - .usage = .{ .storage = true, .copy_dst = true }, - .size = @sizeOf(Vec2) * sprite_buffer_cap, - .mapped_at_creation = .false, - }); - - const texture_sampler = opt.texture_sampler orelse device.createSampler(&.{ - .mag_filter = .nearest, - .min_filter = .nearest, - }); - const uniforms = device.createBuffer(&.{ - .usage = .{ .copy_dst = true, .uniform = true }, - .size = @sizeOf(Uniforms), - .mapped_at_creation = .false, - }); - const bind_group_layout = opt.bind_group_layout orelse device.createBindGroupLayout( - &gpu.BindGroupLayout.Descriptor.init(.{ - .entries = &.{ - gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true }, .uniform, false, 0), - gpu.BindGroupLayout.Entry.buffer(1, .{ .vertex = true }, .read_only_storage, false, 0), - gpu.BindGroupLayout.Entry.buffer(2, .{ .vertex = true }, .read_only_storage, false, 0), - gpu.BindGroupLayout.Entry.buffer(3, .{ .vertex = true }, .read_only_storage, false, 0), - gpu.BindGroupLayout.Entry.sampler(4, .{ .fragment = true }, .filtering), - gpu.BindGroupLayout.Entry.texture(5, .{ .fragment = true }, .float, .dimension_2d, false), - gpu.BindGroupLayout.Entry.texture(6, .{ .fragment = true }, .float, .dimension_2d, false), - gpu.BindGroupLayout.Entry.texture(7, .{ .fragment = true }, .float, .dimension_2d, false), - gpu.BindGroupLayout.Entry.texture(8, .{ .fragment = true }, .float, .dimension_2d, false), - }, - }), - ); - defer bind_group_layout.release(); - - const texture_view = opt.texture.createView(&gpu.TextureView.Descriptor{}); - const texture2_view = if (opt.texture2) |tex| tex.createView(&gpu.TextureView.Descriptor{}) else texture_view; - const texture3_view = if (opt.texture3) |tex| tex.createView(&gpu.TextureView.Descriptor{}) else texture_view; - const texture4_view = if (opt.texture4) |tex| tex.createView(&gpu.TextureView.Descriptor{}) else texture_view; - defer texture_view.release(); - defer texture2_view.release(); - defer texture3_view.release(); - defer texture4_view.release(); - - const bind_group = opt.bind_group orelse device.createBindGroup( - &gpu.BindGroup.Descriptor.init(.{ - .layout = bind_group_layout, - .entries = &.{ - gpu.BindGroup.Entry.buffer(0, uniforms, 0, @sizeOf(Uniforms)), - gpu.BindGroup.Entry.buffer(1, transforms, 0, @sizeOf(Mat4x4) * sprite_buffer_cap), - gpu.BindGroup.Entry.buffer(2, uv_transforms, 0, @sizeOf(Mat3x3) * sprite_buffer_cap), - gpu.BindGroup.Entry.buffer(3, sizes, 0, @sizeOf(Vec2) * sprite_buffer_cap), - gpu.BindGroup.Entry.sampler(4, texture_sampler), - gpu.BindGroup.Entry.textureView(5, texture_view), - gpu.BindGroup.Entry.textureView(6, texture2_view), - gpu.BindGroup.Entry.textureView(7, texture3_view), - gpu.BindGroup.Entry.textureView(8, texture4_view), - }, - }), - ); - - const blend_state = opt.blend_state orelse gpu.BlendState{ - .color = .{ - .operation = .add, - .src_factor = .src_alpha, - .dst_factor = .one_minus_src_alpha, + const texture_sampler = opt.texture_sampler orelse device.createSampler(&.{ + .mag_filter = .nearest, + .min_filter = .nearest, + }); + const uniforms = device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .uniform = true }, + .size = @sizeOf(Uniforms), + .mapped_at_creation = .false, + }); + const bind_group_layout = opt.bind_group_layout orelse device.createBindGroupLayout( + &gpu.BindGroupLayout.Descriptor.init(.{ + .entries = &.{ + gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true }, .uniform, false, 0), + gpu.BindGroupLayout.Entry.buffer(1, .{ .vertex = true }, .read_only_storage, false, 0), + gpu.BindGroupLayout.Entry.buffer(2, .{ .vertex = true }, .read_only_storage, false, 0), + gpu.BindGroupLayout.Entry.buffer(3, .{ .vertex = true }, .read_only_storage, false, 0), + gpu.BindGroupLayout.Entry.sampler(4, .{ .fragment = true }, .filtering), + gpu.BindGroupLayout.Entry.texture(5, .{ .fragment = true }, .float, .dimension_2d, false), + gpu.BindGroupLayout.Entry.texture(6, .{ .fragment = true }, .float, .dimension_2d, false), + gpu.BindGroupLayout.Entry.texture(7, .{ .fragment = true }, .float, .dimension_2d, false), + gpu.BindGroupLayout.Entry.texture(8, .{ .fragment = true }, .float, .dimension_2d, false), }, - .alpha = .{ - .operation = .add, - .src_factor = .one, - .dst_factor = .zero, + }), + ); + defer bind_group_layout.release(); + + const texture_view = opt.texture.createView(&gpu.TextureView.Descriptor{}); + const texture2_view = if (opt.texture2) |tex| tex.createView(&gpu.TextureView.Descriptor{}) else texture_view; + const texture3_view = if (opt.texture3) |tex| tex.createView(&gpu.TextureView.Descriptor{}) else texture_view; + const texture4_view = if (opt.texture4) |tex| tex.createView(&gpu.TextureView.Descriptor{}) else texture_view; + defer texture_view.release(); + defer texture2_view.release(); + defer texture3_view.release(); + defer texture4_view.release(); + + const bind_group = opt.bind_group orelse device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = bind_group_layout, + .entries = &.{ + gpu.BindGroup.Entry.buffer(0, uniforms, 0, @sizeOf(Uniforms)), + gpu.BindGroup.Entry.buffer(1, transforms, 0, @sizeOf(Mat4x4) * sprite_buffer_cap), + gpu.BindGroup.Entry.buffer(2, uv_transforms, 0, @sizeOf(Mat3x3) * sprite_buffer_cap), + gpu.BindGroup.Entry.buffer(3, sizes, 0, @sizeOf(Vec2) * sprite_buffer_cap), + gpu.BindGroup.Entry.sampler(4, texture_sampler), + gpu.BindGroup.Entry.textureView(5, texture_view), + gpu.BindGroup.Entry.textureView(6, texture2_view), + gpu.BindGroup.Entry.textureView(7, texture3_view), + gpu.BindGroup.Entry.textureView(8, texture4_view), }, - }; + }), + ); - const shader_module = opt.shader orelse device.createShaderModuleWGSL("sprite.wgsl", @embedFile("sprite.wgsl")); - defer shader_module.release(); + const blend_state = opt.blend_state orelse gpu.BlendState{ + .color = .{ + .operation = .add, + .src_factor = .src_alpha, + .dst_factor = .one_minus_src_alpha, + }, + .alpha = .{ + .operation = .add, + .src_factor = .one, + .dst_factor = .zero, + }, + }; - const color_target = opt.color_target_state orelse gpu.ColorTargetState{ - .format = core.descriptor.format, - .blend = &blend_state, - .write_mask = gpu.ColorWriteMaskFlags.all, - }; - const fragment = opt.fragment_state orelse gpu.FragmentState.init(.{ + const shader_module = opt.shader orelse device.createShaderModuleWGSL("sprite.wgsl", @embedFile("sprite.wgsl")); + defer shader_module.release(); + + const color_target = opt.color_target_state orelse gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &blend_state, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + const fragment = opt.fragment_state orelse gpu.FragmentState.init(.{ + .module = shader_module, + .entry_point = "fragMain", + .targets = &.{color_target}, + }); + + const bind_group_layouts = [_]*gpu.BindGroupLayout{bind_group_layout}; + const pipeline_layout = opt.pipeline_layout orelse device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{ + .bind_group_layouts = &bind_group_layouts, + })); + defer pipeline_layout.release(); + const render_pipeline = device.createRenderPipeline(&gpu.RenderPipeline.Descriptor{ + .fragment = &fragment, + .layout = pipeline_layout, + .vertex = gpu.VertexState{ .module = shader_module, - .entry_point = "fragMain", - .targets = &.{color_target}, - }); + .entry_point = "vertMain", + }, + }); - const bind_group_layouts = [_]*gpu.BindGroupLayout{bind_group_layout}; - const pipeline_layout = opt.pipeline_layout orelse device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{ - .bind_group_layouts = &bind_group_layouts, - })); - defer pipeline_layout.release(); - const render_pipeline = device.createRenderPipeline(&gpu.RenderPipeline.Descriptor{ - .fragment = &fragment, - .layout = pipeline_layout, - .vertex = gpu.VertexState{ - .module = shader_module, - .entry_point = "vertMain", - }, - }); + pipeline.value_ptr.* = Pipeline{ + .render = render_pipeline, + .texture_sampler = texture_sampler, + .texture = opt.texture, + .texture2 = opt.texture2, + .texture3 = opt.texture3, + .texture4 = opt.texture4, + .bind_group = bind_group, + .uniforms = uniforms, + .num_sprites = 0, + .transforms = transforms, + .uv_transforms = uv_transforms, + .sizes = sizes, + }; + pipeline.value_ptr.reference(); +} - pipeline.value_ptr.* = Pipeline{ - .render = render_pipeline, - .texture_sampler = texture_sampler, - .texture = opt.texture, - .texture2 = opt.texture2, - .texture3 = opt.texture3, - .texture4 = opt.texture4, - .bind_group = bind_group, - .uniforms = uniforms, - .num_sprites = 0, - .transforms = transforms, - .uv_transforms = uv_transforms, - .sizes = sizes, - }; - pipeline.value_ptr.reference(); +fn updated( + engine: *Engine.Mod, + sprite_mod: *Mod, + pipeline_id: u32, +) !void { + const pipeline = sprite_mod.state.pipelines.getPtr(pipeline_id).?; + const device = engine.state.device; + + // TODO: make sure these entities only belong to the given pipeline + // we need a better tagging mechanism + var archetypes_iter = engine.entities.query(.{ .all = &.{ + .{ .mach_gfx_sprite = &.{ + .uv_transform, + .transform, + .size, + .pipeline, + } }, + } }); + + const encoder = device.createCommandEncoder(null); + defer encoder.release(); + + pipeline.num_sprites = 0; + var transforms_offset: usize = 0; + var uv_transforms_offset: usize = 0; + var sizes_offset: usize = 0; + while (archetypes_iter.next()) |archetype| { + const transforms = archetype.slice(.mach_gfx_sprite, .transform); + const uv_transforms = archetype.slice(.mach_gfx_sprite, .uv_transform); + const sizes = archetype.slice(.mach_gfx_sprite, .size); + + // TODO: confirm the lifetime of these slices is OK for writeBuffer, how long do they need + // to live? + encoder.writeBuffer(pipeline.transforms, transforms_offset, transforms); + encoder.writeBuffer(pipeline.uv_transforms, uv_transforms_offset, uv_transforms); + encoder.writeBuffer(pipeline.sizes, sizes_offset, sizes); + + transforms_offset += transforms.len; + uv_transforms_offset += uv_transforms.len; + sizes_offset += sizes.len; + pipeline.num_sprites += @intCast(transforms.len); } - pub fn updated( - engine: *Engine.Mod, - sprite_mod: *Mod, - pipeline_id: u32, - ) !void { - const pipeline = sprite_mod.state.pipelines.getPtr(pipeline_id).?; - const device = engine.state.device; + var command = encoder.finish(null); + defer command.release(); - // TODO: make sure these entities only belong to the given pipeline - // we need a better tagging mechanism - var archetypes_iter = engine.entities.query(.{ .all = &.{ - .{ .mach_gfx_sprite = &.{ - .uv_transform, - .transform, - .size, - .pipeline, - } }, - } }); + engine.state.queue.submit(&[_]*gpu.CommandBuffer{command}); +} - const encoder = device.createCommandEncoder(null); - defer encoder.release(); +fn preRender( + engine: *Engine.Mod, + sprite_mod: *Mod, + pipeline_id: u32, +) !void { + const pipeline = sprite_mod.state.pipelines.get(pipeline_id).?; - pipeline.num_sprites = 0; - var transforms_offset: usize = 0; - var uv_transforms_offset: usize = 0; - var sizes_offset: usize = 0; - while (archetypes_iter.next()) |archetype| { - const transforms = archetype.slice(.mach_gfx_sprite, .transform); - const uv_transforms = archetype.slice(.mach_gfx_sprite, .uv_transform); - const sizes = archetype.slice(.mach_gfx_sprite, .size); + // Update uniform buffer + const proj = Mat4x4.projection2D(.{ + .left = -@as(f32, @floatFromInt(core.size().width)) / 2, + .right = @as(f32, @floatFromInt(core.size().width)) / 2, + .bottom = -@as(f32, @floatFromInt(core.size().height)) / 2, + .top = @as(f32, @floatFromInt(core.size().height)) / 2, + .near = -0.1, + .far = 100000, + }); + const uniforms = Uniforms{ + .view_projection = proj, + // TODO: dimensions of other textures, number of textures present + .texture_size = vec2( + @as(f32, @floatFromInt(pipeline.texture.getWidth())), + @as(f32, @floatFromInt(pipeline.texture.getHeight())), + ), + }; - // TODO: confirm the lifetime of these slices is OK for writeBuffer, how long do they need - // to live? - encoder.writeBuffer(pipeline.transforms, transforms_offset, transforms); - encoder.writeBuffer(pipeline.uv_transforms, uv_transforms_offset, uv_transforms); - encoder.writeBuffer(pipeline.sizes, sizes_offset, sizes); + engine.state.encoder.writeBuffer(pipeline.uniforms, 0, &[_]Uniforms{uniforms}); +} - transforms_offset += transforms.len; - uv_transforms_offset += uv_transforms.len; - sizes_offset += sizes.len; - pipeline.num_sprites += @intCast(transforms.len); - } +fn render( + engine: *Engine.Mod, + sprite_mod: *Mod, + pipeline_id: u32, +) !void { + const pipeline = sprite_mod.state.pipelines.get(pipeline_id).?; - var command = encoder.finish(null); - defer command.release(); - - engine.state.queue.submit(&[_]*gpu.CommandBuffer{command}); - } - - pub fn preRender( - engine: *Engine.Mod, - sprite_mod: *Mod, - pipeline_id: u32, - ) !void { - const pipeline = sprite_mod.state.pipelines.get(pipeline_id).?; - - // Update uniform buffer - const proj = Mat4x4.projection2D(.{ - .left = -@as(f32, @floatFromInt(core.size().width)) / 2, - .right = @as(f32, @floatFromInt(core.size().width)) / 2, - .bottom = -@as(f32, @floatFromInt(core.size().height)) / 2, - .top = @as(f32, @floatFromInt(core.size().height)) / 2, - .near = -0.1, - .far = 100000, - }); - const uniforms = Uniforms{ - .view_projection = proj, - // TODO: dimensions of other textures, number of textures present - .texture_size = vec2( - @as(f32, @floatFromInt(pipeline.texture.getWidth())), - @as(f32, @floatFromInt(pipeline.texture.getHeight())), - ), - }; - - engine.state.encoder.writeBuffer(pipeline.uniforms, 0, &[_]Uniforms{uniforms}); - } - - pub fn render( - engine: *Engine.Mod, - sprite_mod: *Mod, - pipeline_id: u32, - ) !void { - const pipeline = sprite_mod.state.pipelines.get(pipeline_id).?; - - // Draw the sprite batch - const pass = engine.state.pass; - const total_vertices = pipeline.num_sprites * 6; - pass.setPipeline(pipeline.render); - // TODO: remove dynamic offsets? - pass.setBindGroup(0, pipeline.bind_group, &.{}); - pass.draw(total_vertices, 1, 0, 0); - } -}; + // Draw the sprite batch + const pass = engine.state.pass; + const total_vertices = pipeline.num_sprites * 6; + pass.setPipeline(pipeline.render); + // TODO: remove dynamic offsets? + pass.setBindGroup(0, pipeline.bind_group, &.{}); + pass.draw(total_vertices, 1, 0, 0); +} diff --git a/src/gfx/Text.zig b/src/gfx/Text.zig index e2513b30..89acb0e7 100644 --- a/src/gfx/Text.zig +++ b/src/gfx/Text.zig @@ -64,6 +64,15 @@ pub const components = struct { 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 { // WebGPU requires that the size of struct fields are multiples of 16 // So we use align(16) and 'extern' to maintain field order @@ -171,383 +180,381 @@ pub const PipelineOptions = struct { 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); text_mod.state.pipelines.deinit(text_mod.allocator); } -pub const local = struct { - pub fn init( - text_mod: *Mod, - ) !void { - text_mod.state = .{ - // TODO: struct default value initializers don't work - .pipelines = .{}, - }; +fn init( + text_mod: *Mod, +) !void { + text_mod.state = .{ + // TODO: struct default value initializers don't work + .pipelines = .{}, + }; +} + +fn initPipeline( + engine: *Engine.Mod, + text_mod: *Mod, + opt: PipelineOptions, +) !void { + const device = engine.state.device; + + const pipeline = try text_mod.state.pipelines.getOrPut(engine.allocator, opt.pipeline); + if (pipeline.found_existing) { + pipeline.value_ptr.*.deinit(engine.allocator); } - pub fn initPipeline( - engine: *Engine.Mod, - text_mod: *Mod, - opt: PipelineOptions, - ) !void { - const device = engine.state.device; + // Prepare texture for the font atlas. + const img_size = gpu.Extent3D{ .width = 1024, .height = 1024 }; + const texture = device.createTexture(&.{ + .size = img_size, + .format = .rgba8_unorm, + .usage = .{ + .texture_binding = true, + .copy_dst = true, + .render_attachment = true, + }, + }); + const texture_atlas = try gfx.Atlas.init( + engine.allocator, + img_size.width, + .rgba, + ); - const pipeline = try text_mod.state.pipelines.getOrPut(engine.allocator, opt.pipeline); - if (pipeline.found_existing) { - pipeline.value_ptr.*.deinit(engine.allocator); - } + // Storage buffers + const buffer_cap = 1024 * 128; // TODO: allow user to specify preallocation + const glyph_buffer_cap = 1024 * 512; // TODO: allow user to specify preallocation + const transforms = device.createBuffer(&.{ + .usage = .{ .storage = true, .copy_dst = true }, + .size = @sizeOf(Mat4x4) * buffer_cap, + .mapped_at_creation = .false, + }); + const colors = device.createBuffer(&.{ + .usage = .{ .storage = true, .copy_dst = true }, + .size = @sizeOf(Vec4) * buffer_cap, + .mapped_at_creation = .false, + }); + const glyphs = device.createBuffer(&.{ + .usage = .{ .storage = true, .copy_dst = true }, + .size = @sizeOf(Glyph) * glyph_buffer_cap, + .mapped_at_creation = .false, + }); - // Prepare texture for the font atlas. - const img_size = gpu.Extent3D{ .width = 1024, .height = 1024 }; - const texture = device.createTexture(&.{ - .size = img_size, - .format = .rgba8_unorm, - .usage = .{ - .texture_binding = true, - .copy_dst = true, - .render_attachment = true, + const texture_sampler = opt.texture_sampler orelse device.createSampler(&.{ + .mag_filter = .nearest, + .min_filter = .nearest, + }); + const uniforms = device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .uniform = true }, + .size = @sizeOf(Uniforms), + .mapped_at_creation = .false, + }); + const bind_group_layout = opt.bind_group_layout orelse device.createBindGroupLayout( + &gpu.BindGroupLayout.Descriptor.init(.{ + .entries = &.{ + gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true }, .uniform, false, 0), + gpu.BindGroupLayout.Entry.buffer(1, .{ .vertex = true }, .read_only_storage, false, 0), + gpu.BindGroupLayout.Entry.buffer(2, .{ .vertex = true }, .read_only_storage, false, 0), + gpu.BindGroupLayout.Entry.buffer(3, .{ .vertex = true }, .read_only_storage, false, 0), + gpu.BindGroupLayout.Entry.sampler(4, .{ .fragment = true }, .filtering), + gpu.BindGroupLayout.Entry.texture(5, .{ .fragment = true }, .float, .dimension_2d, false), + gpu.BindGroupLayout.Entry.texture(6, .{ .fragment = true }, .float, .dimension_2d, false), + gpu.BindGroupLayout.Entry.texture(7, .{ .fragment = true }, .float, .dimension_2d, false), + gpu.BindGroupLayout.Entry.texture(8, .{ .fragment = true }, .float, .dimension_2d, false), }, - }); - const texture_atlas = try gfx.Atlas.init( - engine.allocator, - img_size.width, - .rgba, - ); + }), + ); + defer bind_group_layout.release(); - // Storage buffers - const buffer_cap = 1024 * 128; // TODO: allow user to specify preallocation - const glyph_buffer_cap = 1024 * 512; // TODO: allow user to specify preallocation - const transforms = device.createBuffer(&.{ - .usage = .{ .storage = true, .copy_dst = true }, - .size = @sizeOf(Mat4x4) * buffer_cap, - .mapped_at_creation = .false, - }); - const colors = device.createBuffer(&.{ - .usage = .{ .storage = true, .copy_dst = true }, - .size = @sizeOf(Vec4) * buffer_cap, - .mapped_at_creation = .false, - }); - const glyphs = device.createBuffer(&.{ - .usage = .{ .storage = true, .copy_dst = true }, - .size = @sizeOf(Glyph) * glyph_buffer_cap, - .mapped_at_creation = .false, - }); + const texture_view = texture.createView(&gpu.TextureView.Descriptor{}); + const texture2_view = if (opt.texture2) |tex| tex.createView(&gpu.TextureView.Descriptor{}) else texture_view; + const texture3_view = if (opt.texture3) |tex| tex.createView(&gpu.TextureView.Descriptor{}) else texture_view; + const texture4_view = if (opt.texture4) |tex| tex.createView(&gpu.TextureView.Descriptor{}) else texture_view; + defer texture_view.release(); + defer texture2_view.release(); + defer texture3_view.release(); + defer texture4_view.release(); - const texture_sampler = opt.texture_sampler orelse device.createSampler(&.{ - .mag_filter = .nearest, - .min_filter = .nearest, - }); - const uniforms = device.createBuffer(&.{ - .usage = .{ .copy_dst = true, .uniform = true }, - .size = @sizeOf(Uniforms), - .mapped_at_creation = .false, - }); - const bind_group_layout = opt.bind_group_layout orelse device.createBindGroupLayout( - &gpu.BindGroupLayout.Descriptor.init(.{ - .entries = &.{ - gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true }, .uniform, false, 0), - gpu.BindGroupLayout.Entry.buffer(1, .{ .vertex = true }, .read_only_storage, false, 0), - gpu.BindGroupLayout.Entry.buffer(2, .{ .vertex = true }, .read_only_storage, false, 0), - gpu.BindGroupLayout.Entry.buffer(3, .{ .vertex = true }, .read_only_storage, false, 0), - gpu.BindGroupLayout.Entry.sampler(4, .{ .fragment = true }, .filtering), - gpu.BindGroupLayout.Entry.texture(5, .{ .fragment = true }, .float, .dimension_2d, false), - gpu.BindGroupLayout.Entry.texture(6, .{ .fragment = true }, .float, .dimension_2d, false), - gpu.BindGroupLayout.Entry.texture(7, .{ .fragment = true }, .float, .dimension_2d, false), - gpu.BindGroupLayout.Entry.texture(8, .{ .fragment = true }, .float, .dimension_2d, false), - }, - }), - ); - defer bind_group_layout.release(); - - const texture_view = texture.createView(&gpu.TextureView.Descriptor{}); - const texture2_view = if (opt.texture2) |tex| tex.createView(&gpu.TextureView.Descriptor{}) else texture_view; - const texture3_view = if (opt.texture3) |tex| tex.createView(&gpu.TextureView.Descriptor{}) else texture_view; - const texture4_view = if (opt.texture4) |tex| tex.createView(&gpu.TextureView.Descriptor{}) else texture_view; - defer texture_view.release(); - defer texture2_view.release(); - defer texture3_view.release(); - defer texture4_view.release(); - - const bind_group = opt.bind_group orelse device.createBindGroup( - &gpu.BindGroup.Descriptor.init(.{ - .layout = bind_group_layout, - .entries = &.{ - gpu.BindGroup.Entry.buffer(0, uniforms, 0, @sizeOf(Uniforms)), - gpu.BindGroup.Entry.buffer(1, transforms, 0, @sizeOf(Mat4x4) * buffer_cap), - gpu.BindGroup.Entry.buffer(2, colors, 0, @sizeOf(Vec4) * buffer_cap), - gpu.BindGroup.Entry.buffer(3, glyphs, 0, @sizeOf(Glyph) * glyph_buffer_cap), - gpu.BindGroup.Entry.sampler(4, texture_sampler), - gpu.BindGroup.Entry.textureView(5, texture_view), - gpu.BindGroup.Entry.textureView(6, texture2_view), - gpu.BindGroup.Entry.textureView(7, texture3_view), - gpu.BindGroup.Entry.textureView(8, texture4_view), - }, - }), - ); - - const blend_state = opt.blend_state orelse gpu.BlendState{ - .color = .{ - .operation = .add, - .src_factor = .src_alpha, - .dst_factor = .one_minus_src_alpha, + const bind_group = opt.bind_group orelse device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = bind_group_layout, + .entries = &.{ + gpu.BindGroup.Entry.buffer(0, uniforms, 0, @sizeOf(Uniforms)), + gpu.BindGroup.Entry.buffer(1, transforms, 0, @sizeOf(Mat4x4) * buffer_cap), + gpu.BindGroup.Entry.buffer(2, colors, 0, @sizeOf(Vec4) * buffer_cap), + gpu.BindGroup.Entry.buffer(3, glyphs, 0, @sizeOf(Glyph) * glyph_buffer_cap), + gpu.BindGroup.Entry.sampler(4, texture_sampler), + gpu.BindGroup.Entry.textureView(5, texture_view), + gpu.BindGroup.Entry.textureView(6, texture2_view), + gpu.BindGroup.Entry.textureView(7, texture3_view), + gpu.BindGroup.Entry.textureView(8, texture4_view), }, - .alpha = .{ - .operation = .add, - .src_factor = .one, - .dst_factor = .zero, - }, - }; + }), + ); - const shader_module = opt.shader orelse device.createShaderModuleWGSL("text.wgsl", @embedFile("text.wgsl")); - defer shader_module.release(); + const blend_state = opt.blend_state orelse gpu.BlendState{ + .color = .{ + .operation = .add, + .src_factor = .src_alpha, + .dst_factor = .one_minus_src_alpha, + }, + .alpha = .{ + .operation = .add, + .src_factor = .one, + .dst_factor = .zero, + }, + }; - const color_target = opt.color_target_state orelse gpu.ColorTargetState{ - .format = core.descriptor.format, - .blend = &blend_state, - .write_mask = gpu.ColorWriteMaskFlags.all, - }; - const fragment = opt.fragment_state orelse gpu.FragmentState.init(.{ + const shader_module = opt.shader orelse device.createShaderModuleWGSL("text.wgsl", @embedFile("text.wgsl")); + defer shader_module.release(); + + const color_target = opt.color_target_state orelse gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &blend_state, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + const fragment = opt.fragment_state orelse gpu.FragmentState.init(.{ + .module = shader_module, + .entry_point = "fragMain", + .targets = &.{color_target}, + }); + + const bind_group_layouts = [_]*gpu.BindGroupLayout{bind_group_layout}; + const pipeline_layout = opt.pipeline_layout orelse device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{ + .bind_group_layouts = &bind_group_layouts, + })); + defer pipeline_layout.release(); + const render_pipeline = device.createRenderPipeline(&gpu.RenderPipeline.Descriptor{ + .fragment = &fragment, + .layout = pipeline_layout, + .vertex = gpu.VertexState{ .module = shader_module, - .entry_point = "fragMain", - .targets = &.{color_target}, - }); + .entry_point = "vertMain", + }, + }); - const bind_group_layouts = [_]*gpu.BindGroupLayout{bind_group_layout}; - const pipeline_layout = opt.pipeline_layout orelse device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{ - .bind_group_layouts = &bind_group_layouts, - })); - defer pipeline_layout.release(); - const render_pipeline = device.createRenderPipeline(&gpu.RenderPipeline.Descriptor{ - .fragment = &fragment, - .layout = pipeline_layout, - .vertex = gpu.VertexState{ - .module = shader_module, - .entry_point = "vertMain", - }, - }); + pipeline.value_ptr.* = Pipeline{ + .render = render_pipeline, + .texture_sampler = texture_sampler, + .texture = texture, + .texture_atlas = texture_atlas, + .texture2 = opt.texture2, + .texture3 = opt.texture3, + .texture4 = opt.texture4, + .bind_group = bind_group, + .uniforms = uniforms, + .num_texts = 0, + .num_glyphs = 0, + .transforms = transforms, + .colors = colors, + .glyphs = glyphs, + }; + pipeline.value_ptr.reference(); +} - pipeline.value_ptr.* = Pipeline{ - .render = render_pipeline, - .texture_sampler = texture_sampler, - .texture = texture, - .texture_atlas = texture_atlas, - .texture2 = opt.texture2, - .texture3 = opt.texture3, - .texture4 = opt.texture4, - .bind_group = bind_group, - .uniforms = uniforms, - .num_texts = 0, - .num_glyphs = 0, - .transforms = transforms, - .colors = colors, - .glyphs = glyphs, - }; - pipeline.value_ptr.reference(); - } +fn updated( + engine: *Engine.Mod, + text_mod: *Mod, + pipeline_id: u32, +) !void { + const pipeline = text_mod.state.pipelines.getPtr(pipeline_id).?; + const device = engine.state.device; - pub fn updated( - engine: *Engine.Mod, - text_mod: *Mod, - pipeline_id: u32, - ) !void { - const pipeline = text_mod.state.pipelines.getPtr(pipeline_id).?; - const device = engine.state.device; + // TODO: make sure these entities only belong to the given pipeline + // we need a better tagging mechanism + var archetypes_iter = engine.entities.query(.{ .all = &.{ + .{ .mach_gfx_text = &.{ + .pipeline, + .transform, + .text, + } }, + } }); - // TODO: make sure these entities only belong to the given pipeline - // we need a better tagging mechanism - var archetypes_iter = engine.entities.query(.{ .all = &.{ - .{ .mach_gfx_text = &.{ - .pipeline, - .transform, - .text, - } }, - } }); + const encoder = device.createCommandEncoder(null); + defer encoder.release(); - const encoder = device.createCommandEncoder(null); - defer encoder.release(); + pipeline.num_texts = 0; + pipeline.num_glyphs = 0; + var glyphs = std.ArrayListUnmanaged(Glyph){}; + var transforms_offset: usize = 0; + var texture_update = false; + while (archetypes_iter.next()) |archetype| { + const transforms = archetype.slice(.mach_gfx_text, .transform); - pipeline.num_texts = 0; - pipeline.num_glyphs = 0; - var glyphs = std.ArrayListUnmanaged(Glyph){}; - var transforms_offset: usize = 0; - var texture_update = false; - while (archetypes_iter.next()) |archetype| { - const transforms = archetype.slice(.mach_gfx_text, .transform); + // TODO: confirm the lifetime of these slices is OK for writeBuffer, how long do they need + // to live? + encoder.writeBuffer(pipeline.transforms, transforms_offset, transforms); + // encoder.writeBuffer(pipeline.colors, colors_offset, colors); - // TODO: confirm the lifetime of these slices is OK for writeBuffer, how long do they need - // to live? - encoder.writeBuffer(pipeline.transforms, transforms_offset, transforms); - // encoder.writeBuffer(pipeline.colors, colors_offset, colors); + transforms_offset += transforms.len; + // colors_offset += colors.len; + pipeline.num_texts += @intCast(transforms.len); - transforms_offset += transforms.len; - // colors_offset += colors.len; - pipeline.num_texts += @intCast(transforms.len); + // Render texts + // TODO: this is very expensive and shouldn't be done here, should be done only on detected + // text change. + const px_density = 2.0; + const segment_lists = archetype.slice(.mach_gfx_text, .text); + const style_lists = archetype.slice(.mach_gfx_text, .style); + for (segment_lists, style_lists) |segments, styles| { + var origin_x: f32 = 0.0; + var origin_y: f32 = 0.0; - // Render texts - // TODO: this is very expensive and shouldn't be done here, should be done only on detected - // text change. - const px_density = 2.0; - const segment_lists = archetype.slice(.mach_gfx_text, .text); - const style_lists = archetype.slice(.mach_gfx_text, .style); - for (segment_lists, style_lists) |segments, styles| { - var origin_x: f32 = 0.0; - var origin_y: f32 = 0.0; + for (segments, styles) |segment, style| { + // Load a font + const font_name = engine.entities.getComponent(style, .mach_gfx_text, .font_name).?; + _ = font_name; // TODO: actually use font name + const font_bytes = @import("font-assets").fira_sans_regular_ttf; + var font = try gfx.Font.initBytes(font_bytes); + defer font.deinit(engine.allocator); - for (segments, styles) |segment, style| { - // Load a font - const font_name = engine.entities.getComponent(style, .mach_gfx_text, .font_name).?; - _ = font_name; // TODO: actually use font name - const font_bytes = @import("font-assets").fira_sans_regular_ttf; - var font = try gfx.Font.initBytes(font_bytes); - defer font.deinit(engine.allocator); + const font_size = engine.entities.getComponent(style, .mach_gfx_text, .font_size).?; + const font_weight = engine.entities.getComponent(style, .mach_gfx_text, .font_weight); + const italic = engine.entities.getComponent(style, .mach_gfx_text, .italic); + const color = engine.entities.getComponent(style, .mach_gfx_text, .color); + // TODO: actually apply these + _ = font_weight; + _ = italic; + _ = color; - const font_size = engine.entities.getComponent(style, .mach_gfx_text, .font_size).?; - const font_weight = engine.entities.getComponent(style, .mach_gfx_text, .font_weight); - const italic = engine.entities.getComponent(style, .mach_gfx_text, .italic); - const color = engine.entities.getComponent(style, .mach_gfx_text, .color); - // TODO: actually apply these - _ = font_weight; - _ = italic; - _ = color; + // Create a text shaper + var run = try gfx.TextRun.init(); + run.font_size_px = font_size; + run.px_density = 2; // TODO - // Create a text shaper - var run = try gfx.TextRun.init(); - run.font_size_px = font_size; - run.px_density = 2; // TODO + defer run.deinit(); - defer run.deinit(); + run.addText(segment); + try font.shape(&run); - run.addText(segment); - try font.shape(&run); - - while (run.next()) |glyph| { - const codepoint = segment[glyph.cluster]; - // TODO: use flags(?) to detect newline, or at least something more reliable? - if (codepoint != '\n') { - const region = try pipeline.regions.getOrPut(engine.allocator, .{ - .index = glyph.glyph_index, - .size = @bitCast(font_size), + while (run.next()) |glyph| { + const codepoint = segment[glyph.cluster]; + // TODO: use flags(?) to detect newline, or at least something more reliable? + if (codepoint != '\n') { + const region = try pipeline.regions.getOrPut(engine.allocator, .{ + .index = glyph.glyph_index, + .size = @bitCast(font_size), + }); + if (!region.found_existing) { + const rendered_glyph = try font.render(engine.allocator, glyph.glyph_index, .{ + .font_size_px = run.font_size_px, }); - if (!region.found_existing) { - const rendered_glyph = try font.render(engine.allocator, glyph.glyph_index, .{ - .font_size_px = run.font_size_px, - }); - if (rendered_glyph.bitmap) |bitmap| { - var glyph_atlas_region = try pipeline.texture_atlas.reserve(engine.allocator, rendered_glyph.width, rendered_glyph.height); - pipeline.texture_atlas.set(glyph_atlas_region, @as([*]const u8, @ptrCast(bitmap.ptr))[0 .. bitmap.len * 4]); - texture_update = true; + if (rendered_glyph.bitmap) |bitmap| { + var glyph_atlas_region = try pipeline.texture_atlas.reserve(engine.allocator, rendered_glyph.width, rendered_glyph.height); + pipeline.texture_atlas.set(glyph_atlas_region, @as([*]const u8, @ptrCast(bitmap.ptr))[0 .. bitmap.len * 4]); + texture_update = true; - // Exclude the 1px blank space margin when describing the region of the texture - // that actually represents the glyph. - const margin = 1; - glyph_atlas_region.x += margin; - glyph_atlas_region.y += margin; - glyph_atlas_region.width -= margin * 2; - glyph_atlas_region.height -= margin * 2; - region.value_ptr.* = glyph_atlas_region; - } else { - // whitespace - region.value_ptr.* = gfx.Atlas.Region{ - .width = 0, - .height = 0, - .x = 0, - .y = 0, - }; - } + // Exclude the 1px blank space margin when describing the region of the texture + // that actually represents the glyph. + const margin = 1; + glyph_atlas_region.x += margin; + glyph_atlas_region.y += margin; + glyph_atlas_region.width -= margin * 2; + glyph_atlas_region.height -= margin * 2; + region.value_ptr.* = glyph_atlas_region; + } else { + // whitespace + region.value_ptr.* = gfx.Atlas.Region{ + .width = 0, + .height = 0, + .x = 0, + .y = 0, + }; } - - const r = region.value_ptr.*; - const size = vec2(@floatFromInt(r.width), @floatFromInt(r.height)); - try glyphs.append(engine.allocator, .{ - .pos = vec2( - origin_x + glyph.offset.x(), - origin_y - (size.y() - glyph.offset.y()), - ).divScalar(px_density), - .size = size.divScalar(px_density), - .text_index = 0, - .uv_pos = vec2(@floatFromInt(r.x), @floatFromInt(r.y)), - }); - pipeline.num_glyphs += 1; } - if (codepoint == '\n') { - origin_x = 0; - origin_y -= font_size; - } else { - origin_x += glyph.advance.x(); - } + const r = region.value_ptr.*; + const size = vec2(@floatFromInt(r.width), @floatFromInt(r.height)); + try glyphs.append(engine.allocator, .{ + .pos = vec2( + origin_x + glyph.offset.x(), + origin_y - (size.y() - glyph.offset.y()), + ).divScalar(px_density), + .size = size.divScalar(px_density), + .text_index = 0, + .uv_pos = vec2(@floatFromInt(r.x), @floatFromInt(r.y)), + }); + pipeline.num_glyphs += 1; + } + + if (codepoint == '\n') { + origin_x = 0; + origin_y -= font_size; + } else { + origin_x += glyph.advance.x(); } } } } - - // TODO: could writeBuffer check for zero? - if (glyphs.items.len > 0) encoder.writeBuffer(pipeline.glyphs, 0, glyphs.items); - defer glyphs.deinit(engine.allocator); - if (texture_update) { - // rgba32_pixels - // TODO: use proper texture dimensions here - const img_size = gpu.Extent3D{ .width = 1024, .height = 1024 }; - const data_layout = gpu.Texture.DataLayout{ - .bytes_per_row = @as(u32, @intCast(img_size.width * 4)), - .rows_per_image = @as(u32, @intCast(img_size.height)), - }; - engine.state.queue.writeTexture( - &.{ .texture = pipeline.texture }, - &data_layout, - &img_size, - pipeline.texture_atlas.data, - ); - } - - var command = encoder.finish(null); - defer command.release(); - - engine.state.queue.submit(&[_]*gpu.CommandBuffer{command}); } - pub fn preRender( - engine: *Engine.Mod, - text_mod: *Mod, - pipeline_id: u32, - ) !void { - const pipeline = text_mod.state.pipelines.get(pipeline_id).?; - - // Update uniform buffer - const proj = Mat4x4.projection2D(.{ - .left = -@as(f32, @floatFromInt(core.size().width)) / 2, - .right = @as(f32, @floatFromInt(core.size().width)) / 2, - .bottom = -@as(f32, @floatFromInt(core.size().height)) / 2, - .top = @as(f32, @floatFromInt(core.size().height)) / 2, - .near = -0.1, - .far = 100000, - }); - const uniforms = Uniforms{ - .view_projection = proj, - // TODO: dimensions of other textures, number of textures present - .texture_size = vec2( - @as(f32, @floatFromInt(pipeline.texture.getWidth())), - @as(f32, @floatFromInt(pipeline.texture.getHeight())), - ), + // TODO: could writeBuffer check for zero? + if (glyphs.items.len > 0) encoder.writeBuffer(pipeline.glyphs, 0, glyphs.items); + defer glyphs.deinit(engine.allocator); + if (texture_update) { + // rgba32_pixels + // TODO: use proper texture dimensions here + const img_size = gpu.Extent3D{ .width = 1024, .height = 1024 }; + const data_layout = gpu.Texture.DataLayout{ + .bytes_per_row = @as(u32, @intCast(img_size.width * 4)), + .rows_per_image = @as(u32, @intCast(img_size.height)), }; - - engine.state.encoder.writeBuffer(pipeline.uniforms, 0, &[_]Uniforms{uniforms}); + engine.state.queue.writeTexture( + &.{ .texture = pipeline.texture }, + &data_layout, + &img_size, + pipeline.texture_atlas.data, + ); } - pub fn render( - engine: *Engine.Mod, - text_mod: *Mod, - pipeline_id: u32, - ) !void { - const pipeline = text_mod.state.pipelines.get(pipeline_id).?; + var command = encoder.finish(null); + defer command.release(); - // Draw the text batch - const pass = engine.state.pass; - const total_vertices = pipeline.num_glyphs * 6; - pass.setPipeline(pipeline.render); - // TODO: remove dynamic offsets? - pass.setBindGroup(0, pipeline.bind_group, &.{}); - pass.draw(total_vertices, 1, 0, 0); - } -}; + engine.state.queue.submit(&[_]*gpu.CommandBuffer{command}); +} + +fn preRender( + engine: *Engine.Mod, + text_mod: *Mod, + pipeline_id: u32, +) !void { + const pipeline = text_mod.state.pipelines.get(pipeline_id).?; + + // Update uniform buffer + const proj = Mat4x4.projection2D(.{ + .left = -@as(f32, @floatFromInt(core.size().width)) / 2, + .right = @as(f32, @floatFromInt(core.size().width)) / 2, + .bottom = -@as(f32, @floatFromInt(core.size().height)) / 2, + .top = @as(f32, @floatFromInt(core.size().height)) / 2, + .near = -0.1, + .far = 100000, + }); + const uniforms = Uniforms{ + .view_projection = proj, + // TODO: dimensions of other textures, number of textures present + .texture_size = vec2( + @as(f32, @floatFromInt(pipeline.texture.getWidth())), + @as(f32, @floatFromInt(pipeline.texture.getHeight())), + ), + }; + + engine.state.encoder.writeBuffer(pipeline.uniforms, 0, &[_]Uniforms{uniforms}); +} + +fn render( + engine: *Engine.Mod, + text_mod: *Mod, + pipeline_id: u32, +) !void { + const pipeline = text_mod.state.pipelines.get(pipeline_id).?; + + // Draw the text batch + const pass = engine.state.pass; + const total_vertices = pipeline.num_glyphs * 6; + pass.setPipeline(pipeline.render); + // TODO: remove dynamic offsets? + pass.setBindGroup(0, pipeline.bind_group, &.{}); + pass.draw(total_vertices, 1, 0, 0); +} diff --git a/src/module.zig b/src/module.zig index 235d6b18..d649ae80 100644 --- a/src/module.zig +++ b/src/module.zig @@ -2,17 +2,21 @@ const builtin = @import("builtin"); const std = @import("std"); const testing = @import("testing.zig"); -/// Verifies that T matches the basic layout of a Mach module -pub fn Module(comptime T: type) type { - if (@typeInfo(T) != .Struct) @compileError("Module must be a struct type. Found:" ++ @typeName(T)); - if (!@hasDecl(T, "name")) @compileError("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)); +/// Verifies that M matches the basic layout of a Mach module +pub fn Module(comptime M: type) type { + if (@typeInfo(M) != .Struct) @compileError("mach: expected module struct, found: " ++ @typeName(M)); + if (!@hasDecl(M, "name")) @compileError("mach: module must have `pub const name = .foobar;`"); + 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 - if (@hasDecl(T, "components")) { - if (@typeInfo(T.components) != .Struct) @compileError("Module.components must be `pub const components = struct { ... };`, found type:" ++ @typeName(T.components)); + if (@hasDecl(M, "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 @@ -65,49 +69,63 @@ pub fn Modules(comptime mods: anytype) type { /// Returns an args tuple representing the standard, uninjected, arguments which the given /// 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| { + _ = Module(M); // Validate the module if (M.name != module_name) continue; - if (!@hasDecl(M, "local")) @compileError("Module " ++ @tagName(module_name) ++ " has no `pub const local = struct { ... };` event handlers"); - if (!@hasDecl(M.local, @tagName(event_name))) @compileError("Module " ++ @tagName(module_name) ++ ".local has no event handler named: " ++ @tagName(event_name)); - const handler = @field(M.local, @tagName(event_name)); - switch (@typeInfo(@TypeOf(handler))) { + inline for (M.events) |event| { + const Ev = @TypeOf(event); + const name_tag = if (@hasField(Ev, "local")) event.local else continue; + 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 // segfault. The only difference is that TupleHACk does not produce a real tuple, // `@Type(.{.Struct = .{ .is_tuple = false }})` instead of `.is_tuple = true`. - .Fn => return UninjectedArgsTuple(TupleHACK, @TypeOf(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"), + return UninjectedArgsTuple(TupleHACK, Handler); } + @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 /// global event handler requires. - fn Args(event_name: EventName(mods)) type { + fn Args(event_name: GlobalEventEnum(mods)) type { inline for (modules) |M| { - // TODO: enforce any defined event handlers of the same name have the same argument types - if (@hasDecl(M, @tagName(event_name))) { - const Handler = switch (@typeInfo(@TypeOf(@field(M, @tagName(event_name))))) { - .Fn => @TypeOf(@field(M, @tagName(event_name))), - .Type => switch (@typeInfo(@field(M, @tagName(event_name)))) { - .Fn => @field(M, @tagName(event_name)), - else => continue, + _ = Module(M); // Validate the module + + inline for (M.events) |event| { + const Ev = @TypeOf(event); + const name_tag = if (@hasField(Ev, "global")) event.global else continue; + if (name_tag != event_name) 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); } } - @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 pub fn send( m: *@This(), // 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), ) void { // TODO: comptime safety/debugging @@ -119,7 +137,7 @@ pub fn Modules(comptime mods: anytype) type { 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? comptime module_name: ModuleName(mods), - comptime event_name: EventName(mods), + comptime event_name: LocalEventEnum(mods), args: LocalArgs(module_name, event_name), ) void { // 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); } else { // 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 - 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) { - inline else => |name| { + inline else => |ev_name| { inline for (modules) |M| { - if (@hasDecl(M, @tagName(name))) { - switch (@typeInfo(@TypeOf(@field(M, @tagName(name))))) { - .Fn => { - const handler = @field(M, @tagName(name)); - try callHandler(handler, args, injectable); + _ = Module(M); // Validate the module + inline for (M.events) |event| { + const Ev = @TypeOf(event); + const name_tag = if (@hasField(Ev, "global")) event.global else continue; + 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 - 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 switch (module_name) { inline else => |mod_name| { switch (event_name) { inline else => |ev_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. - if (@hasDecl(M, "local") and @hasDecl(M.local, @tagName(ev_name))) { - const handler = @field(M.local, @tagName(ev_name)); - switch (@typeInfo(@TypeOf(handler))) { - .Fn => { - try callHandler(handler, args, injectable); + _ = Module(M); // Validate the module + + inline for (M.events) |event| { + const Ev = @TypeOf(event); + const name_tag = if (@hasField(Ev, "local")) event.local else continue; + 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); } -/// enum describing every possible comptime-known global event name. -fn GlobalEvent(comptime mods: anytype) type { +/// enum describing every possible comptime-known local event name +fn LocalEventEnum(comptime mods: anytype) type { var enum_fields: []const std.builtin.Type.EnumField = &[0]std.builtin.Type.EnumField{}; var i: u32 = 0; for (mods) |M| { - // Global event handlers - for (@typeInfo(M).Struct.decls) |decl| { - const is_event_handler = switch (@typeInfo(@TypeOf(@field(M, decl.name)))) { - .Fn => true, - .Type => switch (@typeInfo(@field(M, decl.name))) { - .Fn => true, - else => false, - }, - else => false, + _ = Module(M); // Validate the module + inline for (M.events) |event| { + const Event = @TypeOf(event); + const name_tag = if (@hasField(Event, "local")) event.local else continue; + + const exists_already = blk: { + for (enum_fields) |existing| if (std.mem.eql(u8, existing.name, @tagName(name_tag))) break :blk true; + break :blk 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; - } + if (!exists_already) { + enum_fields = enum_fields ++ [_]std.builtin.Type.EnumField{.{ .name = @tagName(name_tag), .value = i }}; + i += 1; } } } - return @Type(.{ .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, .decls = &[_]std.builtin.Type.Declaration{}, .is_exhaustive = true, @@ -374,55 +397,29 @@ fn GlobalEvent(comptime mods: anytype) type { }); } -/// enum describing every possible comptime-known event name -fn EventName(comptime mods: anytype) type { +/// enum describing every possible comptime-known global event name +fn GlobalEventEnum(comptime mods: anytype) type { var enum_fields: []const std.builtin.Type.EnumField = &[0]std.builtin.Type.EnumField{}; var i: u32 = 0; for (mods) |M| { - // Global event handlers - for (@typeInfo(M).Struct.decls) |decl| { - const is_event_handler = switch (@typeInfo(@TypeOf(@field(M, decl.name)))) { - .Fn => true, - .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; - } - } - } + _ = Module(M); // Validate the module + inline for (M.events) |event| { + const Event = @TypeOf(event); + const name_tag = if (@hasField(Event, "global")) event.global else continue; - // Local event handlers - if (@hasDecl(M, "local")) { - for (@typeInfo(M.local).Struct.decls) |decl| { - 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) { - enum_fields = enum_fields ++ [_]std.builtin.Type.EnumField{.{ .name = decl.name, .value = i }}; - i += 1; - } - }, - else => {}, - } + const exists_already = blk: { + for (enum_fields) |existing| if (std.mem.eql(u8, existing.name, @tagName(name_tag))) break :blk true; + break :blk false; + }; + if (!exists_already) { + enum_fields = enum_fields ++ [_]std.builtin.Type.EnumField{.{ .name = @tagName(name_tag), .value = i }}; + i += 1; } } } return @Type(.{ .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, .decls = &[_]std.builtin.Type.Declaration{}, .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 { testing.refAllDeclsRecursive(@This()); } @@ -486,7 +525,11 @@ test Module { 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 fn tick() !void {} + pub const events = .{ + .{ .global = .tick, .handler = tick }, + }; + + fn tick() !void {} }); const Renderer = Module(struct { pub const name = .engine_renderer; + pub const events = .{ + .{ .global = .tick, .handler = tick }, + }; /// Renderer module components pub const components = struct {}; - pub fn tick() !void {} + fn tick() !void {} }); const Sprite2D = Module(struct { pub const name = .engine_sprite2d; + pub const events = .{}; }); var modules: Modules(.{ @@ -532,39 +583,48 @@ test Modules { testing.refAllDeclsRecursive(Sprite2D); } -test EventName { +test "event name" { const Physics = Module(struct { pub const name = .engine_physics; pub const components = struct {}; - - pub fn foo() !void {} - pub fn bar() !void {} - - pub const local = struct { - pub fn baz() !void {} - pub fn bam() !void {} + pub const events = .{ + .{ .global = .foo, .handler = foo }, + .{ .global = .bar, .handler = bar }, + .{ .local = .baz, .handler = baz }, + .{ .local = .bam, .handler = bam }, }; + + fn foo() !void {} + fn bar() !void {} + fn baz() !void {} + fn bam() !void {} }); const Renderer = Module(struct { pub const name = .engine_renderer; 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; - pub const barUnused = fn (i32, f32) void; - - 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 + fn tick() !void {} + fn foo() !void {} // same .foo name as .engine_physics.foo + fn bar() !void {} // same .bar name as .engine_physics.bar }); const Sprite2D = Module(struct { pub const name = .engine_sprite2d; - - pub fn tick() void {} // same .tick as .engine_renderer.tick - pub const local = struct { - pub fn foobar() void {} + pub const events = .{ + .{ .global = .tick, .handler = tick }, + .{ .global = .foobar, .handler = foobar }, }; + + fn tick() void {} // same .tick as .engine_renderer.tick + fn foobar() void {} }); const Mods = Modules(.{ @@ -572,38 +632,36 @@ test EventName { Renderer, Sprite2D, }); - const info = @typeInfo(EventName(Mods.modules)).Enum; - try testing.expect(type, u3).eql(info.tag_type); - try testing.expect(usize, 8).eql(info.fields.len); - try testing.expect([]const u8, "foo").eql(info.fields[0].name); - try testing.expect([]const u8, "bar").eql(info.fields[1].name); - try testing.expect([]const u8, "baz").eql(info.fields[2].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 locals = @typeInfo(LocalEventEnum(Mods.modules)).Enum; + try testing.expect(type, u1).eql(locals.tag_type); + try testing.expect(usize, 2).eql(locals.fields.len); + try testing.expect([]const u8, "baz").eql(locals.fields[0].name); + try testing.expect([]const u8, "bam").eql(locals.fields[1].name); - const global_info = @typeInfo(GlobalEvent(Mods.modules)).Enum; - try testing.expect(type, u3).eql(global_info.tag_type); - try testing.expect(usize, 5).eql(global_info.fields.len); - try testing.expect([]const u8, "foo").eql(global_info.fields[0].name); - try testing.expect([]const u8, "bar").eql(global_info.fields[1].name); - try testing.expect([]const u8, "fooUnused").eql(global_info.fields[2].name); - try testing.expect([]const u8, "barUnused").eql(global_info.fields[3].name); - try testing.expect([]const u8, "tick").eql(global_info.fields[4].name); + const globals = @typeInfo(GlobalEventEnum(Mods.modules)).Enum; + try testing.expect(type, u3).eql(globals.tag_type); + try testing.expect(usize, 6).eql(globals.fields.len); + try testing.expect([]const u8, "foo").eql(globals.fields[0].name); + try testing.expect([]const u8, "bar").eql(globals.fields[1].name); + try testing.expect([]const u8, "foo_unused").eql(globals.fields[2].name); + try testing.expect([]const u8, "bar_unused").eql(globals.fields[3].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 { const Physics = Module(struct { pub const name = .engine_physics; + pub const events = .{}; }); const Renderer = Module(struct { pub const name = .engine_renderer; + pub const events = .{}; }); const Sprite2D = Module(struct { pub const name = .engine_sprite2d; + pub const events = .{}; }); const Mods = Modules(.{ Physics, @@ -742,34 +800,39 @@ test "event name calling" { const Physics = Module(struct { pub const name = .engine_physics; 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; } - pub const local = struct { - pub fn update() void { - global.physics_updates += 1; - } + fn update() void { + global.physics_updates += 1; + } - pub fn calc() void { - global.physics_calc += 1; - } - }; + fn calc() void { + global.physics_calc += 1; + } }); const Renderer = Module(struct { pub const name = .engine_renderer; 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; } - pub const local = struct { - pub fn update() void { - global.renderer_updates += 1; - } - }; + fn update() void { + global.renderer_updates += 1; + } }); var modules: Modules(.{ @@ -779,52 +842,46 @@ test "event name calling" { try modules.init(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); - // 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); defer testing.allocator.destroy(alloc); - const E = EventName(@TypeOf(modules).modules); - alloc.* = @intFromEnum(@as(E, .tick)); + const GE = GlobalEventEnum(@TypeOf(modules).modules); + const LE = LocalEventEnum(@TypeOf(modules).modules); + alloc.* = @intFromEnum(@as(GE, .tick)); - var event_name = @as(E, @enumFromInt(alloc.*)); - try @TypeOf(modules).call(event_name, &.{}, .{}); + const global_event_name = @as(GE, @enumFromInt(alloc.*)); + try @TypeOf(modules).callGlobal(global_event_name, &.{}, .{}); 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. const m_alloc = try testing.allocator.create(u3); defer testing.allocator.destroy(m_alloc); const M = ModuleName(@TypeOf(modules).modules); 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.*)); - try @TypeOf(modules).callLocal(module_name, event_name, &.{}, .{}); - try @TypeOf(modules).callLocal(module_name, event_name, &.{}, .{}); + var local_event_name = @as(LE, @enumFromInt(alloc.*)); + 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, 0).eql(global.physics_updates); try testing.expect(usize, 2).eql(global.renderer_updates); m_alloc.* = @intFromEnum(@as(M, .engine_physics)); - alloc.* = @intFromEnum(@as(E, .update)); + alloc.* = @intFromEnum(@as(LE, .update)); module_name = @as(M, @enumFromInt(m_alloc.*)); - event_name = @as(E, @enumFromInt(alloc.*)); - try @TypeOf(modules).callLocal(module_name, event_name, &.{}, .{}); + local_event_name = @as(LE, @enumFromInt(alloc.*)); + try @TypeOf(modules).callLocal(module_name, local_event_name, &.{}, .{}); try testing.expect(usize, 1).eql(global.physics_updates); m_alloc.* = @intFromEnum(@as(M, .engine_physics)); - alloc.* = @intFromEnum(@as(E, .calc)); + alloc.* = @intFromEnum(@as(LE, .calc)); module_name = @as(M, @enumFromInt(m_alloc.*)); - event_name = @as(E, @enumFromInt(alloc.*)); - try @TypeOf(modules).callLocal(module_name, event_name, &.{}, .{}); + local_event_name = @as(LE, @enumFromInt(alloc.*)); + try @TypeOf(modules).callLocal(module_name, local_event_name, &.{}, .{}); 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_updates); @@ -846,48 +903,57 @@ test "dispatch" { }{}; const Minimal = Module(struct { pub const name = .engine_minimal; + pub const events = .{}; }); const Physics = Module(struct { pub const name = .engine_physics; 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; } - pub const local = struct { - pub fn update() void { - global.physics_updates += 1; - } + fn update() void { + global.physics_updates += 1; + } - pub fn calc() void { - global.physics_calc += 1; - } - }; + fn calc() void { + global.physics_calc += 1; + } }); const Renderer = Module(struct { pub const name = .engine_renderer; 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 fn tick() void { + fn tick() void { global.ticks += 1; } - pub const local = struct { - pub fn update() void { - global.renderer_updates += 1; - } + fn update() void { + global.renderer_updates += 1; + } - pub fn basicArgs(a: u32, b: u32) void { - global.basic_args_sum = a + b; - } + fn basicArgs(a: u32, b: u32) void { + global.basic_args_sum = a + b; + } - pub fn injectedArgs(foo_ptr: *@TypeOf(foo), a: u32, b: u32) void { - foo_ptr.*.injected_args_sum = a + b; - } - }; + fn injectedArgs(foo_ptr: *@TypeOf(foo), a: u32, b: u32) void { + foo_ptr.*.injected_args_sum = a + b; + } }); var modules: Modules(.{ @@ -898,7 +964,8 @@ test "dispatch" { try modules.init(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); // Global events @@ -912,14 +979,14 @@ test "dispatch" { try modules.dispatch(.{&foo}); try testing.expect(usize, 2).eql(global.ticks); // 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 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;` // within a module, which allows pre-declaring that `fooBar` is a valid global event, and enables // its arguments to be inferred still like this: - modules.send(.frameDone, .{1337}); + modules.send(.frame_done, .{1337}); // Local events modules.sendToModule(.engine_renderer, .update, .{}); @@ -928,7 +995,7 @@ test "dispatch" { modules.sendToModule(.engine_physics, .update, .{}); modules.sendToModuleDynamic( @intFromEnum(@as(M, .engine_physics)), - @intFromEnum(@as(E, .calc)), + @intFromEnum(@as(LE, .calc)), .{}, ); try modules.dispatch(.{&foo}); @@ -936,8 +1003,8 @@ test "dispatch" { try testing.expect(usize, 1).eql(global.physics_calc); // 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, .injectedArgs, .{ .@"0" = @as(u32, 1), .@"1" = @as(u32, 2) }); + 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, .injected_args, .{ .@"0" = @as(u32, 1), .@"1" = @as(u32, 2) }); try modules.dispatch(.{&foo}); try testing.expect(usize, 3).eql(global.basic_args_sum); try testing.expect(usize, 3).eql(foo.injected_args_sum);