diff --git a/build.zig b/build.zig index 93095107..f28bc047 100644 --- a/build.zig +++ b/build.zig @@ -663,11 +663,10 @@ fn buildExamples( deps: []const Dependency = &.{}, std_platform_only: bool = false, has_assets: bool = false, - use_module_api: bool = false, }{ - .{ .name = "sysaudio", .deps = &.{}, .use_module_api = true }, - .{ .name = "core-custom-entrypoint", .deps = &.{}, .use_module_api = true }, - .{ .name = "custom-renderer", .deps = &.{}, .use_module_api = true }, + .{ .name = "sysaudio", .deps = &.{} }, + .{ .name = "core-custom-entrypoint", .deps = &.{} }, + .{ .name = "custom-renderer", .deps = &.{} }, .{ .name = "sprite", .deps = &.{ .zigimg, .assets }, @@ -691,61 +690,31 @@ fn buildExamples( if (target.result.cpu.arch == .wasm32) break; - if (example.use_module_api) { - const exe = b.addExecutable(.{ - .name = example.name, - .root_source_file = .{ .path = "examples/" ++ example.name ++ "/main.zig" }, - .target = target, - .optimize = optimize, - }); - exe.root_module.addImport("mach", mach_mod); - addPaths(&exe.root_module); - link(b, exe, &exe.root_module); - b.installArtifact(exe); + const exe = b.addExecutable(.{ + .name = example.name, + .root_source_file = .{ .path = "examples/" ++ example.name ++ "/main.zig" }, + .target = target, + .optimize = optimize, + }); + exe.root_module.addImport("mach", mach_mod); + addPaths(&exe.root_module); + link(b, exe, &exe.root_module); + b.installArtifact(exe); - const compile_step = b.step(example.name, "Compile " ++ example.name); - compile_step.dependOn(b.getInstallStep()); - - const run_cmd = b.addRunArtifact(exe); - run_cmd.step.dependOn(b.getInstallStep()); - if (b.args) |args| run_cmd.addArgs(args); - - const run_step = b.step("run-" ++ example.name, "Run " ++ example.name); - run_step.dependOn(&run_cmd.step); - } else { - var deps = std.ArrayList(std.Build.Module.Import).init(b.allocator); - for (example.deps) |d| try deps.append(d.dependency(b, target, optimize)); - const app = try App.init( - b, - .{ - .name = example.name, - .src = "examples/" ++ example.name ++ "/main.zig", - .target = target, - .optimize = optimize, - .deps = deps.items, - .res_dirs = if (example.has_assets) &.{example.name ++ "/assets"} else null, - .watch_paths = &.{"examples/" ++ example.name}, - .mach_builder = b, - .mach_mod = mach_mod, - }, - ); - - try app.link(); - - for (example.deps) |dep| switch (dep) { - .model3d => app.compile.linkLibrary(b.dependency("mach_model3d", .{ - .target = target, - .optimize = optimize, - }).artifact("mach-model3d")), - else => {}, - }; - - const compile_step = b.step(example.name, "Compile " ++ example.name); - compile_step.dependOn(&app.install.step); - - const run_step = b.step("run-" ++ example.name, "Run " ++ example.name); - run_step.dependOn(&app.run.step); + for (example.deps) |d| { + const dep = d.dependency(b, target, optimize); + exe.root_module.addImport(dep.name, dep.module); } + + const compile_step = b.step(example.name, "Compile " ++ example.name); + compile_step.dependOn(b.getInstallStep()); + + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| run_cmd.addArgs(args); + + const run_step = b.step("run-" ++ example.name, "Run " ++ example.name); + run_step.dependOn(&run_cmd.step); } } diff --git a/examples/glyphs/Game.zig b/examples/glyphs/Game.zig index a520c7e0..34595114 100644 --- a/examples/glyphs/Game.zig +++ b/examples/glyphs/Game.zig @@ -1,7 +1,6 @@ // TODO(important): review all code in this file in-depth const std = @import("std"); const mach = @import("mach"); -const core = mach.core; const gpu = mach.gpu; const gfx = mach.gfx; const math = mach.math; @@ -25,19 +24,11 @@ sprites: usize, rand: std.rand.DefaultPrng, time: f32, pipeline: mach.EntityID, +frame_encoder: *gpu.CommandEncoder = undefined, +frame_render_pass: *gpu.RenderPassEncoder = undefined, -const d0 = 0.000001; - -// 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: -// -// 1. `.mach` and the `.mach_foobar` namespace is reserved for Mach itself and the modules it -// provides. -// 2. Single-word names like `.game` are reserved for the application itself. -// 3. Libraries which provide modules MUST be prefixed with an "owner" name, e.g. `.ziglibs_imgui` -// instead of `.imgui`. We encourage using e.g. your GitHub name, as these must be globally -// unique. -// +// Define the globally unique name of our module. You can use any name here, but keep in mind no +// two modules in the program can have the same name. pub const name = .game; pub const Mod = mach.Mod(@This()); @@ -48,6 +39,7 @@ pub const global_events = .{ pub const local_events = .{ .after_init = .{ .handler = afterInit }, + .end_frame = .{ .handler = endFrame }, }; fn init( @@ -69,7 +61,7 @@ fn afterInit( game: *Mod, ) !void { // The Mach .core is where we set window options, etc. - core.setTitle("gfx.Sprite example"); + mach.core.setTitle("gfx.Sprite example"); // Create a sprite rendering pipeline const texture = glyphs.state().texture; @@ -103,14 +95,14 @@ fn afterInit( } fn tick( - engine: *mach.Engine.Mod, + core: *mach.Core.Mod, sprite: *gfx.Sprite.Mod, sprite_pipeline: *gfx.SpritePipeline.Mod, glyphs: *Glyphs.Mod, game: *Mod, ) !void { - // TODO(engine): event polling should occur in mach.Engine module and get fired as ECS events. - var iter = core.pollEvents(); + // TODO(important): event polling should occur in mach.Core module and get fired as ECS events. + var iter = mach.core.pollEvents(); var direction = game.state().direction; var spawning = game.state().spawning; while (iter.next()) |event| { @@ -135,7 +127,7 @@ fn tick( else => {}, } }, - .close => engine.send(.exit, .{}), + .close => core.send(.exit, .{}), else => {}, } } @@ -155,7 +147,7 @@ fn tick( const rand_index = game.state().rand.random().intRangeAtMost(usize, 0, glyphs.state().regions.count() - 1); const r = glyphs.state().regions.entries.get(rand_index).value; - const new_entity = try engine.newEntity(); + const new_entity = try core.newEntity(); try sprite.set(new_entity, .transform, Mat4x4.translate(new_pos).mul(&Mat4x4.scaleScalar(0.3))); try sprite.set(new_entity, .size, vec2(@floatFromInt(r.width), @floatFromInt(r.height))); try sprite.set(new_entity, .uv_transform, Mat3x3.translate(vec2(@floatFromInt(r.x), @floatFromInt(r.y)))); @@ -168,7 +160,7 @@ fn tick( const delta_time = game.state().timer.lap(); // Animate entities - var archetypes_iter = engine.entities.query(.{ .all = &.{ + var archetypes_iter = core.entities.query(.{ .all = &.{ .{ .mach_gfx_sprite = &.{.transform} }, } }); while (archetypes_iter.next()) |archetype| { @@ -176,8 +168,9 @@ fn tick( const transforms = archetype.slice(.mach_gfx_sprite, .transform); for (ids, transforms) |id, *old_transform| { 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); + // TODO: formatting + if (location.x() < -@as(f32, @floatFromInt(mach.core.size().width)) / 1.5 or location.x() > @as(f32, @floatFromInt(mach.core.size().width)) / 1.5 or location.y() < -@as(f32, @floatFromInt(mach.core.size().height)) / 1.5 or location.y() > @as(f32, @floatFromInt(mach.core.size().height)) / 1.5) { + try core.entities.remove(id); game.state().sprites -= 1; continue; } @@ -208,18 +201,53 @@ fn tick( // Perform pre-render work sprite_pipeline.send(.pre_render, .{}); - // Render a frame - engine.send(.begin_pass, .{gpu.Color{ .r = 1.0, .g = 1.0, .b = 1.0, .a = 1.0 }}); + // Create a command encoder for this frame + game.state().frame_encoder = mach.core.device.createCommandEncoder(null); + + // Grab the back buffer of the swapchain + const back_buffer_view = mach.core.swap_chain.getCurrentTextureView().?; + defer back_buffer_view.release(); + + // Begin render pass + const sky_blue = gpu.Color{ .r = 0.776, .g = 0.988, .b = 1, .a = 1 }; + const color_attachments = [_]gpu.RenderPassColorAttachment{.{ + .view = back_buffer_view, + .clear_value = sky_blue, + .load_op = .clear, + .store_op = .store, + }}; + game.state().frame_encoder = mach.core.device.createCommandEncoder(null); + game.state().frame_render_pass = game.state().frame_encoder.beginRenderPass(&gpu.RenderPassDescriptor.init(.{ + .label = "main render pass", + .color_attachments = &color_attachments, + })); + + // Render our sprite batch + sprite_pipeline.state().render_pass = game.state().frame_render_pass; sprite_pipeline.send(.render, .{}); - engine.send(.end_pass, .{}); - engine.send(.frame_done, .{}); // Present the frame + + // Finish the frame once rendering is done. + game.send(.end_frame, .{}); + + game.state().time += delta_time; +} + +fn endFrame(game: *Mod) !void { + // Finish render pass + game.state().frame_render_pass.end(); + var command = game.state().frame_encoder.finish(null); + game.state().frame_encoder.release(); + defer command.release(); + mach.core.queue.submit(&[_]*gpu.CommandBuffer{command}); + + // Present the frame + mach.core.swap_chain.present(); // 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 }); + try mach.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; } diff --git a/examples/glyphs/Glyphs.zig b/examples/glyphs/Glyphs.zig index 23821d70..5f58c7d8 100644 --- a/examples/glyphs/Glyphs.zig +++ b/examples/glyphs/Glyphs.zig @@ -38,10 +38,10 @@ fn deinit(glyphs: *Mod) !void { } fn init( - engine: *mach.Engine.Mod, + core: *mach.Core.Mod, glyphs: *Mod, ) !void { - const device = engine.state().device; + const device = core.state().device; const allocator = gpa.allocator(); // rgba32_pixels @@ -77,11 +77,11 @@ fn init( } fn prepare( - engine: *mach.Engine.Mod, + core: *mach.Core.Mod, glyphs: *Mod, codepoints: []const u21, ) !void { - const device = engine.state().device; + const device = core.state().device; const queue = device.getQueue(); var s = glyphs.state(); diff --git a/examples/glyphs/main.zig b/examples/glyphs/main.zig index 93e9e049..4638e141 100644 --- a/examples/glyphs/main.zig +++ b/examples/glyphs/main.zig @@ -1,16 +1,19 @@ -// TODO(important): review all code in this file in-depth - -// Experimental ECS app example. Not yet ready for actual use. const mach = @import("mach"); -// The list of modules to be used in our application. Our game itself is implemented in our own -// module called Game. +// The global list of Mach modules registered for use in our application. pub const modules = .{ - mach.Engine, + mach.Core, mach.gfx.Sprite, mach.gfx.SpritePipeline, @import("Glyphs.zig"), @import("Game.zig"), }; -pub const App = mach.App; +// TODO(important): use standard entrypoint instead +pub fn main() !void { + // Initialize mach.Core + try mach.core.initModule(); + + // Main loop + while (try mach.core.tick()) {} +} diff --git a/examples/sprite/Game.zig b/examples/sprite/Game.zig index 3da9b99f..7ab4d40c 100644 --- a/examples/sprite/Game.zig +++ b/examples/sprite/Game.zig @@ -3,7 +3,6 @@ const std = @import("std"); const zigimg = @import("zigimg"); const assets = @import("assets"); const mach = @import("mach"); -const core = mach.core; const gpu = mach.gpu; const gfx = mach.gfx; const math = mach.math; @@ -29,19 +28,11 @@ rand: std.rand.DefaultPrng, time: f32, allocator: std.mem.Allocator, pipeline: mach.EntityID, +frame_encoder: *gpu.CommandEncoder = undefined, +frame_render_pass: *gpu.RenderPassEncoder = undefined, -const d0 = 0.000001; - -// 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: -// -// 1. `.mach` and the `.mach_foobar` namespace is reserved for Mach itself and the modules it -// provides. -// 2. Single-word names like `.game` are reserved for the application itself. -// 3. Libraries which provide modules MUST be prefixed with an "owner" name, e.g. `.ziglibs_imgui` -// instead of `.imgui`. We encourage using e.g. your GitHub name, as these must be globally -// unique. -// +// Define the globally unique name of our module. You can use any name here, but keep in mind no +// two modules in the program can have the same name. pub const name = .game; pub const Mod = mach.Mod(@This()); @@ -50,14 +41,18 @@ pub const global_events = .{ .tick = .{ .handler = tick }, }; +pub const local_events = .{ + .end_frame = .{ .handler = endFrame }, +}; + fn init( - engine: *mach.Engine.Mod, + core: *mach.Core.Mod, sprite: *gfx.Sprite.Mod, sprite_pipeline: *gfx.SpritePipeline.Mod, game: *Mod, ) !void { // The Mach .core is where we set window options, etc. - core.setTitle("gfx.Sprite example"); + 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 @@ -65,12 +60,12 @@ fn init( // Create a sprite rendering pipeline const allocator = gpa.allocator(); - const pipeline = try engine.newEntity(); - try sprite_pipeline.set(pipeline, .texture, try loadTexture(engine, allocator)); + const pipeline = try core.newEntity(); + try sprite_pipeline.set(pipeline, .texture, try loadTexture(core, allocator)); sprite_pipeline.send(.update, .{}); // Create our player sprite - const player = try engine.newEntity(); + const player = try core.newEntity(); try sprite.set(player, .transform, Mat4x4.translate(vec3(-0.02, 0, 0))); try sprite.set(player, .size, vec2(32, 32)); try sprite.set(player, .uv_transform, Mat3x3.translate(vec2(0, 0))); @@ -92,13 +87,13 @@ fn init( } fn tick( - engine: *mach.Engine.Mod, + core: *mach.Core.Mod, sprite: *gfx.Sprite.Mod, sprite_pipeline: *gfx.SpritePipeline.Mod, game: *Mod, ) !void { - // TODO(engine): event polling should occur in mach.Engine module and get fired as ECS events. - var iter = core.pollEvents(); + // TODO(important): event polling should occur in mach.Core module and get fired as ECS events. + var iter = mach.core.pollEvents(); var direction = game.state().direction; var spawning = game.state().spawning; while (iter.next()) |event| { @@ -123,7 +118,7 @@ fn tick( else => {}, } }, - .close => engine.send(.exit, .{}), + .close => core.send(.exit, .{}), else => {}, } } @@ -140,7 +135,7 @@ fn tick( 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(); + const new_entity = try core.newEntity(); try sprite.set(new_entity, .transform, Mat4x4.translate(new_pos).mul(&Mat4x4.scale(Vec3.splat(0.3)))); try sprite.set(new_entity, .size, vec2(32, 32)); try sprite.set(new_entity, .uv_transform, Mat3x3.translate(vec2(0, 0))); @@ -153,7 +148,7 @@ fn tick( const delta_time = game.state().timer.lap(); // Rotate entities - var archetypes_iter = engine.entities.query(.{ .all = &.{ + var archetypes_iter = core.entities.query(.{ .all = &.{ .{ .mach_gfx_sprite = &.{.transform} }, } }); while (archetypes_iter.next()) |archetype| { @@ -187,26 +182,60 @@ fn tick( // Perform pre-render work sprite_pipeline.send(.pre_render, .{}); - // Render a frame - engine.send(.begin_pass, .{gpu.Color{ .r = 1.0, .g = 1.0, .b = 1.0, .a = 1.0 }}); + // Create a command encoder for this frame + game.state().frame_encoder = mach.core.device.createCommandEncoder(null); + // Grab the back buffer of the swapchain + const back_buffer_view = mach.core.swap_chain.getCurrentTextureView().?; + defer back_buffer_view.release(); + + // Begin render pass + const sky_blue = gpu.Color{ .r = 0.776, .g = 0.988, .b = 1, .a = 1 }; + const color_attachments = [_]gpu.RenderPassColorAttachment{.{ + .view = back_buffer_view, + .clear_value = sky_blue, + .load_op = .clear, + .store_op = .store, + }}; + game.state().frame_encoder = mach.core.device.createCommandEncoder(null); + game.state().frame_render_pass = game.state().frame_encoder.beginRenderPass(&gpu.RenderPassDescriptor.init(.{ + .label = "main render pass", + .color_attachments = &color_attachments, + })); + + // Render our sprite batch + sprite_pipeline.state().render_pass = game.state().frame_render_pass; sprite_pipeline.send(.render, .{}); - engine.send(.end_pass, .{}); - engine.send(.frame_done, .{}); // Present the frame + + // Finish the frame once rendering is done. + game.send(.end_frame, .{}); + + game.state().time += delta_time; +} + +fn endFrame(game: *Mod) !void { + // Finish render pass + game.state().frame_render_pass.end(); + var command = game.state().frame_encoder.finish(null); + game.state().frame_encoder.release(); + defer command.release(); + mach.core.queue.submit(&[_]*gpu.CommandBuffer{command}); + + // Present the frame + mach.core.swap_chain.present(); // 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 }); + try mach.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; } // TODO: move this helper into gfx module -fn loadTexture(engine: *mach.Engine.Mod, allocator: std.mem.Allocator) !*gpu.Texture { - const device = engine.state().device; +fn loadTexture(core: *mach.Core.Mod, allocator: std.mem.Allocator) !*gpu.Texture { + const device = core.state().device; const queue = device.getQueue(); // Load the image from memory diff --git a/examples/sprite/main.zig b/examples/sprite/main.zig index b858ceee..8dfdd6cb 100644 --- a/examples/sprite/main.zig +++ b/examples/sprite/main.zig @@ -1,17 +1,18 @@ -// TODO(important): review all code in this file in-depth - -// Experimental ECS app example. Not yet ready for actual use. const mach = @import("mach"); -const Game = @import("Game.zig"); - -// The list of modules to be used in our application. Our game itself is implemented in our own -// module called Game. +// The global list of Mach modules registered for use in our application. pub const modules = .{ - mach.Engine, + mach.Core, mach.gfx.Sprite, mach.gfx.SpritePipeline, - Game, + @import("Game.zig"), }; -pub const App = mach.App; +// TODO(important): use standard entrypoint instead +pub fn main() !void { + // Initialize mach.Core + try mach.core.initModule(); + + // Main loop + while (try mach.core.tick()) {} +} diff --git a/examples/text/Game.zig b/examples/text/Game.zig index d5110412..d90dc3f2 100644 --- a/examples/text/Game.zig +++ b/examples/text/Game.zig @@ -3,10 +3,8 @@ const std = @import("std"); const zigimg = @import("zigimg"); const assets = @import("assets"); const mach = @import("mach"); -const core = mach.core; const gfx = mach.gfx; const gpu = mach.gpu; -const Text = mach.gfx.Text; const math = mach.math; const vec2 = math.vec2; @@ -26,35 +24,26 @@ spawning: bool = false, spawn_timer: mach.Timer, fps_timer: mach.Timer, frame_count: usize, -texts: usize, rand: std.rand.DefaultPrng, time: f32, style1: mach.EntityID, allocator: std.mem.Allocator, +pipeline: mach.EntityID, +frame_encoder: *gpu.CommandEncoder = undefined, +frame_render_pass: *gpu.RenderPassEncoder = undefined, -const d0 = 0.000001; - -// 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: -// -// 1. `.mach` and the `.mach_foobar` namespace is reserved for Mach itself and the modules it -// provides. -// 2. Single-word names like `.game` are reserved for the application itself. -// 3. Libraries which provide modules MUST be prefixed with an "owner" name, e.g. `.ziglibs_imgui` -// instead of `.imgui`. We encourage using e.g. your GitHub name, as these must be globally -// unique. -// +// Define the globally unique name of our module. You can use any name here, but keep in mind no +// two modules in the program can have the same name. pub const name = .game; pub const Mod = mach.Mod(@This()); pub const global_events = .{ .init = .{ .handler = init }, - .deinit = .{ .handler = deinit }, .tick = .{ .handler = tick }, }; -pub const Pipeline = enum(u32) { - default, +pub const local_events = .{ + .end_frame = .{ .handler = endFrame }, }; const upscale = 1.0; @@ -65,42 +54,49 @@ const text1: []const []const u8 = &.{ "bold\nand\n", }; -const text2: []const []const u8 = &.{"!$?😊"}; +const text2: []const []const u8 = &.{"$!?😊"}; fn init( - engine: *mach.Engine.Mod, - text: *Text.Mod, + core: *mach.Core.Mod, + text: *gfx.Text.Mod, + text_pipeline: *gfx.TextPipeline.Mod, text_style: *gfx.TextStyle.Mod, game: *Mod, ) !void { // The Mach .core is where we set window options, etc. - core.setTitle("gfx.Text example"); + mach.core.setTitle("gfx.Text example"); // TODO: a better way to initialize entities with default values - const style1 = try engine.newEntity(); + // TODO(text): most of these style options are not respected yet. + const style1 = try core.newEntity(); try text_style.set(style1, .font_name, "Roboto Medium"); // TODO try text_style.set(style1, .font_size, 48 * gfx.px_per_pt); // 48pt try text_style.set(style1, .font_weight, gfx.font_weight_normal); try text_style.set(style1, .italic, false); try text_style.set(style1, .color, vec4(0.6, 1.0, 0.6, 1.0)); - const style2 = try engine.newEntity(); + const style2 = try core.newEntity(); try text_style.set(style2, .font_name, "Roboto Medium"); // TODO try text_style.set(style2, .font_size, 48 * gfx.px_per_pt); // 48pt try text_style.set(style2, .font_weight, gfx.font_weight_normal); try text_style.set(style2, .italic, true); try text_style.set(style2, .color, vec4(0.6, 1.0, 0.6, 1.0)); - const style3 = try engine.newEntity(); + const style3 = try core.newEntity(); try text_style.set(style3, .font_name, "Roboto Medium"); // TODO try text_style.set(style3, .font_size, 48 * gfx.px_per_pt); // 48pt try text_style.set(style3, .font_weight, gfx.font_weight_bold); try text_style.set(style3, .italic, false); try text_style.set(style3, .color, vec4(0.6, 1.0, 0.6, 1.0)); + // Create a text rendering pipeline + const pipeline = try core.newEntity(); + try text_pipeline.set(pipeline, .is_pipeline, {}); + text_pipeline.send(.update, .{}); + // Create some text - const player = try engine.newEntity(); - try text.set(player, .pipeline, @intFromEnum(Pipeline.default)); + const player = try core.newEntity(); + try text.set(player, .pipeline, pipeline); try text.set(player, .transform, Mat4x4.scaleScalar(upscale).mul(&Mat4x4.translate(vec3(0, 0, 0)))); // TODO: better storage mechanism for this @@ -112,10 +108,7 @@ fn init( styles[2] = style3; try text.set(player, .text, text1); try text.set(player, .style, styles); - - text.send(.init_pipeline, .{Text.PipelineOptions{ - .pipeline = @intFromEnum(Pipeline.default), - }}); + try text.set(player, .dirty, true); game.init(.{ .timer = try mach.Timer.start(), @@ -123,25 +116,22 @@ fn init( .player = player, .fps_timer = try mach.Timer.start(), .frame_count = 0, - .texts = 0, .rand = std.rand.DefaultPrng.init(1337), .time = 0, .style1 = style1, .allocator = allocator, + .pipeline = pipeline, }); } -fn deinit(engine: *mach.Engine.Mod) !void { - _ = engine; -} - fn tick( - engine: *mach.Engine.Mod, - text: *Text.Mod, + core: *mach.Core.Mod, + text: *gfx.Text.Mod, + text_pipeline: *gfx.TextPipeline.Mod, game: *Mod, ) !void { - // TODO(engine): event polling should occur in mach.Engine module and get fired as ECS events. - var iter = core.pollEvents(); + // TODO(important): event polling should occur in mach.Core module and get fired as ECS events. + var iter = mach.core.pollEvents(); var direction = game.state().direction; var spawning = game.state().spawning; while (iter.next()) |event| { @@ -166,7 +156,7 @@ fn tick( else => {}, } }, - .close => engine.send(.exit, .{}), + .close => core.send(.exit, .{}), else => {}, } } @@ -175,16 +165,16 @@ fn tick( var player_transform = text.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(); - for (0..1) |_| { + for (0..10) |_| { 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) * 50; + new_pos.v[1] += game.state().rand.random().floatNorm(f32) * 50; - const new_entity = try engine.newEntity(); - try text.set(new_entity, .pipeline, @intFromEnum(Pipeline.default)); + const new_entity = try core.newEntity(); + try text.set(new_entity, .pipeline, game.state().pipeline); try text.set(new_entity, .transform, Mat4x4.scaleScalar(upscale).mul(&Mat4x4.translate(new_pos))); // TODO: better storage mechanism for this @@ -193,8 +183,7 @@ fn tick( styles[0] = game.state().style1; try text.set(new_entity, .text, text2); try text.set(new_entity, .style, styles); - - game.state().texts += 1; + try text.set(new_entity, .dirty, true); } } @@ -202,7 +191,7 @@ fn tick( const delta_time = game.state().timer.lap(); // Rotate entities - var archetypes_iter = engine.entities.query(.{ .all = &.{ + var archetypes_iter = core.entities.query(.{ .all = &.{ .{ .mach_gfx_text = &.{.transform} }, } }); while (archetypes_iter.next()) |archetype| { @@ -231,23 +220,75 @@ fn tick( player_pos.v[0] += direction.x() * speed * delta_time; player_pos.v[1] += direction.y() * speed * delta_time; try text.set(game.state().player, .transform, Mat4x4.scaleScalar(upscale).mul(&Mat4x4.translate(player_pos))); - text.send(.updated, .{@intFromEnum(Pipeline.default)}); + try text.set(game.state().player, .dirty, true); + text.send(.update, .{}); // Perform pre-render work - text.send(.pre_render, .{@intFromEnum(Pipeline.default)}); + text_pipeline.send(.pre_render, .{}); - // Render a frame - engine.send(.begin_pass, .{gpu.Color{ .r = 1.0, .g = 1.0, .b = 1.0, .a = 1.0 }}); - text.send(.render, .{@intFromEnum(Pipeline.default)}); - engine.send(.end_pass, .{}); - engine.send(.frame_done, .{}); // Present the frame + // Create a command encoder for this frame + game.state().frame_encoder = mach.core.device.createCommandEncoder(null); + + // Grab the back buffer of the swapchain + const back_buffer_view = mach.core.swap_chain.getCurrentTextureView().?; + defer back_buffer_view.release(); + + // Begin render pass + const sky_blue = gpu.Color{ .r = 0.776, .g = 0.988, .b = 1, .a = 1 }; + const color_attachments = [_]gpu.RenderPassColorAttachment{.{ + .view = back_buffer_view, + .clear_value = sky_blue, + .load_op = .clear, + .store_op = .store, + }}; + game.state().frame_encoder = mach.core.device.createCommandEncoder(null); + game.state().frame_render_pass = game.state().frame_encoder.beginRenderPass(&gpu.RenderPassDescriptor.init(.{ + .label = "main render pass", + .color_attachments = &color_attachments, + })); + + // Render our text batch + text_pipeline.state().render_pass = game.state().frame_render_pass; + text_pipeline.send(.render, .{}); + + // Finish the frame once rendering is done. + game.send(.end_frame, .{}); + + game.state().time += delta_time; +} + +fn endFrame(game: *Mod, text: *gfx.Text.Mod) !void { + // Finish render pass + game.state().frame_render_pass.end(); + var command = game.state().frame_encoder.finish(null); + game.state().frame_encoder.release(); + defer command.release(); + mach.core.queue.submit(&[_]*gpu.CommandBuffer{command}); + + // Present the frame + mach.core.swap_chain.present(); // 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 }); + // Gather some text rendering stats + var num_texts: u32 = 0; + var num_glyphs: usize = 0; + var archetypes_iter = text.entities.query(.{ .all = &.{ + .{ .mach_gfx_text = &.{ + .built, + } }, + } }); + while (archetypes_iter.next()) |archetype| { + const builts = archetype.slice(.mach_gfx_text, .built); + for (builts) |built| { + num_texts += 1; + num_glyphs += built.glyphs.items.len; + } + } + + try mach.core.printTitle("gfx.Text example [ FPS: {d} ] [ Texts: {d} ] [ Glyphs: {d} ]", .{ game.state().frame_count, num_texts, num_glyphs }); game.state().fps_timer.reset(); game.state().frame_count = 0; } game.state().frame_count += 1; - game.state().time += delta_time; } diff --git a/examples/text/main.zig b/examples/text/main.zig index 145f0500..12e5676a 100644 --- a/examples/text/main.zig +++ b/examples/text/main.zig @@ -1,17 +1,19 @@ -// TODO(important): review all code in this file in-depth - -// Experimental ECS app example. Not yet ready for actual use. const mach = @import("mach"); -const Game = @import("Game.zig"); - -// The list of modules to be used in our application. Our game itself is implemented in our own -// module called Game. +// The global list of Mach modules registered for use in our application. pub const modules = .{ - mach.Engine, + mach.Core, mach.gfx.Text, + mach.gfx.TextPipeline, mach.gfx.TextStyle, - Game, + @import("Game.zig"), }; -pub const App = mach.App; +// TODO(important): use standard entrypoint instead +pub fn main() !void { + // Initialize mach.Core + try mach.core.initModule(); + + // Main loop + while (try mach.core.tick()) {} +} diff --git a/src/gfx/Sprite.zig b/src/gfx/Sprite.zig index cfdf5103..5086d5bb 100644 --- a/src/gfx/Sprite.zig +++ b/src/gfx/Sprite.zig @@ -1,7 +1,6 @@ const std = @import("std"); const mach = @import("../main.zig"); -const core = mach.core; -const gpu = mach.core.gpu; +const gpu = mach.gpu; const gfx = mach.gfx; const Engine = mach.Engine; @@ -44,7 +43,7 @@ pub const local_events = .{ .update = .{ .handler = update }, }; -fn update(engine: *Engine.Mod, sprite: *Mod, sprite_pipeline: *gfx.SpritePipeline.Mod) !void { +fn update(core: *mach.Core.Mod, sprite: *Mod, sprite_pipeline: *gfx.SpritePipeline.Mod) !void { var archetypes_iter = sprite_pipeline.entities.query(.{ .all = &.{ .{ .mach_gfx_sprite_pipeline = &.{ .built, @@ -54,19 +53,19 @@ fn update(engine: *Engine.Mod, sprite: *Mod, sprite_pipeline: *gfx.SpritePipelin const ids = archetype.slice(.entity, .id); const built_pipelines = archetype.slice(.mach_gfx_sprite_pipeline, .built); for (ids, built_pipelines) |pipeline_id, *built| { - try updatePipeline(engine, sprite, sprite_pipeline, pipeline_id, built); + try updatePipeline(core, sprite, sprite_pipeline, pipeline_id, built); } } } fn updatePipeline( - engine: *Engine.Mod, + core: *mach.Core.Mod, sprite: *Mod, sprite_pipeline: *gfx.SpritePipeline.Mod, pipeline_id: mach.EntityID, built: *gfx.SpritePipeline.BuiltPipeline, ) !void { - const device = engine.state().device; + const device = core.state().device; const encoder = device.createCommandEncoder(null); defer encoder.release(); @@ -110,6 +109,6 @@ fn updatePipeline( encoder.writeBuffer(built.sizes, 0, gfx.SpritePipeline.cp_sizes[0..i]); var command = encoder.finish(null); defer command.release(); - engine.state().queue.submit(&[_]*gpu.CommandBuffer{command}); + core.state().queue.submit(&[_]*gpu.CommandBuffer{command}); } } diff --git a/src/gfx/SpritePipeline.zig b/src/gfx/SpritePipeline.zig index 2c4526a7..06c29d6e 100644 --- a/src/gfx/SpritePipeline.zig +++ b/src/gfx/SpritePipeline.zig @@ -1,6 +1,5 @@ const std = @import("std"); const mach = @import("../main.zig"); -const core = mach.core; const gpu = mach.gpu; const math = mach.math; @@ -89,6 +88,9 @@ pub var cp_transforms: [sprite_buffer_cap]math.Mat4x4 = undefined; pub var cp_uv_transforms: [sprite_buffer_cap]math.Mat3x3 = undefined; pub var cp_sizes: [sprite_buffer_cap]math.Vec2 = undefined; +/// Which render pass should be used during .render +render_pass: ?*gpu.RenderPassEncoder = null, + pub const BuiltPipeline = struct { render: *gpu.RenderPipeline, texture_sampler: *gpu.Sampler, @@ -144,7 +146,9 @@ fn deinit(sprite_pipeline: *Mod) void { } } -fn update(engine: *mach.Engine.Mod, sprite_pipeline: *Mod) !void { +fn update(core: *mach.Core.Mod, sprite_pipeline: *Mod) !void { + sprite_pipeline.init(.{}); + // Destroy all sprite render pipelines. We will rebuild them all. deinit(sprite_pipeline); @@ -158,13 +162,13 @@ fn update(engine: *mach.Engine.Mod, sprite_pipeline: *Mod) !void { const textures = archetype.slice(.mach_gfx_sprite_pipeline, .texture); for (ids, textures) |pipeline_id, texture| { - try buildPipeline(engine, sprite_pipeline, pipeline_id, texture); + try buildPipeline(core, sprite_pipeline, pipeline_id, texture); } } } fn buildPipeline( - engine: *mach.Engine.Mod, + core: *mach.Core.Mod, sprite_pipeline: *Mod, pipeline_id: mach.EntityID, texture: *gpu.Texture, @@ -181,7 +185,7 @@ fn buildPipeline( const opt_fragment_state = sprite_pipeline.get(pipeline_id, .fragment_state); const opt_layout = sprite_pipeline.get(pipeline_id, .layout); - const device = engine.state().device; + const device = core.state().device; // Storage buffers const transforms = device.createBuffer(&.{ @@ -269,7 +273,7 @@ fn buildPipeline( defer shader_module.release(); const color_target = opt_color_target_state orelse gpu.ColorTargetState{ - .format = core.descriptor.format, + .format = mach.core.descriptor.format, .blend = &blend_state, .write_mask = gpu.ColorWriteMaskFlags.all, }; @@ -311,10 +315,12 @@ fn buildPipeline( try sprite_pipeline.set(pipeline_id, .num_sprites, 0); } -fn preRender( - engine: *mach.Engine.Mod, - sprite_pipeline: *Mod, -) void { +fn preRender(sprite_pipeline: *Mod) void { + const encoder = mach.core.device.createCommandEncoder(&gpu.CommandEncoder.Descriptor{ + .label = "SpritePipeline.encoder", + }); + defer encoder.release(); + var archetypes_iter = sprite_pipeline.entities.query(.{ .all = &.{ .{ .mach_gfx_sprite_pipeline = &.{ .built, @@ -326,10 +332,10 @@ fn preRender( // Create the projection matrix // TODO(sprite): move this out of the hot codepath const proj = math.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, + .left = -@as(f32, @floatFromInt(mach.core.size().width)) / 2, + .right = @as(f32, @floatFromInt(mach.core.size().width)) / 2, + .bottom = -@as(f32, @floatFromInt(mach.core.size().height)) / 2, + .top = @as(f32, @floatFromInt(mach.core.size().height)) / 2, .near = -0.1, .far = 100000, }); @@ -343,15 +349,20 @@ fn preRender( @as(f32, @floatFromInt(built.texture.getHeight())), ), }; - engine.state().encoder.writeBuffer(built.uniforms, 0, &[_]Uniforms{uniforms}); + encoder.writeBuffer(built.uniforms, 0, &[_]Uniforms{uniforms}); } } + + var command = encoder.finish(null); + defer command.release(); + mach.core.queue.submit(&[_]*gpu.CommandBuffer{command}); } -fn render( - engine: *mach.Engine.Mod, - sprite_pipeline: *Mod, -) !void { +fn render(sprite_pipeline: *Mod) !void { + const render_pass = if (sprite_pipeline.state().render_pass) |rp| rp else std.debug.panic("sprite_pipeline.state().render_pass must be specified", .{}); + sprite_pipeline.state().render_pass = null; + + // TODO(sprite): need a way to specify order of rendering with multiple pipelines var archetypes_iter = sprite_pipeline.entities.query(.{ .all = &.{ .{ .mach_gfx_sprite_pipeline = &.{ .built, @@ -362,12 +373,11 @@ fn render( const built_pipelines = archetype.slice(.mach_gfx_sprite_pipeline, .built); for (ids, built_pipelines) |pipeline_id, built| { // Draw the sprite batch - const pass = engine.state().pass; const total_vertices = sprite_pipeline.get(pipeline_id, .num_sprites).? * 6; - pass.setPipeline(built.render); + render_pass.setPipeline(built.render); // TODO(sprite): remove dynamic offsets? - pass.setBindGroup(0, built.bind_group, &.{}); - pass.draw(total_vertices, 1, 0, 0); + render_pass.setBindGroup(0, built.bind_group, &.{}); + render_pass.draw(total_vertices, 1, 0, 0); } } } diff --git a/src/gfx/Text.zig b/src/gfx/Text.zig index 1995ec46..aeeca036 100644 --- a/src/gfx/Text.zig +++ b/src/gfx/Text.zig @@ -1,6 +1,5 @@ const std = @import("std"); const mach = @import("../main.zig"); -const core = mach.core; const gpu = mach.gpu; const Engine = mach.Engine; const gfx = mach.gfx; @@ -14,22 +13,10 @@ const vec4 = math.vec4; const Mat3x3 = math.Mat3x3; const Mat4x4 = math.Mat4x4; -var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - -/// Internal state -pipelines: std.AutoArrayHashMapUnmanaged(u32, Pipeline) = .{}, -allocator: std.mem.Allocator, - pub const name = .mach_gfx_text; pub const Mod = mach.Mod(@This()); pub const components = .{ - .pipeline = .{ .type = u8, .description = - \\ The ID of the pipeline this text belongs to. By default, zero. - \\ - \\ This determines which shader, textures, etc. are used for rendering the text. - }, - .transform = .{ .type = Mat4x4, .description = \\ The text model transformation matrix. Text is measured in pixel units, starting from \\ (0, 0) at the top-left corner and extending to the size of the text. By default, the world @@ -47,370 +34,139 @@ pub const components = .{ \\ \\ Expected to match the length of the text component. }, -}; -pub const global_events = .{ - .deinit = .{ .handler = deinit }, - .init = .{ .handler = init }, + .dirty = .{ .type = bool, .description = + \\ If true, the underlying glyph buffers, texture atlas, and transform buffers will be updated + \\ as needed to reflect the latest component values. + \\ + \\ This lets rendering be static if no changes have occurred. + }, + + .pipeline = .{ .type = mach.EntityID, .description = + \\ Which render pipeline to use for rendering the text. + \\ + \\ This determines which shader, textures, etc. are used for rendering the text. + }, + + .built = .{ .type = BuiltText, .description = "internal" }, }; pub const local_events = .{ - .init_pipeline = .{ .handler = initPipeline }, - .updated = .{ .handler = updated }, - .pre_render = .{ .handler = preRender }, - .render = .{ .handler = render }, + .update = .{ .handler = update }, }; -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 - - /// The view * orthographic projection matrix - view_projection: Mat4x4 align(16), - - /// Total size of the font atlas texture in pixels - texture_size: Vec2 align(16), +const BuiltText = struct { + glyphs: std.ArrayListUnmanaged(gfx.TextPipeline.Glyph), }; -const Glyph = extern struct { - /// Position of this glyph (top-left corner.) - pos: Vec2, - - /// Width of the glyph in pixels. - size: Vec2, - - /// Normalized position of the top-left UV coordinate - uv_pos: Vec2, - - /// Which text this glyph belongs to; this is the index for transforms[i], colors[i]. - text_index: u32, -}; - -const GlyphKey = struct { - index: u32, - // Auto Hashing doesn't work for floats, so we bitcast to integer. - size: u32, -}; -const RegionMap = std.AutoArrayHashMapUnmanaged(GlyphKey, gfx.Atlas.Region); - -const Pipeline = struct { - render: *gpu.RenderPipeline, - texture_sampler: *gpu.Sampler, - texture: *gpu.Texture, - texture_atlas: gfx.Atlas, - texture2: ?*gpu.Texture, - texture3: ?*gpu.Texture, - texture4: ?*gpu.Texture, - bind_group: *gpu.BindGroup, - uniforms: *gpu.Buffer, - regions: RegionMap = .{}, - - // Storage buffers - num_texts: u32, - num_glyphs: u32, - transforms: *gpu.Buffer, - colors: *gpu.Buffer, - glyphs: *gpu.Buffer, - - pub fn reference(p: *Pipeline) void { - p.render.reference(); - p.texture_sampler.reference(); - p.texture.reference(); - if (p.texture2) |tex| tex.reference(); - if (p.texture3) |tex| tex.reference(); - if (p.texture4) |tex| tex.reference(); - p.bind_group.reference(); - p.uniforms.reference(); - p.transforms.reference(); - p.colors.reference(); - p.glyphs.reference(); - } - - pub fn deinit(p: *Pipeline, allocator: std.mem.Allocator) void { - p.render.release(); - p.texture_sampler.release(); - p.texture.release(); - p.texture_atlas.deinit(allocator); - if (p.texture2) |tex| tex.release(); - if (p.texture3) |tex| tex.release(); - if (p.texture4) |tex| tex.release(); - p.bind_group.release(); - p.uniforms.release(); - p.regions.deinit(allocator); - p.transforms.release(); - p.colors.release(); - p.glyphs.release(); - } -}; - -pub const PipelineOptions = struct { - pipeline: u32, - - /// Shader program to use when rendering. - shader: ?*gpu.ShaderModule = null, - - /// Whether to use linear (blurry) or nearest (pixelated) upscaling/downscaling. - texture_sampler: ?*gpu.Sampler = null, - - /// Textures to use when rendering. The default shader can handle one texture (the font atlas.) - texture2: ?*gpu.Texture = null, - texture3: ?*gpu.Texture = null, - texture4: ?*gpu.Texture = null, - - /// Alpha and color blending options. - blend_state: ?gpu.BlendState = null, - - /// Pipeline overrides, these can be used to e.g. pass additional things to your shader program. - bind_group_layout: ?*gpu.BindGroupLayout = null, - bind_group: ?*gpu.BindGroup = null, - color_target_state: ?gpu.ColorTargetState = null, - fragment_state: ?gpu.FragmentState = null, - pipeline_layout: ?*gpu.PipelineLayout = null, -}; - -fn deinit(text: *Mod) !void { - for (text.state().pipelines.entries.items(.value)) |*pipeline| pipeline.deinit(text.state().allocator); - text.state().pipelines.deinit(text.state().allocator); -} - -fn init(text: *Mod) void { - text.init(.{ - .allocator = gpa.allocator(), - }); -} - -// TODO(text): no args -fn initPipeline( - engine: *Engine.Mod, - text: *Mod, - opt: PipelineOptions, -) !void { - const device = engine.state().device; - - const pipeline = try text.state().pipelines.getOrPut(text.state().allocator, opt.pipeline); - if (pipeline.found_existing) { - pipeline.value_ptr.*.deinit(text.state().allocator); - } - - // 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( - text.state().allocator, - img_size.width, - .rgba, - ); - - // 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_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, - }, - .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 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 = "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(); -} - -// TODO(text): no args -fn updated( - engine: *Engine.Mod, - text: *Mod, - pipeline_id: u32, -) !void { - const pipeline = text.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, +fn update(core: *mach.Core.Mod, text: *Mod, text_pipeline: *gfx.TextPipeline.Mod) !void { + var archetypes_iter = text_pipeline.entities.query(.{ .all = &.{ + .{ .mach_gfx_text_pipeline = &.{ + .built, } }, } }); + while (archetypes_iter.next()) |archetype| { + const ids = archetype.slice(.entity, .id); + const built_pipelines = archetype.slice(.mach_gfx_text_pipeline, .built); + for (ids, built_pipelines) |pipeline_id, *built| { + try updatePipeline(core, text, text_pipeline, pipeline_id, built); + } + } +} +fn updatePipeline( + core: *mach.Core.Mod, + text: *Mod, + text_pipeline: *gfx.TextPipeline.Mod, + pipeline_id: mach.EntityID, + built: *gfx.TextPipeline.BuiltPipeline, +) !void { + const device = core.state().device; 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; + const allocator = text_pipeline.state().allocator; + var glyphs = if (text_pipeline.state().glyph_update_buffer) |*b| b else blk: { + // TODO(text): better default allocation size + const b = try std.ArrayListUnmanaged(gfx.TextPipeline.Glyph).initCapacity(allocator, 256); + text_pipeline.state().glyph_update_buffer = b; + break :blk &text_pipeline.state().glyph_update_buffer.?; + }; + glyphs.clearRetainingCapacity(); + var texture_update = false; + var num_texts: u32 = 0; + var removes = try std.ArrayListUnmanaged(mach.EntityID).initCapacity(allocator, 8); + var archetypes_iter = text.entities.query(.{ .all = &.{ + .{ .mach_gfx_text = &.{ + .transform, + .text, + .style, + .pipeline, + } }, + } }); while (archetypes_iter.next()) |archetype| { + const ids = archetype.slice(.entity, .id); const transforms = archetype.slice(.mach_gfx_text, .transform); + const segment_slices = archetype.slice(.mach_gfx_text, .text); + const style_slices = archetype.slice(.mach_gfx_text, .style); + const pipelines = archetype.slice(.mach_gfx_text, .pipeline); - // 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: currently we cannot query all texts which have a _single_ pipeline component + // value and get back contiguous memory for all of them. This is because all texts with + // possibly different pipeline component values are stored as the same archetype. If we + // introduce a new concept of tagging-by-value to our entity storage then we can enforce + // that all entities with the same pipeline value are stored in contiguous memory, and + // skip this copy. + for (ids, transforms, segment_slices, style_slices, pipelines) |id, transform, segments, styles, text_pipeline_id| { + if (text_pipeline_id != pipeline_id) continue; - transforms_offset += transforms.len; - // colors_offset += colors.len; - pipeline.num_texts += @intCast(transforms.len); + gfx.TextPipeline.cp_transforms[num_texts] = transform; - // 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| { + if (text.get(id, .dirty) == null) { + // We do not need to rebuild this specific entity, so use cached glyph information + // from its previous build. + const built_text = text.get(id, .built).?; + for (built_text.glyphs.items) |*glyph| glyph.text_index = num_texts; + try glyphs.appendSlice(allocator, built_text.glyphs.items); + num_texts += 1; + continue; + } + + // Where we will store the built glyphs for this text entity. + var built_text = if (text.get(id, .built)) |bt| bt else BuiltText{ + // TODO: better default allocations + .glyphs = try std.ArrayListUnmanaged(gfx.TextPipeline.Glyph).initCapacity(allocator, 64), + }; + built_text.glyphs.clearRetainingCapacity(); + + const px_density = 2.0; // TODO(text): do not hard-code pixel density 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_style, .font_name).?; + // Load the font + // TODO(text): allow specifying a font + // TODO(text): keep fonts around for reuse later + const font_name = core.entities.getComponent(style, .mach_gfx_text_style, .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(text.state().allocator); + defer font.deinit(allocator); - const font_size = engine.entities.getComponent(style, .mach_gfx_text_style, .font_size).?; - const font_weight = engine.entities.getComponent(style, .mach_gfx_text_style, .font_weight); - const italic = engine.entities.getComponent(style, .mach_gfx_text_style, .italic); - const color = engine.entities.getComponent(style, .mach_gfx_text_style, .color); - // TODO: actually apply these - _ = font_weight; - _ = italic; - _ = color; + // TODO(text): respect these style parameters + const font_size = core.entities.getComponent(style, .mach_gfx_text_style, .font_size).?; + const font_weight = core.entities.getComponent(style, .mach_gfx_text_style, .font_weight); + _ = font_weight; // autofix + const italic = core.entities.getComponent(style, .mach_gfx_text_style, .italic); + _ = italic; // autofix + const color = core.entities.getComponent(style, .mach_gfx_text_style, .color); + _ = color; // autofix // Create a text shaper var run = try gfx.TextRun.init(); run.font_size_px = font_size; - run.px_density = 2; // TODO - + run.px_density = px_density; defer run.deinit(); run.addText(segment); @@ -419,131 +175,106 @@ fn updated( 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(text.state().allocator, .{ - .index = glyph.glyph_index, - .size = @bitCast(font_size), - }); - if (!region.found_existing) { - const rendered_glyph = try font.render(text.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(text.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; - - // 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(text.state().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(); + continue; } + + const region = try built.regions.getOrPut(allocator, .{ + .index = glyph.glyph_index, + .size = @bitCast(font_size), + }); + if (!region.found_existing) { + const rendered_glyph = try font.render(allocator, glyph.glyph_index, .{ + .font_size_px = run.font_size_px, + }); + if (rendered_glyph.bitmap) |bitmap| { + var glyph_atlas_region = try built.texture_atlas.reserve(allocator, rendered_glyph.width, rendered_glyph.height); + built.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, + }; + } + } + + const r = region.value_ptr.*; + const size = vec2(@floatFromInt(r.width), @floatFromInt(r.height)); + try built_text.glyphs.append(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 = num_texts, + .uv_pos = vec2(@floatFromInt(r.x), @floatFromInt(r.y)), + }); + origin_x += glyph.advance.x(); } } + // Update the text entity's built form + try text.set(id, .built, built_text); + // TODO(text): see below + // try text.remove(id, .dirty); + try removes.append(allocator, id); + + // Add to the entire set of glyphs for this pipeline + try glyphs.appendSlice(allocator, built_text.glyphs.items); + num_texts += 1; + } + + // TODO(important): removing components within an iter() currently produces undefined behavior + // (entity may exist in current iteration, plus a future iteration as the iterator moves + // on to the next archetype where the entity is now located.) + for (removes.items) |remove_id| { + try text.remove(remove_id, .dirty); } } - // TODO: could writeBuffer check for zero? - if (glyphs.items.len > 0) encoder.writeBuffer(pipeline.glyphs, 0, glyphs.items); - defer glyphs.deinit(text.state().allocator); + // Every pipeline update, we copy updated glyph and text buffers to the GPU. + try text_pipeline.set(pipeline_id, .num_texts, num_texts); + try text_pipeline.set(pipeline_id, .num_glyphs, @intCast(glyphs.items.len)); + if (glyphs.items.len > 0) encoder.writeBuffer(built.glyphs, 0, glyphs.items); + if (num_texts > 0) { + encoder.writeBuffer(built.transforms, 0, gfx.TextPipeline.cp_transforms[0..num_texts]); + } + if (texture_update) { - // rgba32_pixels - // TODO: use proper texture dimensions here + // TODO(text): do not assume texture's data_layout and img_size here, instead get it from + // somewhere known to be matching the actual texture. + // + // TODO(text): allow users to specify RGBA32 or other pixel formats 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 }, + core.state().queue.writeTexture( + &.{ .texture = built.texture }, &data_layout, &img_size, - pipeline.texture_atlas.data, + built.texture_atlas.data, ); } - var command = encoder.finish(null); - defer command.release(); - - engine.state().queue.submit(&[_]*gpu.CommandBuffer{command}); -} - -// TODO(text): no args -fn preRender( - engine: *Engine.Mod, - text: *Mod, - pipeline_id: u32, -) !void { - const pipeline = text.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}); -} - -// TODO(text): no args -fn render( - engine: *Engine.Mod, - text: *Mod, - pipeline_id: u32, -) !void { - const pipeline = text.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); + if (num_texts > 0 or glyphs.items.len > 0) { + var command = encoder.finish(null); + defer command.release(); + core.state().queue.submit(&[_]*gpu.CommandBuffer{command}); + } } diff --git a/src/gfx/TextPipeline.zig b/src/gfx/TextPipeline.zig new file mode 100644 index 00000000..2a0b71d3 --- /dev/null +++ b/src/gfx/TextPipeline.zig @@ -0,0 +1,407 @@ +const std = @import("std"); +const mach = @import("../main.zig"); + +const gfx = mach.gfx; +const gpu = mach.gpu; +const math = mach.math; + +pub const name = .mach_gfx_text_pipeline; +pub const Mod = mach.Mod(@This()); + +pub const components = .{ + .is_pipeline = .{ .type = void, .description = + \\ Tag to indicate an entity represents a text pipeline. + }, + + .shader = .{ .type = *gpu.ShaderModule, .description = + \\ Shader program to use when rendering + \\ Defaults to text.wgsl + }, + + .texture_sampler = .{ .type = *gpu.Sampler, .description = + \\ Whether to use linear (blurry) or nearest (pixelated) upscaling/downscaling. + \\ Defaults to nearest (pixelated) + }, + + .blend_state = .{ .type = gpu.BlendState, .description = + \\ Alpha and color blending options + \\ Defaults to + \\ .{ + \\ .color = .{ .operation = .add, .src_factor = .src_alpha .dst_factor = .one_minus_src_alpha }, + \\ .alpha = .{ .operation = .add, .src_factor = .one, .dst_factor = .zero }, + \\ } + }, + + .bind_group_layout = .{ .type = *gpu.BindGroupLayout, .description = + \\ Override to enable passing additional data to your shader program. + }, + .bind_group = .{ .type = *gpu.BindGroup, .description = + \\ Override to enable passing additional data to your shader program. + }, + .color_target_state = .{ .type = gpu.ColorTargetState, .description = + \\ Override to enable custom color target state for render pipeline. + }, + .fragment_state = .{ .type = gpu.FragmentState, .description = + \\ Override to enable custom fragment state for render pipeline. + }, + .layout = .{ .type = *gpu.PipelineLayout, .description = + \\ Override to enable custom pipeline layout. + }, + + .num_texts = .{ .type = u32, .description = + \\ Number of texts this pipeline will render. + \\ Read-only, updated as part of Text.update + }, + .num_glyphs = .{ .type = u32, .description = + \\ Number of glyphs this pipeline will render. + \\ Read-only, updated as part of Text.update + }, + + .built = .{ .type = BuiltPipeline, .description = "internal" }, +}; + +pub const global_events = .{ + .deinit = .{ .handler = deinit }, +}; + +pub const local_events = .{ + .update = .{ .handler = update }, + .pre_render = .{ .handler = preRender }, + .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 + + /// The view * orthographic projection matrix + view_projection: math.Mat4x4 align(16), + + /// Total size of the font atlas texture in pixels + texture_size: math.Vec2 align(16), +}; + +const texts_buffer_cap = 1024 * 512; // TODO(text): allow user to specify preallocation + +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +// TODO(text): eliminate these, see Text.updatePipeline for details on why these exist +// currently. +pub var cp_transforms: [texts_buffer_cap]math.Mat4x4 = undefined; +pub var cp_colors: [texts_buffer_cap]math.Vec4 = undefined; +pub var cp_glyphs: [texts_buffer_cap]Glyph = undefined; + +/// Which render pass should be used during .render +render_pass: ?*gpu.RenderPassEncoder = null, + +glyph_update_buffer: ?std.ArrayListUnmanaged(Glyph) = null, +allocator: std.mem.Allocator, + +pub const Glyph = extern struct { + /// Position of this glyph (top-left corner.) + pos: math.Vec2, + + /// Width of the glyph in pixels. + size: math.Vec2, + + /// Normalized position of the top-left UV coordinate + uv_pos: math.Vec2, + + /// Which text this glyph belongs to; this is the index for transforms[i], colors[i]. + text_index: u32, +}; + +const GlyphKey = struct { + index: u32, + // Auto Hashing doesn't work for floats, so we bitcast to integer. + size: u32, +}; +const RegionMap = std.AutoArrayHashMapUnmanaged(GlyphKey, gfx.Atlas.Region); + +pub const BuiltPipeline = struct { + render: *gpu.RenderPipeline, + texture_sampler: *gpu.Sampler, + texture: *gpu.Texture, + bind_group: *gpu.BindGroup, + uniforms: *gpu.Buffer, + texture_atlas: gfx.Atlas, + regions: RegionMap = .{}, + + // Storage buffers + transforms: *gpu.Buffer, + colors: *gpu.Buffer, + glyphs: *gpu.Buffer, + + pub fn reference(p: *BuiltPipeline) void { + p.render.reference(); + p.texture_sampler.reference(); + p.texture.reference(); + p.bind_group.reference(); + p.uniforms.reference(); + p.transforms.reference(); + p.colors.reference(); + p.glyphs.reference(); + } + + pub fn deinit(p: *BuiltPipeline, allocator: std.mem.Allocator) void { + p.render.release(); + p.texture_sampler.release(); + p.texture.release(); + p.bind_group.release(); + p.uniforms.release(); + p.transforms.release(); + p.colors.release(); + p.glyphs.release(); + p.texture_atlas.deinit(allocator); + p.regions.deinit(allocator); + } +}; + +fn deinit(text_pipeline: *Mod) void { + var archetypes_iter = text_pipeline.entities.query(.{ .all = &.{ + .{ .mach_gfx_text_pipeline = &.{ + .built, + } }, + } }); + while (archetypes_iter.next()) |archetype| { + for (archetype.slice(.mach_gfx_text_pipeline, .built)) |*p| p.deinit(text_pipeline.state().allocator); + } +} + +fn update(core: *mach.Core.Mod, text_pipeline: *Mod) !void { + text_pipeline.init(.{ + .allocator = gpa.allocator(), + }); + + // Destroy all text render pipelines. We will rebuild them all. + deinit(text_pipeline); + + var archetypes_iter = text_pipeline.entities.query(.{ .all = &.{ + .{ .mach_gfx_text_pipeline = &.{ + .is_pipeline, + } }, + } }); + while (archetypes_iter.next()) |archetype| { + const ids = archetype.slice(.entity, .id); + for (ids) |pipeline_id| { + try buildPipeline(core, text_pipeline, pipeline_id); + } + } +} + +fn buildPipeline( + core: *mach.Core.Mod, + text_pipeline: *Mod, + pipeline_id: mach.EntityID, +) !void { + const opt_shader = text_pipeline.get(pipeline_id, .shader); + const opt_texture_sampler = text_pipeline.get(pipeline_id, .texture_sampler); + const opt_blend_state = text_pipeline.get(pipeline_id, .blend_state); + const opt_bind_group_layout = text_pipeline.get(pipeline_id, .bind_group_layout); + const opt_bind_group = text_pipeline.get(pipeline_id, .bind_group); + const opt_color_target_state = text_pipeline.get(pipeline_id, .color_target_state); + const opt_fragment_state = text_pipeline.get(pipeline_id, .fragment_state); + const opt_layout = text_pipeline.get(pipeline_id, .layout); + + const device = core.state().device; + + // Prepare texture for the font atlas. + // TODO(text): dynamic texture re-allocation when not large enough + // TODO(text): better default allocation size + 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( + text_pipeline.state().allocator, + img_size.width, + .rgba, + ); + + // Storage buffers + const transforms = device.createBuffer(&.{ + .usage = .{ .storage = true, .copy_dst = true }, + .size = @sizeOf(math.Mat4x4) * texts_buffer_cap, + .mapped_at_creation = .false, + }); + const colors = device.createBuffer(&.{ + .usage = .{ .storage = true, .copy_dst = true }, + .size = @sizeOf(math.Vec4) * texts_buffer_cap, + .mapped_at_creation = .false, + }); + const glyphs = device.createBuffer(&.{ + .usage = .{ .storage = true, .copy_dst = true }, + .size = @sizeOf(Glyph) * texts_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), + }, + }), + ); + defer bind_group_layout.release(); + + const texture_view = texture.createView(&gpu.TextureView.Descriptor{}); + defer texture_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(math.Mat4x4) * texts_buffer_cap), + gpu.BindGroup.Entry.buffer(2, colors, 0, @sizeOf(math.Vec4) * texts_buffer_cap), + gpu.BindGroup.Entry.buffer(3, glyphs, 0, @sizeOf(Glyph) * texts_buffer_cap), + gpu.BindGroup.Entry.sampler(4, texture_sampler), + gpu.BindGroup.Entry.textureView(5, texture_view), + }, + }), + ); + + 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 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 = mach.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_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", + }, + }); + + var built = BuiltPipeline{ + .render = render_pipeline, + .texture_sampler = texture_sampler, + .texture = texture, + .bind_group = bind_group, + .uniforms = uniforms, + .transforms = transforms, + .colors = colors, + .glyphs = glyphs, + .texture_atlas = texture_atlas, + }; + built.reference(); + try text_pipeline.set(pipeline_id, .built, built); + try text_pipeline.set(pipeline_id, .num_texts, 0); + try text_pipeline.set(pipeline_id, .num_glyphs, 0); +} + +fn preRender(text_pipeline: *Mod) void { + const encoder = mach.core.device.createCommandEncoder(&gpu.CommandEncoder.Descriptor{ + .label = "TextPipeline.encoder", + }); + defer encoder.release(); + + var archetypes_iter = text_pipeline.entities.query(.{ .all = &.{ + .{ .mach_gfx_text_pipeline = &.{ + .built, + } }, + } }); + while (archetypes_iter.next()) |archetype| { + const built_pipelines = archetype.slice(.mach_gfx_text_pipeline, .built); + for (built_pipelines) |built| { + // Create the projection matrix + // TODO(text): move this out of the hot codepath + const proj = math.Mat4x4.projection2D(.{ + .left = -@as(f32, @floatFromInt(mach.core.size().width)) / 2, + .right = @as(f32, @floatFromInt(mach.core.size().width)) / 2, + .bottom = -@as(f32, @floatFromInt(mach.core.size().height)) / 2, + .top = @as(f32, @floatFromInt(mach.core.size().height)) / 2, + .near = -0.1, + .far = 100000, + }); + + // Update uniform buffer + const uniforms = Uniforms{ + .view_projection = proj, + // TODO(text): dimensions of other textures, number of textures present + .texture_size = math.vec2( + @as(f32, @floatFromInt(built.texture.getWidth())), + @as(f32, @floatFromInt(built.texture.getHeight())), + ), + }; + encoder.writeBuffer(built.uniforms, 0, &[_]Uniforms{uniforms}); + } + } + + var command = encoder.finish(null); + defer command.release(); + mach.core.queue.submit(&[_]*gpu.CommandBuffer{command}); +} + +fn render(text_pipeline: *Mod) !void { + const render_pass = if (text_pipeline.state().render_pass) |rp| rp else std.debug.panic("text_pipeline.state().render_pass must be specified", .{}); + text_pipeline.state().render_pass = null; + + // TODO(text): need a way to specify order of rendering with multiple pipelines + var archetypes_iter = text_pipeline.entities.query(.{ .all = &.{ + .{ .mach_gfx_text_pipeline = &.{ + .built, + } }, + } }); + while (archetypes_iter.next()) |archetype| { + const ids = archetype.slice(.entity, .id); + const built_pipelines = archetype.slice(.mach_gfx_text_pipeline, .built); + for (ids, built_pipelines) |pipeline_id, built| { + // Draw the text batch + const total_vertices = text_pipeline.get(pipeline_id, .num_glyphs).? * 6; + render_pass.setPipeline(built.render); + // TODO(text): remove dynamic offsets? + render_pass.setBindGroup(0, built.bind_group, &.{}); + render_pass.draw(total_vertices, 1, 0, 0); + } + } +} diff --git a/src/gfx/main.zig b/src/gfx/main.zig index 2fda105c..8653626b 100644 --- a/src/gfx/main.zig +++ b/src/gfx/main.zig @@ -5,6 +5,7 @@ pub const Atlas = @import("atlas/Atlas.zig"); pub const Sprite = @import("Sprite.zig"); pub const SpritePipeline = @import("SpritePipeline.zig"); pub const Text = @import("Text.zig"); +pub const TextPipeline = @import("TextPipeline.zig"); pub const TextStyle = @import("TextStyle.zig"); // Fonts