diff --git a/examples/custom-renderer/Game.zig b/examples/custom-renderer/Game.zig index ec179c18..a5487990 100644 --- a/examples/custom-renderer/Game.zig +++ b/examples/custom-renderer/Game.zig @@ -55,11 +55,11 @@ fn init( try renderer.set(player, .location, vec3(0, 0, 0)); try renderer.set(player, .scale, 1.0); - game.state = .{ + game.init(.{ .timer = try mach.Timer.start(), .spawn_timer = try mach.Timer.start(), .player = player, - }; + }); } // TODO(engine): remove need for returning an error here @@ -70,8 +70,8 @@ fn tick( ) !void { // TODO(engine): event polling should occur in mach.Engine module and get fired as ECS events. var iter = core.pollEvents(); - var direction = game.state.direction; - var spawning = game.state.spawning; + var direction = game.state().direction; + var spawning = game.state().spawning; while (iter.next()) |event| { switch (event) { .key_press => |ev| { @@ -98,14 +98,14 @@ fn tick( else => {}, } } - game.state.direction = direction; - game.state.spawning = spawning; + game.state().direction = direction; + game.state().spawning = spawning; - var player_pos = renderer.get(game.state.player, .location).?; - if (spawning and game.state.spawn_timer.read() > 1.0 / 60.0) { + var player_pos = renderer.get(game.state().player, .location).?; + if (spawning and game.state().spawn_timer.read() > 1.0 / 60.0) { for (0..10) |_| { // Spawn a new follower entity - _ = game.state.spawn_timer.lap(); + _ = game.state().spawn_timer.lap(); const new_entity = try engine.newEntity(); try game.set(new_entity, .follower, {}); try renderer.set(new_entity, .location, player_pos); @@ -114,7 +114,7 @@ fn tick( } // Multiply by delta_time to ensure that movement is the same speed regardless of the frame rate. - const delta_time = game.state.timer.lap(); + const delta_time = game.state().timer.lap(); // Move following entities closer to us. var archetypes_iter = engine.entities.query(.{ .all = &.{ @@ -165,5 +165,5 @@ fn tick( const speed = 1.0; player_pos.v[0] += direction.x() * speed * delta_time; player_pos.v[1] += direction.y() * speed * delta_time; - try renderer.set(game.state.player, .location, player_pos); + try renderer.set(game.state().player, .location, player_pos); } diff --git a/examples/custom-renderer/Renderer.zig b/examples/custom-renderer/Renderer.zig index bd3662b2..36759ed4 100644 --- a/examples/custom-renderer/Renderer.zig +++ b/examples/custom-renderer/Renderer.zig @@ -42,7 +42,7 @@ fn init( engine: *mach.Engine.Mod, renderer: *Mod, ) !void { - const device = engine.state.device; + const device = engine.state().device; const shader_module = device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl")); // Fragment state @@ -94,29 +94,29 @@ fn init( }, }; - renderer.state = .{ + renderer.init(.{ .pipeline = device.createRenderPipeline(&pipeline_descriptor), .queue = device.getQueue(), .bind_groups = bind_groups, .uniform_buffer = uniform_buffer, - }; + }); shader_module.release(); } fn deinit( renderer: *Mod, ) !void { - renderer.state.pipeline.release(); - renderer.state.queue.release(); - for (renderer.state.bind_groups) |bind_group| bind_group.release(); - renderer.state.uniform_buffer.release(); + renderer.state().pipeline.release(); + renderer.state().queue.release(); + for (renderer.state().bind_groups) |bind_group| bind_group.release(); + renderer.state().uniform_buffer.release(); } fn tick( engine: *mach.Engine.Mod, renderer: *Mod, ) !void { - const device = engine.state.device; + const device = engine.state().device; // Begin our render pass const back_buffer_view = core.swap_chain.getCurrentTextureView().?; @@ -148,14 +148,14 @@ fn tick( .offset = location.v, .scale = scale, }; - encoder.writeBuffer(renderer.state.uniform_buffer, uniform_offset * num_entities, &[_]UniformBufferObject{ubo}); + encoder.writeBuffer(renderer.state().uniform_buffer, uniform_offset * num_entities, &[_]UniformBufferObject{ubo}); num_entities += 1; } } const pass = encoder.beginRenderPass(&render_pass_info); - for (renderer.state.bind_groups[0..num_entities]) |bind_group| { - pass.setPipeline(renderer.state.pipeline); + for (renderer.state().bind_groups[0..num_entities]) |bind_group| { + pass.setPipeline(renderer.state().pipeline); pass.setBindGroup(0, bind_group, &.{0}); pass.draw(3, 1, 0, 0); } @@ -165,7 +165,7 @@ fn tick( var command = encoder.finish(null); encoder.release(); - renderer.state.queue.submit(&[_]*gpu.CommandBuffer{command}); + renderer.state().queue.submit(&[_]*gpu.CommandBuffer{command}); command.release(); core.swap_chain.present(); back_buffer_view.release(); diff --git a/examples/glyphs/Game.zig b/examples/glyphs/Game.zig index 857f8718..03aaa9b1 100644 --- a/examples/glyphs/Game.zig +++ b/examples/glyphs/Game.zig @@ -60,7 +60,7 @@ fn init( core.setTitle("gfx.Sprite example"); // Tell sprite_mod to use the texture - const texture = text_mod.state.texture; + const texture = text_mod.state().texture; sprite_mod.send(.init_pipeline, .{Sprite.PipelineOptions{ .pipeline = @intFromEnum(Pipeline.text), .texture = texture, @@ -71,7 +71,7 @@ fn init( // type than the `.physics2d` module's `.location` component if you desire. engine.dispatchNoError(); // TODO: no dispatch in user code - const r = text_mod.state.regions.get('?').?; + const r = text_mod.state().regions.get('?').?; const player = try engine.newEntity(); try sprite_mod.set(player, .transform, Mat4x4.translate(vec3(-0.02, 0, 0))); try sprite_mod.set(player, .size, vec2(@floatFromInt(r.width), @floatFromInt(r.height))); @@ -79,7 +79,7 @@ fn init( try sprite_mod.set(player, .pipeline, @intFromEnum(Pipeline.text)); sprite_mod.send(.updated, .{@intFromEnum(Pipeline.text)}); - game.state = .{ + game.init(.{ .timer = try mach.Timer.start(), .spawn_timer = try mach.Timer.start(), .player = player, @@ -88,7 +88,7 @@ fn init( .sprites = 0, .rand = std.rand.DefaultPrng.init(1337), .time = 0, - }; + }); } fn tick( @@ -99,8 +99,8 @@ fn tick( ) !void { // TODO(engine): event polling should occur in mach.Engine module and get fired as ECS events. var iter = core.pollEvents(); - var direction = game.state.direction; - var spawning = game.state.spawning; + var direction = game.state().direction; + var spawning = game.state().spawning; while (iter.next()) |event| { switch (event) { .key_press => |ev| { @@ -127,33 +127,33 @@ fn tick( else => {}, } } - game.state.direction = direction; - game.state.spawning = spawning; + game.state().direction = direction; + game.state().spawning = spawning; - var player_transform = sprite_mod.get(game.state.player, .transform).?; + var player_transform = sprite_mod.get(game.state().player, .transform).?; var player_pos = player_transform.translation(); - if (!spawning and game.state.spawn_timer.read() > 1.0 / 60.0) { + if (!spawning and game.state().spawn_timer.read() > 1.0 / 60.0) { // Spawn new entities - _ = game.state.spawn_timer.lap(); + _ = game.state().spawn_timer.lap(); for (0..50) |_| { var new_pos = player_pos; - new_pos.v[0] += game.state.rand.random().floatNorm(f32) * 25; - new_pos.v[1] += game.state.rand.random().floatNorm(f32) * 25; + new_pos.v[0] += game.state().rand.random().floatNorm(f32) * 25; + new_pos.v[1] += game.state().rand.random().floatNorm(f32) * 25; - const rand_index = game.state.rand.random().intRangeAtMost(usize, 0, text_mod.state.regions.count() - 1); - const r = text_mod.state.regions.entries.get(rand_index).value; + const rand_index = game.state().rand.random().intRangeAtMost(usize, 0, text_mod.state().regions.count() - 1); + const r = text_mod.state().regions.entries.get(rand_index).value; const new_entity = try engine.newEntity(); try sprite_mod.set(new_entity, .transform, Mat4x4.translate(new_pos).mul(&Mat4x4.scaleScalar(0.3))); try sprite_mod.set(new_entity, .size, vec2(@floatFromInt(r.width), @floatFromInt(r.height))); try sprite_mod.set(new_entity, .uv_transform, Mat3x3.translate(vec2(@floatFromInt(r.x), @floatFromInt(r.y)))); try sprite_mod.set(new_entity, .pipeline, @intFromEnum(Pipeline.text)); - game.state.sprites += 1; + game.state().sprites += 1; } } // Multiply by delta_time to ensure that movement is the same speed regardless of the frame rate. - const delta_time = game.state.timer.lap(); + const delta_time = game.state().timer.lap(); // Animate entities var archetypes_iter = engine.entities.query(.{ .all = &.{ @@ -166,15 +166,15 @@ fn tick( var location = old_transform.translation(); if (location.x() < -@as(f32, @floatFromInt(core.size().width)) / 1.5 or location.x() > @as(f32, @floatFromInt(core.size().width)) / 1.5 or location.y() < -@as(f32, @floatFromInt(core.size().height)) / 1.5 or location.y() > @as(f32, @floatFromInt(core.size().height)) / 1.5) { try engine.entities.remove(id); - game.state.sprites -= 1; + game.state().sprites -= 1; continue; } var transform = Mat4x4.ident; transform = transform.mul(&Mat4x4.scale(Vec3.splat(1.0 + (0.2 * delta_time)))); transform = transform.mul(&Mat4x4.translate(location)); - transform = transform.mul(&Mat4x4.rotateZ(2 * math.pi * game.state.time)); - transform = transform.mul(&Mat4x4.scale(Vec3.splat(@max(math.cos(game.state.time / 2.0), 0.2)))); + transform = transform.mul(&Mat4x4.rotateZ(2 * math.pi * game.state().time)); + transform = transform.mul(&Mat4x4.scale(Vec3.splat(@max(math.cos(game.state().time / 2.0), 0.2)))); // TODO: .set() API is substantially slower due to internals // try sprite_mod.set(id, .transform, transform); @@ -190,7 +190,7 @@ fn tick( player_transform = Mat4x4.translate(player_pos).mul( &Mat4x4.scale(Vec3.splat(1.0)), ); - try sprite_mod.set(game.state.player, .transform, player_transform); + try sprite_mod.set(game.state().player, .transform, player_transform); sprite_mod.send(.updated, .{@intFromEnum(Pipeline.text)}); @@ -204,11 +204,11 @@ fn tick( engine.send(.present, .{}); // Present the frame // Every second, update the window title with the FPS - if (game.state.fps_timer.read() >= 1.0) { - try core.printTitle("gfx.Sprite example [ FPS: {d} ] [ Sprites: {d} ]", .{ game.state.frame_count, game.state.sprites }); - game.state.fps_timer.reset(); - game.state.frame_count = 0; + if (game.state().fps_timer.read() >= 1.0) { + try core.printTitle("gfx.Sprite example [ FPS: {d} ] [ Sprites: {d} ]", .{ game.state().frame_count, game.state().sprites }); + game.state().fps_timer.reset(); + game.state().frame_count = 0; } - game.state.frame_count += 1; - game.state.time += delta_time; + game.state().frame_count += 1; + game.state().time += delta_time; } diff --git a/examples/glyphs/Text.zig b/examples/glyphs/Text.zig index d44a21ad..df8b3261 100644 --- a/examples/glyphs/Text.zig +++ b/examples/glyphs/Text.zig @@ -19,28 +19,30 @@ pub const local_events = .{ const RegionMap = std.AutoArrayHashMapUnmanaged(u21, mach.gfx.Atlas.Region); +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + texture_atlas: mach.gfx.Atlas, texture: *gpu.Texture, ft: ft.Library, face: ft.Face, regions: RegionMap = .{}, +allocator: std.mem.Allocator, -fn deinit( - engine: *mach.Engine.Mod, - text_mod: *Mod, -) !void { - text_mod.state.texture_atlas.deinit(engine.allocator); - text_mod.state.texture.release(); - text_mod.state.face.deinit(); - text_mod.state.ft.deinit(); - text_mod.state.regions.deinit(engine.allocator); +fn deinit(text_mod: *Mod) !void { + const state = text_mod.state(); + state.texture_atlas.deinit(text_mod.state().allocator); + state.texture.release(); + state.face.deinit(); + state.ft.deinit(); + state.regions.deinit(state.allocator); } fn init( engine: *mach.Engine.Mod, text_mod: *Mod, ) !void { - const device = engine.state.device; + const device = engine.state().device; + const allocator = gpa.allocator(); // rgba32_pixels const img_size = gpu.Extent3D{ .width = 1024, .height = 1024 }; @@ -56,19 +58,22 @@ fn init( }, }); - var s = &text_mod.state; - s.texture = texture; - s.texture_atlas = try mach.gfx.Atlas.init( - engine.allocator, + const texture_atlas = try mach.gfx.Atlas.init( + allocator, img_size.width, .rgba, ); - // TODO: state fields' default values do not work - s.regions = .{}; + const ft_lib = try ft.Library.init(); + const face = try ft_lib.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.init(.{ + .texture_atlas = texture_atlas, + .texture = texture, + .ft = ft_lib, + .face = face, + .allocator = allocator, + }); text_mod.send(.prepare, .{&[_]u21{ '?', '!', 'a', 'b', '#', '@', '%', '$', '&', '^', '*', '+', '=', '<', '>', '/', ':', ';', 'Q', '~' }}); } @@ -78,9 +83,9 @@ fn prepare( text_mod: *Mod, codepoints: []const u21, ) !void { - const device = engine.state.device; + const device = engine.state().device; const queue = device.getQueue(); - var s = &text_mod.state; + var s = text_mod.state(); for (codepoints) |codepoint| { const font_size = 48 * 1; @@ -95,8 +100,8 @@ fn prepare( // 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_data = try s.allocator.alloc([4]u8, (glyph_width + (margin * 2)) * (glyph_height + (margin * 2))); + defer s.allocator.free(glyph_data); const glyph_buffer = glyph_bitmap.buffer().?; for (glyph_data, 0..) |*data, i| { const x = i % (glyph_width + (margin * 2)); @@ -108,7 +113,7 @@ fn prepare( 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)); + var glyph_atlas_region = try s.texture_atlas.reserve(s.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; @@ -116,7 +121,7 @@ fn prepare( glyph_atlas_region.width -= margin * 2; glyph_atlas_region.height -= margin * 2; - try s.regions.put(engine.allocator, codepoint, glyph_atlas_region); + try s.regions.put(s.allocator, codepoint, glyph_atlas_region); _ = metrics; } diff --git a/examples/sprite/Game.zig b/examples/sprite/Game.zig index dcfb4f6c..aeaeb7a8 100644 --- a/examples/sprite/Game.zig +++ b/examples/sprite/Game.zig @@ -15,6 +15,8 @@ const Vec3 = math.Vec3; const Mat3x3 = math.Mat3x3; const Mat4x4 = math.Mat4x4; +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + timer: mach.Timer, player: mach.ecs.EntityID, direction: Vec2 = vec2(0, 0), @@ -25,6 +27,7 @@ frame_count: usize, sprites: usize, rand: std.rand.DefaultPrng, time: f32, +allocator: std.mem.Allocator, const d0 = 0.000001; @@ -68,13 +71,14 @@ fn init( try sprite_mod.set(player, .uv_transform, Mat3x3.translate(vec2(0, 0))); try sprite_mod.set(player, .pipeline, @intFromEnum(Pipeline.default)); + const allocator = gpa.allocator(); sprite_mod.send(.init_pipeline, .{Sprite.PipelineOptions{ .pipeline = @intFromEnum(Pipeline.default), - .texture = try loadTexture(engine), + .texture = try loadTexture(engine, allocator), }}); sprite_mod.send(.updated, .{@intFromEnum(Pipeline.default)}); - game.state = .{ + game.init(.{ .timer = try mach.Timer.start(), .spawn_timer = try mach.Timer.start(), .player = player, @@ -83,7 +87,8 @@ fn init( .sprites = 0, .rand = std.rand.DefaultPrng.init(1337), .time = 0, - }; + .allocator = allocator, + }); } fn tick( @@ -93,8 +98,8 @@ fn tick( ) !void { // TODO(engine): event polling should occur in mach.Engine module and get fired as ECS events. var iter = core.pollEvents(); - var direction = game.state.direction; - var spawning = game.state.spawning; + var direction = game.state().direction; + var spawning = game.state().spawning; while (iter.next()) |event| { switch (event) { .key_press => |ev| { @@ -121,30 +126,30 @@ fn tick( else => {}, } } - game.state.direction = direction; - game.state.spawning = spawning; + game.state().direction = direction; + game.state().spawning = spawning; - var player_transform = sprite_mod.get(game.state.player, .transform).?; + var player_transform = sprite_mod.get(game.state().player, .transform).?; var player_pos = player_transform.translation(); - if (spawning and game.state.spawn_timer.read() > 1.0 / 60.0) { + if (spawning and game.state().spawn_timer.read() > 1.0 / 60.0) { // Spawn new entities - _ = game.state.spawn_timer.lap(); + _ = game.state().spawn_timer.lap(); for (0..100) |_| { var new_pos = player_pos; - new_pos.v[0] += game.state.rand.random().floatNorm(f32) * 25; - new_pos.v[1] += game.state.rand.random().floatNorm(f32) * 25; + new_pos.v[0] += game.state().rand.random().floatNorm(f32) * 25; + new_pos.v[1] += game.state().rand.random().floatNorm(f32) * 25; const new_entity = try engine.newEntity(); try sprite_mod.set(new_entity, .transform, Mat4x4.translate(new_pos).mul(&Mat4x4.scale(Vec3.splat(0.3)))); try sprite_mod.set(new_entity, .size, vec2(32, 32)); try sprite_mod.set(new_entity, .uv_transform, Mat3x3.translate(vec2(0, 0))); try sprite_mod.set(new_entity, .pipeline, @intFromEnum(Pipeline.default)); - game.state.sprites += 1; + game.state().sprites += 1; } } // Multiply by delta_time to ensure that movement is the same speed regardless of the frame rate. - const delta_time = game.state.timer.lap(); + const delta_time = game.state().timer.lap(); // Rotate entities var archetypes_iter = engine.entities.query(.{ .all = &.{ @@ -161,8 +166,8 @@ fn tick( // transform = transform.mul(&Mat4x4.translate(location)); var transform = Mat4x4.ident; transform = transform.mul(&Mat4x4.translate(location)); - transform = transform.mul(&Mat4x4.rotateZ(2 * math.pi * game.state.time)); - transform = transform.mul(&Mat4x4.scaleScalar(@min(math.cos(game.state.time / 2.0), 0.5))); + transform = transform.mul(&Mat4x4.rotateZ(2 * math.pi * game.state().time)); + transform = transform.mul(&Mat4x4.scaleScalar(@min(math.cos(game.state().time / 2.0), 0.5))); // TODO: .set() API is substantially slower due to internals // try sprite_mod.set(id, .transform, transform); @@ -175,7 +180,7 @@ fn tick( const speed = 200.0; player_pos.v[0] += direction.x() * speed * delta_time; player_pos.v[1] += direction.y() * speed * delta_time; - try sprite_mod.set(game.state.player, .transform, Mat4x4.translate(player_pos)); + try sprite_mod.set(game.state().player, .transform, Mat4x4.translate(player_pos)); sprite_mod.send(.updated, .{@intFromEnum(Pipeline.default)}); // Perform pre-render work @@ -188,24 +193,22 @@ fn tick( engine.send(.present, .{}); // Present the frame // Every second, update the window title with the FPS - if (game.state.fps_timer.read() >= 1.0) { - try core.printTitle("gfx.Sprite example [ FPS: {d} ] [ Sprites: {d} ]", .{ game.state.frame_count, game.state.sprites }); - game.state.fps_timer.reset(); - game.state.frame_count = 0; + if (game.state().fps_timer.read() >= 1.0) { + try core.printTitle("gfx.Sprite example [ FPS: {d} ] [ Sprites: {d} ]", .{ game.state().frame_count, game.state().sprites }); + game.state().fps_timer.reset(); + game.state().frame_count = 0; } - game.state.frame_count += 1; - game.state.time += delta_time; + game.state().frame_count += 1; + game.state().time += delta_time; } // TODO: move this helper into gfx module -fn loadTexture( - engine: *mach.Engine.Mod, -) !*gpu.Texture { - const device = engine.state.device; +fn loadTexture(engine: *mach.Engine.Mod, allocator: std.mem.Allocator) !*gpu.Texture { + const device = engine.state().device; const queue = device.getQueue(); // Load the image from memory - var img = try zigimg.Image.fromMemory(engine.allocator, assets.sprites_sheet_png); + var img = try zigimg.Image.fromMemory(allocator, assets.sprites_sheet_png); defer img.deinit(); const img_size = gpu.Extent3D{ .width = @as(u32, @intCast(img.width)), .height = @as(u32, @intCast(img.height)) }; @@ -226,8 +229,8 @@ fn loadTexture( switch (img.pixels) { .rgba32 => |pixels| queue.writeTexture(&.{ .texture = texture }, &data_layout, &img_size, pixels), .rgb24 => |pixels| { - const data = try rgb24ToRgba32(engine.allocator, pixels); - defer data.deinit(engine.allocator); + const data = try rgb24ToRgba32(allocator, pixels); + defer data.deinit(allocator); queue.writeTexture(&.{ .texture = texture }, &data_layout, &img_size, data.rgba32); }, else => @panic("unsupported image color format"), diff --git a/examples/text/Game.zig b/examples/text/Game.zig index 1708073d..4075ae9e 100644 --- a/examples/text/Game.zig +++ b/examples/text/Game.zig @@ -17,6 +17,8 @@ const Vec3 = math.Vec3; const Mat3x3 = math.Mat3x3; const Mat4x4 = math.Mat4x4; +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + timer: mach.Timer, player: mach.ecs.EntityID, direction: Vec2 = vec2(0, 0), @@ -28,6 +30,7 @@ texts: usize, rand: std.rand.DefaultPrng, time: f32, style1: mach.ecs.EntityID, +allocator: std.mem.Allocator, const d0 = 0.000001; @@ -101,7 +104,8 @@ fn init( // TODO: better storage mechanism for this // TODO: this is a leak - const styles = try engine.allocator.alloc(mach.ecs.EntityID, 3); + const allocator = gpa.allocator(); + const styles = try allocator.alloc(mach.ecs.EntityID, 3); styles[0] = style1; styles[1] = style2; styles[2] = style3; @@ -113,7 +117,7 @@ fn init( }}); engine.dispatchNoError(); // TODO: no dispatch in user code - game.state = .{ + game.init(.{ .timer = try mach.Timer.start(), .spawn_timer = try mach.Timer.start(), .player = player, @@ -123,7 +127,8 @@ fn init( .rand = std.rand.DefaultPrng.init(1337), .time = 0, .style1 = style1, - }; + .allocator = allocator, + }); } fn deinit(engine: *mach.Engine.Mod) !void { @@ -137,8 +142,8 @@ fn tick( ) !void { // TODO(engine): event polling should occur in mach.Engine module and get fired as ECS events. var iter = core.pollEvents(); - var direction = game.state.direction; - var spawning = game.state.spawning; + var direction = game.state().direction; + var spawning = game.state().spawning; while (iter.next()) |event| { switch (event) { .key_press => |ev| { @@ -165,18 +170,18 @@ fn tick( else => {}, } } - game.state.direction = direction; - game.state.spawning = spawning; + game.state().direction = direction; + game.state().spawning = spawning; - var player_transform = text_mod.get(game.state.player, .transform).?; + var player_transform = text_mod.get(game.state().player, .transform).?; var player_pos = player_transform.translation().divScalar(upscale); - if (spawning and game.state.spawn_timer.read() > 1.0 / 60.0) { + if (spawning and game.state().spawn_timer.read() > 1.0 / 60.0) { // Spawn new entities - _ = game.state.spawn_timer.lap(); + _ = game.state().spawn_timer.lap(); for (0..1) |_| { var new_pos = player_pos; - new_pos.v[0] += game.state.rand.random().floatNorm(f32) * 25; - new_pos.v[1] += game.state.rand.random().floatNorm(f32) * 25; + new_pos.v[0] += game.state().rand.random().floatNorm(f32) * 25; + new_pos.v[1] += game.state().rand.random().floatNorm(f32) * 25; const new_entity = try engine.newEntity(); try text_mod.set(new_entity, .pipeline, @intFromEnum(Pipeline.default)); @@ -184,17 +189,17 @@ fn tick( // TODO: better storage mechanism for this // TODO: this is a leak - const styles = try engine.allocator.alloc(mach.ecs.EntityID, 1); - styles[0] = game.state.style1; + const styles = try game.state().allocator.alloc(mach.ecs.EntityID, 1); + styles[0] = game.state().style1; try text_mod.set(new_entity, .text, text2); try text_mod.set(new_entity, .style, styles); - game.state.texts += 1; + game.state().texts += 1; } } // Multiply by delta_time to ensure that movement is the same speed regardless of the frame rate. - const delta_time = game.state.timer.lap(); + const delta_time = game.state().timer.lap(); // Rotate entities var archetypes_iter = engine.entities.query(.{ .all = &.{ @@ -211,8 +216,8 @@ fn tick( // transform = transform.mul(&Mat4x4.translate(location)); var transform = Mat4x4.ident; transform = transform.mul(&Mat4x4.translate(location)); - transform = transform.mul(&Mat4x4.rotateZ(2 * math.pi * game.state.time)); - transform = transform.mul(&Mat4x4.scaleScalar(@min(math.cos(game.state.time / 2.0), 0.5))); + transform = transform.mul(&Mat4x4.rotateZ(2 * math.pi * game.state().time)); + transform = transform.mul(&Mat4x4.scaleScalar(@min(math.cos(game.state().time / 2.0), 0.5))); // TODO: .set() API is substantially slower due to internals // try text_mod.set(id, .transform, transform); @@ -225,7 +230,7 @@ fn tick( const speed = 200.0 / upscale; player_pos.v[0] += direction.x() * speed * delta_time; player_pos.v[1] += direction.y() * speed * delta_time; - try text_mod.set(game.state.player, .transform, Mat4x4.scaleScalar(upscale).mul(&Mat4x4.translate(player_pos))); + try text_mod.set(game.state().player, .transform, Mat4x4.scaleScalar(upscale).mul(&Mat4x4.translate(player_pos))); text_mod.send(.updated, .{@intFromEnum(Pipeline.default)}); // Perform pre-render work @@ -238,11 +243,11 @@ fn tick( engine.send(.present, .{}); // Present the frame // Every second, update the window title with the FPS - if (game.state.fps_timer.read() >= 1.0) { - try core.printTitle("gfx.Text example [ FPS: {d} ] [ Texts: {d} ]", .{ game.state.frame_count, game.state.texts }); - game.state.fps_timer.reset(); - game.state.frame_count = 0; + if (game.state().fps_timer.read() >= 1.0) { + try core.printTitle("gfx.Text example [ FPS: {d} ] [ Texts: {d} ]", .{ game.state().frame_count, game.state().texts }); + game.state().fps_timer.reset(); + game.state().frame_count = 0; } - game.state.frame_count += 1; - game.state.time += delta_time; + game.state().frame_count += 1; + game.state().time += delta_time; } diff --git a/src/ecs/main.zig b/src/ecs/main.zig index 9afff7b2..36e64185 100644 --- a/src/ecs/main.zig +++ b/src/ecs/main.zig @@ -83,17 +83,11 @@ test "example" { try world.init(allocator); defer world.deinit(allocator); - // TODO: better module initialization location - world.mod.physics.entities = &world.entities; - world.mod.physics.allocator = world.entities.allocator; - world.mod.renderer.entities = &world.entities; - world.mod.renderer.allocator = world.entities.allocator; - // Initialize module state. var physics = &world.mod.physics; var renderer = &world.mod.renderer; - physics.state = .{ .pointer = 123 }; - _ = physics.state.pointer; // == 123 + physics.init(.{ .pointer = 123 }); + _ = physics.state().pointer; // == 123 const player1 = try physics.newEntity(); const player2 = try physics.newEntity(); diff --git a/src/engine.zig b/src/engine.zig index c9e0491b..a653edec 100644 --- a/src/engine.zig +++ b/src/engine.zig @@ -39,20 +39,23 @@ pub const Engine = struct { fn init(engine: *Mod) !void { core.allocator = allocator; try core.init(.{}); - const state = &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", + engine.init(.{ + .device = core.device, + .queue = core.device.getQueue(), + .should_exit = false, + .pass = undefined, + .encoder = core.device.createCommandEncoder(&gpu.CommandEncoder.Descriptor{ + .label = "engine.state.encoder", + }), }); engine.sendGlobal(.init, .{}); } fn deinit(engine: *Mod) void { + const state = engine.state(); // TODO: this triggers a device loss error, which we should handle correctly - // engine.state.device.release(); - engine.state.queue.release(); + // state.device.release(); + state.queue.release(); engine.sendGlobal(.deinit, .{}); core.deinit(); } @@ -60,7 +63,8 @@ pub const Engine = struct { // Engine module's exit handler fn exit(engine: *Mod) void { engine.sendGlobal(.exit, .{}); - engine.state.should_exit = true; + const state = engine.state(); + state.should_exit = true; } fn beginPass(engine: *Mod, clear_color: gpu.Color) void { @@ -78,21 +82,23 @@ pub const Engine = struct { .color_attachments = &.{color_attachment}, }); - engine.state.pass = engine.state.encoder.beginRenderPass(&pass_info); + const state = engine.state(); + state.pass = state.encoder.beginRenderPass(&pass_info); } fn endPass(engine: *Mod) void { + const state = engine.state(); // End this pass - engine.state.pass.end(); - engine.state.pass.release(); + state.pass.end(); + state.pass.release(); - var command = engine.state.encoder.finish(null); + var command = state.encoder.finish(null); defer command.release(); - engine.state.encoder.release(); - engine.state.queue.submit(&[_]*gpu.CommandBuffer{command}); + state.encoder.release(); + state.queue.submit(&[_]*gpu.CommandBuffer{command}); // Prepare for next pass - engine.state.encoder = engine.state.device.createCommandEncoder(&gpu.CommandEncoder.Descriptor{ + state.encoder = state.device.createCommandEncoder(&gpu.CommandEncoder.Descriptor{ .label = "engine.state.encoder", }); } @@ -125,6 +131,6 @@ pub const App = struct { app.modules.mod.engine.sendGlobal(.tick, .{}); try app.modules.dispatch(); // dispatch .tick try app.modules.dispatch(); // dispatch any events produced by .tick - return app.modules.mod.engine.state.should_exit; + return app.modules.mod.engine.state().should_exit; } }; diff --git a/src/gfx/Sprite.zig b/src/gfx/Sprite.zig index 4d65fdda..c9603a81 100644 --- a/src/gfx/Sprite.zig +++ b/src/gfx/Sprite.zig @@ -12,8 +12,11 @@ const Vec3 = math.Vec3; const Mat3x3 = math.Mat3x3; const Mat4x4 = math.Mat4x4; +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + /// Internal state -pipelines: std.AutoArrayHashMapUnmanaged(u32, Pipeline), +pipelines: std.AutoArrayHashMapUnmanaged(u32, Pipeline) = .{}, +allocator: std.mem.Allocator, pub const name = .mach_gfx_sprite; pub const Mod = mach.Mod(@This()); @@ -137,18 +140,15 @@ pub const PipelineOptions = struct { pipeline_layout: ?*gpu.PipelineLayout = null, }; -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); +fn init(sprite_mod: *Mod) void { + sprite_mod.init(.{ + .allocator = gpa.allocator(), + }); } -fn init( - sprite_mod: *Mod, -) !void { - sprite_mod.state = .{ - // TODO: struct default value initializers don't work - .pipelines = .{}, - }; +fn deinit(sprite_mod: *Mod) !void { + for (sprite_mod.state().pipelines.entries.items(.value)) |*pipeline| pipeline.deinit(); + sprite_mod.state().pipelines.deinit(sprite_mod.state().allocator); } fn initPipeline( @@ -156,9 +156,9 @@ fn initPipeline( sprite_mod: *Mod, opt: PipelineOptions, ) !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(sprite_mod.state().allocator, opt.pipeline); if (pipeline.found_existing) { pipeline.value_ptr.*.deinit(); } @@ -296,8 +296,8 @@ fn updated( sprite_mod: *Mod, pipeline_id: u32, ) !void { - const pipeline = sprite_mod.state.pipelines.getPtr(pipeline_id).?; - const device = engine.state.device; + 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 @@ -337,7 +337,7 @@ fn updated( var command = encoder.finish(null); defer command.release(); - engine.state.queue.submit(&[_]*gpu.CommandBuffer{command}); + engine.state().queue.submit(&[_]*gpu.CommandBuffer{command}); } fn preRender( @@ -345,7 +345,7 @@ fn preRender( sprite_mod: *Mod, pipeline_id: u32, ) !void { - const pipeline = sprite_mod.state.pipelines.get(pipeline_id).?; + const pipeline = sprite_mod.state().pipelines.get(pipeline_id).?; // Update uniform buffer const proj = Mat4x4.projection2D(.{ @@ -365,7 +365,7 @@ fn preRender( ), }; - engine.state.encoder.writeBuffer(pipeline.uniforms, 0, &[_]Uniforms{uniforms}); + engine.state().encoder.writeBuffer(pipeline.uniforms, 0, &[_]Uniforms{uniforms}); } fn render( @@ -373,10 +373,10 @@ fn render( sprite_mod: *Mod, pipeline_id: u32, ) !void { - const pipeline = sprite_mod.state.pipelines.get(pipeline_id).?; + const pipeline = sprite_mod.state().pipelines.get(pipeline_id).?; // Draw the sprite batch - const pass = engine.state.pass; + const pass = engine.state().pass; const total_vertices = pipeline.num_sprites * 6; pass.setPipeline(pipeline.render); // TODO: remove dynamic offsets? diff --git a/src/gfx/Text.zig b/src/gfx/Text.zig index a5bfcc0a..373e937a 100644 --- a/src/gfx/Text.zig +++ b/src/gfx/Text.zig @@ -15,8 +15,11 @@ const vec4 = math.vec4; const Mat3x3 = math.Mat3x3; const Mat4x4 = math.Mat4x4; +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + /// Internal state -pipelines: std.AutoArrayHashMapUnmanaged(u32, Pipeline), +pipelines: std.AutoArrayHashMapUnmanaged(u32, Pipeline) = .{}, +allocator: std.mem.Allocator, pub const name = .mach_gfx_text; pub const Mod = mach.Mod(@This()); @@ -198,17 +201,14 @@ pub const PipelineOptions = struct { }; 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); + for (text_mod.state().pipelines.entries.items(.value)) |*pipeline| pipeline.deinit(text_mod.state().allocator); + text_mod.state().pipelines.deinit(text_mod.state().allocator); } -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.init(.{ + .allocator = gpa.allocator(), + }); } fn initPipeline( @@ -216,11 +216,11 @@ fn initPipeline( text_mod: *Mod, opt: PipelineOptions, ) !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(text_mod.state().allocator, opt.pipeline); if (pipeline.found_existing) { - pipeline.value_ptr.*.deinit(engine.allocator); + pipeline.value_ptr.*.deinit(text_mod.state().allocator); } // Prepare texture for the font atlas. @@ -235,7 +235,7 @@ fn initPipeline( }, }); const texture_atlas = try gfx.Atlas.init( - engine.allocator, + text_mod.state().allocator, img_size.width, .rgba, ); @@ -376,8 +376,8 @@ fn updated( text_mod: *Mod, pipeline_id: u32, ) !void { - const pipeline = text_mod.state.pipelines.getPtr(pipeline_id).?; - const device = engine.state.device; + 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 @@ -425,7 +425,7 @@ fn updated( _ = 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); + defer font.deinit(text_mod.state().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); @@ -450,16 +450,16 @@ fn updated( 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, .{ + const region = try pipeline.regions.getOrPut(text_mod.state().allocator, .{ .index = glyph.glyph_index, .size = @bitCast(font_size), }); if (!region.found_existing) { - const rendered_glyph = try font.render(engine.allocator, glyph.glyph_index, .{ + const rendered_glyph = try font.render(text_mod.state().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); + var glyph_atlas_region = try pipeline.texture_atlas.reserve(text_mod.state().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; @@ -484,7 +484,7 @@ fn updated( const r = region.value_ptr.*; const size = vec2(@floatFromInt(r.width), @floatFromInt(r.height)); - try glyphs.append(engine.allocator, .{ + try glyphs.append(text_mod.state().allocator, .{ .pos = vec2( origin_x + glyph.offset.x(), origin_y - (size.y() - glyph.offset.y()), @@ -509,7 +509,7 @@ fn updated( // TODO: could writeBuffer check for zero? if (glyphs.items.len > 0) encoder.writeBuffer(pipeline.glyphs, 0, glyphs.items); - defer glyphs.deinit(engine.allocator); + defer glyphs.deinit(text_mod.state().allocator); if (texture_update) { // rgba32_pixels // TODO: use proper texture dimensions here @@ -518,7 +518,7 @@ fn updated( .bytes_per_row = @as(u32, @intCast(img_size.width * 4)), .rows_per_image = @as(u32, @intCast(img_size.height)), }; - engine.state.queue.writeTexture( + engine.state().queue.writeTexture( &.{ .texture = pipeline.texture }, &data_layout, &img_size, @@ -529,7 +529,7 @@ fn updated( var command = encoder.finish(null); defer command.release(); - engine.state.queue.submit(&[_]*gpu.CommandBuffer{command}); + engine.state().queue.submit(&[_]*gpu.CommandBuffer{command}); } fn preRender( @@ -537,7 +537,7 @@ fn preRender( text_mod: *Mod, pipeline_id: u32, ) !void { - const pipeline = text_mod.state.pipelines.get(pipeline_id).?; + const pipeline = text_mod.state().pipelines.get(pipeline_id).?; // Update uniform buffer const proj = Mat4x4.projection2D(.{ @@ -557,7 +557,7 @@ fn preRender( ), }; - engine.state.encoder.writeBuffer(pipeline.uniforms, 0, &[_]Uniforms{uniforms}); + engine.state().encoder.writeBuffer(pipeline.uniforms, 0, &[_]Uniforms{uniforms}); } fn render( @@ -565,10 +565,10 @@ fn render( text_mod: *Mod, pipeline_id: u32, ) !void { - const pipeline = text_mod.state.pipelines.get(pipeline_id).?; + const pipeline = text_mod.state().pipelines.get(pipeline_id).?; // Draw the text batch - const pass = engine.state.pass; + const pass = engine.state().pass; const total_vertices = pipeline.num_glyphs * 6; pass.setPipeline(pipeline.render); // TODO: remove dynamic offsets? diff --git a/src/module.zig b/src/module.zig index 93275d1f..1d6e759c 100644 --- a/src/module.zig +++ b/src/module.zig @@ -4,6 +4,7 @@ const testing = @import("testing.zig"); const Entities = @import("ecs/entities.zig").Entities; const EntityID = @import("ecs/entities.zig").EntityID; +const is_debug = @import("ecs/comptime.zig").is_debug; // TODO: make sendToModule the default name for sending events? and sendGlobal always secondary? Or vice-versa? @@ -71,6 +72,16 @@ pub fn Modules(comptime modules: anytype) type { errdefer m.args_queue.deinit(allocator); errdefer m.events.deinit(); try m.events.ensureTotalCapacity(1024); + + // Default initialize m.mod + inline for (@typeInfo(@TypeOf(m.mod)).Struct.fields) |field| { + const Mod2 = @TypeOf(@field(m.mod, field.name)); + @field(m.mod, field.name) = Mod2{ + .__is_initialized = false, + .__state = undefined, + .entities = &m.entities, + }; + } } pub fn deinit(m: *@This(), allocator: std.mem.Allocator) void { @@ -99,6 +110,7 @@ pub fn Modules(comptime modules: anytype) type { } } + // TODO: docs fn moduleToGlobalEvent( comptime M: type, comptime EventEnumM: anytype, @@ -106,9 +118,7 @@ pub fn Modules(comptime modules: anytype) type { comptime event_name: EventEnumM(M), ) EventEnum(modules) { for (@typeInfo(EventEnum(modules)).Enum.fields) |gfield| { - if (std.mem.eql(u8, @tagName(event_name), gfield.name)) { - return @enumFromInt(gfield.value); - } + if (std.mem.eql(u8, @tagName(event_name), gfield.name)) return @enumFromInt(gfield.value); } unreachable; } @@ -118,12 +128,14 @@ pub fn Modules(comptime modules: anytype) type { 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 module_name: ModuleName(modules), - comptime event_name: GlobalEventEnumM(@TypeOf(@field(m.mod, @tagName(module_name)).state)), - args: GlobalArgsM(@TypeOf(@field(m.mod, @tagName(module_name)).state), event_name), + // TODO: cleanup comptime + comptime event_name: GlobalEventEnumM(@TypeOf(@field(m.mod, @tagName(module_name)).__state)), + args: GlobalArgsM(@TypeOf(@field(m.mod, @tagName(module_name)).__state), event_name), ) void { // TODO: comptime safety/debugging const event_name_g: GlobalEvent = comptime moduleToGlobalEvent( - @TypeOf(@field(m.mod, @tagName(module_name)).state), + // TODO: cleanup comptime + @TypeOf(@field(m.mod, @tagName(module_name)).__state), GlobalEventEnumM, GlobalEventEnum, event_name, @@ -136,12 +148,14 @@ pub fn Modules(comptime modules: 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(modules), - comptime event_name: LocalEventEnumM(@TypeOf(@field(m.mod, @tagName(module_name)).state)), - args: LocalArgsM(@TypeOf(@field(m.mod, @tagName(module_name)).state), event_name), + // TODO: cleanup comptime + comptime event_name: LocalEventEnumM(@TypeOf(@field(m.mod, @tagName(module_name)).__state)), + args: LocalArgsM(@TypeOf(@field(m.mod, @tagName(module_name)).__state), event_name), ) void { // TODO: comptime safety/debugging const event_name_g: LocalEvent = comptime moduleToGlobalEvent( - @TypeOf(@field(m.mod, @tagName(module_name)).state), + // TODO: cleanup comptime + @TypeOf(@field(m.mod, @tagName(module_name)).__state), LocalEventEnumM, LocalEventEnum, event_name, @@ -197,10 +211,6 @@ pub fn Modules(comptime modules: anytype) type { inline for (@typeInfo(ModsByName(modules)).Struct.fields) |injectable_field| { if (*injectable_field.type == field.type) { @field(injectable, field.name) = &@field(m.mod, injectable_field.name); - - // TODO: better module initialization location - @field(injectable, field.name).entities = &m.entities; - @field(injectable, field.name).allocator = m.entities.allocator; continue :outer; } } @@ -344,26 +354,52 @@ pub fn ModsByName(comptime modules: anytype) type { // Mod() -> Modules() // pub fn ModSet(comptime modules: anytype) type { - const NSComponents = ComponentTypesByName(modules); return struct { pub fn Mod(comptime M: anytype) type { const module_tag = M.name; const components = ComponentTypesM(M){}; return struct { - state: M, - entities: *Entities(NSComponents{}), - // TODO: eliminate this global allocator and/or rethink allocation strategies for modules - allocator: std.mem.Allocator, + entities: *Entities(ComponentTypesByName(modules){}), + + /// Private/internal fields + __is_initialized: bool, + __state: M, pub const IsInjectedArgument = void; + /// Initializes the module's state + pub inline fn init(m: *@This(), s: M) void { + m.__state = s; + m.__is_initialized = true; + } + + /// Returns a mutable pointer to the module's state (literally the struct type of the module.) + /// + /// A panic will occur if m.init(M{}) was not called previously. + pub inline fn state(m: *@This()) *M { + if (is_debug) if (!m.__is_initialized) @panic("mach: module ." ++ @tagName(M.name) ++ " state is not initialized, ensure foo_mod.init(.{}) is called!"); + return &m.__state; + } + + /// Returns a read-only version of the module's state. If an event handler is + /// read-only (i.e. only ever reads state/components/entities), then its events can + /// be skipped during e.g. record-and-replay of events from disk. + /// + /// Only use this if the module state being serialized and deserialized after the + /// event handler runs would accurately reproduce the state of the event handler + /// being run. + pub inline fn stateReadOnly(m: *@This()) *const M { + if (is_debug) if (!m.__is_initialized) @panic("mach: module ." ++ @tagName(M.name) ++ " state is not initialized, ensure mod.init(.{}) is called!"); + return &m.__state; + } + /// Returns a new entity. - pub fn newEntity(m: *@This()) !EntityID { + pub inline fn newEntity(m: *@This()) !EntityID { return m.entities.new(); } /// Removes an entity. - pub fn removeEntity(m: *@This(), entity: EntityID) !void { + pub inline fn removeEntity(m: *@This(), entity: EntityID) !void { try m.entities.removeEntity(entity); }