diff --git a/examples/core-custom-entrypoint/Game.zig b/examples/core-custom-entrypoint/Game.zig index 0f0e6e41..ad9970bb 100644 --- a/examples/core-custom-entrypoint/Game.zig +++ b/examples/core-custom-entrypoint/Game.zig @@ -51,7 +51,7 @@ fn init(game: *Mod, core: *mach.Core.Mod) !void { .title_timer = try mach.Timer.start(), .pipeline = pipeline, }); - try updateWindowTitle(); + try updateWindowTitle(core); } pub fn deinit(game: *Mod) void { @@ -109,13 +109,19 @@ fn tick(core: *mach.Core.Mod, game: *Mod) !void { // update the window title every second if (game.state().title_timer.read() >= 1.0) { game.state().title_timer.reset(); - try updateWindowTitle(); + try updateWindowTitle(core); } } -fn updateWindowTitle() !void { - try mach.core.printTitle("mach.Core - custom entrypoint [ {d}fps ] [ Input {d}hz ]", .{ - mach.core.frameRate(), - mach.core.inputRate(), - }); +fn updateWindowTitle(core: *mach.Core.Mod) !void { + try mach.Core.printTitle( + core, + core.state().main_window, + "core-custom-entrypoint [ {d}fps ] [ Input {d}hz ]", + .{ + mach.core.frameRate(), + mach.core.inputRate(), + }, + ); + core.send(.update, .{}); } diff --git a/examples/glyphs/Game.zig b/examples/glyphs/Game.zig index a71d74f8..25ebf2f6 100644 --- a/examples/glyphs/Game.zig +++ b/examples/glyphs/Game.zig @@ -57,9 +57,6 @@ fn afterInit( glyphs: *Glyphs.Mod, game: *Mod, ) !void { - // The Mach .core is where we set window options, etc. - mach.core.setTitle("gfx.Sprite example"); - // Create a sprite rendering pipeline const texture = glyphs.state().texture; const pipeline = try sprite_pipeline.newEntity(); @@ -231,7 +228,7 @@ fn tick( game.state().time += delta_time; } -fn endFrame(game: *Mod) !void { +fn endFrame(game: *Mod, core: *mach.Core.Mod) !void { // Finish render pass game.state().frame_render_pass.end(); const label = @tagName(name) ++ ".endFrame"; @@ -245,7 +242,13 @@ fn endFrame(game: *Mod) !void { // Every second, update the window title with the FPS if (game.state().fps_timer.read() >= 1.0) { - try mach.core.printTitle("gfx.Sprite example [ FPS: {d} ] [ Sprites: {d} ]", .{ game.state().frame_count, game.state().sprites }); + try mach.Core.printTitle( + core, + core.state().main_window, + "glyphs [ FPS: {d} ] [ Sprites: {d} ]", + .{ game.state().frame_count, game.state().sprites }, + ); + core.send(.update, .{}); game.state().fps_timer.reset(); game.state().frame_count = 0; } diff --git a/examples/sprite/Game.zig b/examples/sprite/Game.zig index 238e8d33..b89caa98 100644 --- a/examples/sprite/Game.zig +++ b/examples/sprite/Game.zig @@ -51,9 +51,6 @@ fn init( sprite_pipeline: *gfx.SpritePipeline.Mod, game: *Mod, ) !void { - // The Mach .core is where we set window options, etc. - mach.core.setTitle("gfx.Sprite example"); - // We can create entities, and set components on them. Note that components live in a module // namespace, e.g. the `.mach_gfx_sprite` module could have a 3D `.location` component with a different // type than the `.physics2d` module's `.location` component if you desire. @@ -215,7 +212,7 @@ fn tick( game.state().time += delta_time; } -fn endFrame(game: *Mod) !void { +fn endFrame(game: *Mod, core: *mach.Core.Mod) !void { // Finish render pass game.state().frame_render_pass.end(); const label = @tagName(name) ++ ".endFrame"; @@ -229,7 +226,13 @@ fn endFrame(game: *Mod) !void { // Every second, update the window title with the FPS if (game.state().fps_timer.read() >= 1.0) { - try mach.core.printTitle("gfx.Sprite example [ FPS: {d} ] [ Sprites: {d} ]", .{ game.state().frame_count, game.state().sprites }); + try mach.Core.printTitle( + core, + core.state().main_window, + "sprite [ FPS: {d} ] [ Sprites: {d} ]", + .{ game.state().frame_count, game.state().sprites }, + ); + core.send(.update, .{}); game.state().fps_timer.reset(); game.state().frame_count = 0; } diff --git a/examples/text/Game.zig b/examples/text/Game.zig index e5e17ac3..5391a3a0 100644 --- a/examples/text/Game.zig +++ b/examples/text/Game.zig @@ -258,7 +258,7 @@ fn tick( game.state().time += delta_time; } -fn endFrame(game: *Mod, text: *gfx.Text.Mod) !void { +fn endFrame(game: *Mod, text: *gfx.Text.Mod, core: *mach.Core.Mod) !void { // Finish render pass game.state().frame_render_pass.end(); const label = @tagName(name) ++ ".tick"; @@ -288,7 +288,13 @@ fn endFrame(game: *Mod, text: *gfx.Text.Mod) !void { } } - try mach.core.printTitle("gfx.Text example [ FPS: {d} ] [ Texts: {d} ] [ Glyphs: {d} ]", .{ game.state().frame_count, num_texts, num_glyphs }); + try mach.Core.printTitle( + core, + core.state().main_window, + "text [ FPS: {d} ] [ Texts: {d} ] [ Glyphs: {d} ]", + .{ game.state().frame_count, num_texts, num_glyphs }, + ); + core.send(.update, .{}); game.state().fps_timer.reset(); game.state().frame_count = 0; } diff --git a/src/Core.zig b/src/Core.zig index 520ba979..d5d0d5e2 100644 --- a/src/Core.zig +++ b/src/Core.zig @@ -15,6 +15,10 @@ pub const global_events = .{ }; pub const local_events = .{ + .update = .{ .handler = update, .description = + \\ Send this when window entities have been updated and you want the new values respected. + }, + .init = .{ .handler = init }, .init_done = .{ .handler = fn () void }, @@ -26,10 +30,40 @@ pub const local_events = .{ .exit = .{ .handler = exit }, }; +pub const components = .{ + .title = .{ .type = [:0]u8, .description = + \\ Window title slice. Can be set with a format string and arguments via: + \\ + \\ ``` + \\ try mach.Core.printTitle(core_mod, core_mod.state().main_window, "Hello, {s}!", .{"Mach"}); + \\ ``` + \\ + \\ If setting this component yourself, ensure the buffer is allocated using core.state().allocator + \\ as it will be freed for you as part of the .deinit event. + }, +}; + +/// Prints into the window title buffer using a format string and arguments. e.g. +/// +/// ``` +/// try mach.Core.printTitle(core_mod, core_mod.state().main_window, "Hello, {s}!", .{"Mach"}); +/// ``` +pub fn printTitle( + core: *mach.Core.Mod, + window_id: mach.EntityID, + comptime fmt: []const u8, + args: anytype, +) !void { + const slice = try std.fmt.allocPrintZ(core.state().allocator, fmt, args); + try core.set(window_id, .title, slice); +} + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; +allocator: std.mem.Allocator, device: *mach.gpu.Device, queue: *mach.gpu.Queue, +main_window: mach.EntityID, should_exit: bool = false, fn init(core: *Mod) !void { @@ -41,19 +75,52 @@ fn init(core: *Mod) !void { try mach.core.init(.{}); core.init(.{ + .allocator = mach.core.allocator, .device = mach.core.device, .queue = mach.core.device.getQueue(), + .main_window = try core.newEntity(), }); core.sendGlobal(.init, .{}); core.send(.init_done, .{}); } +fn update(core: *Mod) !void { + var archetypes_iter = core.entities.query(.{ .all = &.{ + .{ .mach_core = &.{ + .title, + } }, + } }); + + var num_windows: usize = 0; + while (archetypes_iter.next()) |archetype| { + for ( + archetype.slice(.entity, .id), + archetype.slice(.mach_core, .title), + ) |window_id, title| { + num_windows += 1; + _ = window_id; + try mach.core.printTitle("{s}", .{title}); + } + } + if (num_windows > 1) @panic("mach: Core currently only supports a single window"); +} + fn deinit(core: *Mod) void { core.state().queue.release(); // TODO: this triggers a device loss error, which we should handle correctly // core.state().device.release(); mach.core.deinit(); + + var archetypes_iter = core.entities.query(.{ .all = &.{ + .{ .mach_core = &.{ + .title, + } }, + } }); + while (archetypes_iter.next()) |archetype| { + for (archetype.slice(.mach_core, .title)) |title| core.state().allocator.free(title); + } + _ = gpa.deinit(); }