From eda3e68b6e9c1395d484240deb2a6b051a639360 Mon Sep 17 00:00:00 2001 From: Stephen Gutekanst Date: Fri, 27 Dec 2024 16:36:53 -0700 Subject: [PATCH] examples: text example updated to new object system Signed-off-by: Stephen Gutekanst --- build.zig | 2 +- examples/text/App.zig | 287 +++++++++++++++++++++-------------------- examples/text/main.zig | 2 +- 3 files changed, 147 insertions(+), 144 deletions(-) diff --git a/build.zig b/build.zig index baa72813..0927cd3d 100644 --- a/build.zig +++ b/build.zig @@ -80,7 +80,7 @@ pub fn build(b: *std.Build) !void { .{ .name = "piano", .deps = &.{} }, .{ .name = "play-opus", .deps = &.{.assets} }, .{ .name = "sprite", .deps = &.{ .zigimg, .assets } }, - // .{ .name = "text", .deps = &.{.assets} }, + .{ .name = "text", .deps = &.{.assets} }, }; var sysaudio_tests = [_]SysAudioTest{ diff --git a/examples/text/App.zig b/examples/text/App.zig index dc600e83..62d6d323 100644 --- a/examples/text/App.zig +++ b/examples/text/App.zig @@ -18,21 +18,29 @@ const App = @This(); pub const mach_module = .app; -pub const mach_systems = .{ .start, .init, .deinit, .tick, .end_frame }; +pub const mach_systems = .{ .main, .init, .tick, .deinit }; +pub const main = mach.schedule(.{ + .{ mach.Core, .init }, + .{ gfx.Text, .init }, + .{ App, .init }, + .{ mach.Core, .main }, +}); + +allocator: std.mem.Allocator, +window: mach.ObjectID, timer: mach.time.Timer, -player: mach.EntityID, -direction: Vec2 = vec2(0, 0), -spawning: bool = false, spawn_timer: mach.time.Timer, fps_timer: mach.time.Timer, -frame_count: usize, rand: std.Random.DefaultPrng, -time: f32, -style1: mach.EntityID, -pipeline: mach.EntityID, -frame_encoder: *gpu.CommandEncoder = undefined, -frame_render_pass: *gpu.RenderPassEncoder = undefined, + +frame_count: usize = 0, +time: f32 = 0, +direction: Vec2 = vec2(0, 0), +spawning: bool = false, +player_id: mach.ObjectID = undefined, +style1_id: mach.ObjectID = undefined, +pipeline_id: mach.ObjectID = undefined, const upscale = 1.0; @@ -44,75 +52,78 @@ const text1: []const []const u8 = &.{ const text2: []const []const u8 = &.{"$!?"}; -fn deinit(text_pipeline: *gfx.TextPipeline.Mod) !void { - text_pipeline.schedule(.deinit); -} - -fn start( +pub fn init( core: *mach.Core, - text: *gfx.Text.Mod, - text_pipeline: *gfx.TextPipeline.Mod, - app: *App, -) !void { - core.schedule(.init); - text.schedule(.init); - text_pipeline.schedule(.init); - app.schedule(.init); -} - -fn init( - entities: *mach.Entities.Mod, - core: *mach.Core, - text: *gfx.Text.Mod, - text_pipeline: *gfx.TextPipeline.Mod, - text_style: *gfx.TextStyle.Mod, app: *App, app_mod: mach.Mod(App), ) !void { core.on_tick = app_mod.id.tick; core.on_exit = app_mod.id.deinit; - // TODO: a better way to initialize entities with default values - // TODO(text): ability to specify other style options (custom font name, font color, italic/bold, etc.) - const style1 = try entities.new(); - try text_style.set(style1, .font_size, 48 * gfx.px_per_pt); // 48pt + const window = try core.windows.new(.{ + .title = "gfx.Text", + }); - // Create a text rendering pipeline - const pipeline = try entities.new(); - try text_pipeline.set(pipeline, .is_pipeline, {}); - text_pipeline.schedule(.update); + // TODO(allocator): find a better way to get an allocator here + const allocator = std.heap.c_allocator; - // Create some text - const player = try entities.new(); - try text.set(player, .pipeline, pipeline); - try text.set(player, .transform, Mat4x4.translate(vec3(0, 0, 0))); - try gfx.Text.allocPrintText(text, player, style1, - \\ Text with spaces - \\ and newlines - \\ but nothing fancy yet - , .{}); - text.schedule(.update); - - app.init(.{ + app.* = .{ + .allocator = allocator, + .window = window, .timer = try mach.time.Timer.start(), .spawn_timer = try mach.time.Timer.start(), - .player = player, .fps_timer = try mach.time.Timer.start(), - .frame_count = 0, .rand = std.Random.DefaultPrng.init(1337), - .time = 0, - .style1 = style1, - .pipeline = pipeline, - }); + }; } -fn tick( - entities: *mach.Entities.Mod, - core: *mach.Core, - text: *gfx.Text.Mod, - text_pipeline: *gfx.TextPipeline.Mod, +fn setupPipeline( app: *App, + text: *gfx.Text, + window_id: mach.ObjectID, ) !void { + // Create a text rendering pipeline + app.pipeline_id = try text.pipelines.new(.{ + .window = window_id, + .render_pass = undefined, + }); + + // Create a text style + app.style1_id = try text.styles.new(.{ + .font_size = 48 * gfx.px_per_pt, // 48pt + }); + + // TODO(text): release this memory somewhere + const player_text_value = + \\ Text with spaces + \\ and newlines + \\ but nothing fancy (yet) + ; + const player_text = try app.allocator.alloc(u8, player_text_value.len); + @memcpy(player_text, player_text_value); + const player_segments = try app.allocator.alloc(gfx.Text.Segment, 1); + player_segments[0] = .{ + .text = player_text, + .style = app.style1_id, + }; + + // Create our player text + app.player_id = try text.objects.new(.{ + .transform = Mat4x4.translate(vec3(-0.02, 0, 0)), + .segments = player_segments, + }); + // Attach the text object to our text rendering pipeline. + try text.pipelines.setParent(app.player_id, app.pipeline_id); +} + +pub fn tick( + core: *mach.Core, + app: *App, + text: *gfx.Text, + text_mod: mach.Mod(gfx.Text), +) !void { + const label = @tagName(mach_module) ++ ".tick"; + var direction = app.direction; var spawning = app.spawning; while (core.nextEvent()) |event| { @@ -137,6 +148,7 @@ fn tick( else => {}, } }, + .window_open => |ev| try setupPipeline(app, text, ev.window_id), .close => core.exit(), else => {}, } @@ -144,9 +156,9 @@ fn tick( app.direction = direction; app.spawning = spawning; - var player_transform = text.get(app.player, .transform).?; - var player_pos = player_transform.translation().divScalar(upscale); - if (spawning and app.spawn_timer.read() > (1.0 / 60.0)) { + var player = text.objects.getValue(app.player_id); + var player_pos = player.transform.translation(); + if (spawning and app.spawn_timer.read() > 1.0 / 60.0) { // Spawn new entities _ = app.spawn_timer.lap(); for (0..10) |_| { @@ -154,56 +166,61 @@ fn tick( new_pos.v[0] += app.rand.random().floatNorm(f32) * 50; new_pos.v[1] += app.rand.random().floatNorm(f32) * 50; - // Create some text - const new_entity = try entities.new(); - try text.set(new_entity, .pipeline, app.pipeline); - try text.set(new_entity, .transform, Mat4x4.scaleScalar(upscale).mul(&Mat4x4.translate(new_pos))); - try gfx.Text.allocPrintText(text, new_entity, app.style1, "?!$", .{}); + // TODO(text): release this memory somewhere + const new_text_value = "?!"; + const new_text = try app.allocator.alloc(u8, new_text_value.len); + @memcpy(new_text, new_text_value); + const new_segments = try app.allocator.alloc(gfx.Text.Segment, 1); + new_segments[0] = .{ + .text = new_text, + .style = app.style1_id, + }; + + const new_text_id = try text.objects.new(.{ + .transform = Mat4x4.scaleScalar(upscale).mul(&Mat4x4.translate(new_pos)), + .segments = new_segments, + }); + try text.pipelines.setParent(new_text_id, app.pipeline_id); } } // Multiply by delta_time to ensure that movement is the same speed regardless of the frame rate. const delta_time = app.timer.lap(); - // Rotate entities - var q = try entities.query(.{ - .transforms = gfx.Text.Mod.write(.transform), - }); - while (q.next()) |v| { - for (v.transforms) |*entity_transform| { - const location = entity_transform.*.translation(); - // var transform = old_transform.mul(&Mat4x4.translate(-location)); - // transform = mat.rotateZ(0.3 * delta_time).mul(&transform); - // transform = transform.mul(&Mat4x4.translate(location)); - var transform = Mat4x4.ident; - transform = transform.mul(&Mat4x4.translate(location)); - transform = transform.mul(&Mat4x4.rotateZ(2 * math.pi * app.time)); - transform = transform.mul(&Mat4x4.scaleScalar(@min(math.cos(app.time / 2.0), 0.5))); - entity_transform.* = transform; - } + // Rotate all text objects in the pipeline. + var pipeline_children = try text.pipelines.getChildren(app.pipeline_id); + defer pipeline_children.deinit(); + for (pipeline_children.items) |text_id| { + if (!text.objects.is(text_id)) continue; + if (text_id == app.player_id) continue; // don't rotate the player + var s = text.objects.getValue(text_id); + + const location = s.transform.translation(); + var transform = Mat4x4.ident; + transform = transform.mul(&Mat4x4.translate(location)); + transform = transform.mul(&Mat4x4.rotateZ(2 * math.pi * app.time)); + transform = transform.mul(&Mat4x4.scaleScalar(@min(math.cos(app.time / 2.0), 0.5))); + text.objects.set(text_id, .transform, transform); } // Calculate the player position, by moving in the direction the player wants to go // by the speed amount. - const speed = 200.0 / upscale; + const speed = 200.0; player_pos.v[0] += direction.x() * speed * delta_time; player_pos.v[1] += direction.y() * speed * delta_time; - try text.set(app.player, .transform, Mat4x4.scaleScalar(upscale).mul(&Mat4x4.translate(player_pos))); - try text.set(app.player, .dirty, true); - text.schedule(.update); + text.objects.set(app.player_id, .transform, Mat4x4.translate(player_pos)); - // Perform pre-render work - text_pipeline.schedule(.pre_render); - - // Create a command encoder for this frame - const label = @tagName(mach_module) ++ ".tick"; - app.frame_encoder = core.device.createCommandEncoder(&.{ .label = label }); + const window = core.windows.getValue(app.window); // Grab the back buffer of the swapchain // TODO(Core) - const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + const back_buffer_view = window.swap_chain.getCurrentTextureView().?; defer back_buffer_view.release(); + // Create a command encoder + const encoder = window.device.createCommandEncoder(&.{ .label = label }); + defer encoder.release(); + // Begin render pass const sky_blue = gpu.Color{ .r = 0.776, .g = 0.988, .b = 1, .a = 1 }; const color_attachments = [_]gpu.RenderPassColorAttachment{.{ @@ -212,58 +229,44 @@ fn tick( .load_op = .clear, .store_op = .store, }}; - app.frame_render_pass = app.frame_encoder.beginRenderPass(&gpu.RenderPassDescriptor.init(.{ + const render_pass = encoder.beginRenderPass(&gpu.RenderPassDescriptor.init(.{ .label = label, .color_attachments = &color_attachments, })); - // Render our text batch - text_pipeline.state().render_pass = app.frame_render_pass; - text_pipeline.schedule(.render); + // Render text + text.pipelines.set(app.pipeline_id, .render_pass, render_pass); + text_mod.call(.tick); - // Finish the frame once rendering is done. - app.schedule(.end_frame); - - app.time += delta_time; -} - -fn endFrame( - entities: *mach.Entities.Mod, - app: *App, - core: *mach.Core, -) !void { // Finish render pass - app.frame_render_pass.end(); - const label = @tagName(mach_module) ++ ".endFrame"; - var command = app.frame_encoder.finish(&.{ .label = label }); - core.queue.submit(&[_]*gpu.CommandBuffer{command}); + render_pass.end(); + var command = encoder.finish(&.{ .label = label }); + window.queue.submit(&[_]*gpu.CommandBuffer{command}); command.release(); - app.frame_encoder.release(); - app.frame_render_pass.release(); + render_pass.release(); - // Every second, update the window title with the FPS - if (app.fps_timer.read() >= 1.0) { - // Gather some text rendering stats - var num_texts: u32 = 0; - var num_glyphs: usize = 0; - var q = try entities.query(.{ - .built_pipelines = gfx.Text.Mod.read(.built), - }); - while (q.next()) |v| { - for (v.built_pipelines) |built| { - num_texts += 1; - num_glyphs += built.glyphs.items.len; - } - } - - try core.printTitle( - core.main_window, - "text [ FPS: {d} ] [ Texts: {d} ] [ Glyphs: {d} ]", - .{ app.frame_count, num_texts, num_glyphs }, - ); - core.schedule(.update); - app.fps_timer.reset(); - app.frame_count = 0; - } app.frame_count += 1; + app.time += delta_time; + + // TODO(object): window-title + // // Every second, update the window title with the FPS + // if (app.fps_timer.read() >= 1.0) { + // const pipeline = text.pipelines.getValue(app.pipeline_id); + // try core.printTitle( + // core.main_window, + // "text [ FPS: {d} ] [ Texts: {d} ] [ Segments: {d} ] [ Styles: {d} ]", + // .{ app.frame_count, pipeline.num_texts, pipeline.num_segments, pipeline.num_styles }, + // ); + // core.schedule(.update); + // app.fps_timer.reset(); + // app.frame_count = 0; + // } +} + +pub fn deinit( + app: *App, + text: *gfx.Text, +) void { + // Cleanup here, if desired. + text.objects.delete(app.player_id); } diff --git a/examples/text/main.zig b/examples/text/main.zig index 3040c025..420f89fe 100644 --- a/examples/text/main.zig +++ b/examples/text/main.zig @@ -4,7 +4,7 @@ const mach = @import("mach"); // The set of Mach modules our application may use. const Modules = mach.Modules(.{ mach.Core, - mach.gfx.text_modules, + mach.gfx.Text, @import("App.zig"), });