diff --git a/build.zig.zon b/build.zig.zon index 16b13811..cfcfd32c 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -12,8 +12,8 @@ }, .dependencies = .{ .mach_ecs = .{ - .url = "https://pkg.machengine.org/mach-ecs/46fbc9175a70a8cc983b88f911aa076b625e0fb2.tar.gz", - .hash = "1220acdc7bdb09425ccdd92bb906c4e30e3391c95ab15049c946c98cc6643a7ad250", + .url = "https://pkg.machengine.org/mach-ecs/ef06fb647353356eff080ad3ec337e028b492d41.tar.gz", + .hash = "1220014851adb37d191430ac371cc467e0e0eb633b84b856c7a37e41a3149dea7ce8", }, .mach_core = .{ .url = "https://pkg.machengine.org/mach-core/cce02fb96ca787378289c5855b381f0ca9f3e090.tar.gz", diff --git a/src/engine.zig b/src/engine.zig index 00a75ff9..ec449bd3 100644 --- a/src/engine.zig +++ b/src/engine.zig @@ -16,80 +16,83 @@ pub const Engine = struct { pub const name = .engine; - pub fn engineInit(world: *World) !void { - core.allocator = allocator; - try core.init(.{}); - const state = &world.mod.engine.state; - state.device = core.device; - state.queue = core.device.getQueue(); - state.exit = false; - state.encoder = state.device.createCommandEncoder(&gpu.CommandEncoder.Descriptor{ - .label = "engine.state.encoder", - }); + pub const local = struct { + pub fn init(world: *World) !void { + core.allocator = allocator; + try core.init(.{}); + const state = &world.mod.engine.state; + state.device = core.device; + state.queue = core.device.getQueue(); + state.exit = false; + state.encoder = state.device.createCommandEncoder(&gpu.CommandEncoder.Descriptor{ + .label = "engine.state.encoder", + }); - try world.send(.init, .{}); - } + try world.send(null, .init, .{}); + } - pub fn engineDeinit( - world: *World, - engine: *World.Mod(.engine), - ) !void { - // TODO: this triggers a device loss error, which we should handle correctly - // engine.state.device.release(); - engine.state.queue.release(); - try world.send(.deinit, .{}); - core.deinit(); - world.deinit(); - _ = gpa.deinit(); - } + pub fn deinit( + world: *World, + engine: *World.Mod(.engine), + ) !void { + // TODO: this triggers a device loss error, which we should handle correctly + // engine.state.device.release(); + engine.state.queue.release(); + try world.send(null, .deinit, .{}); + core.deinit(); + world.deinit(); + _ = gpa.deinit(); + } - pub fn engineExit(world: *World) !void { - try world.send(.exit, .{}); - world.mod.engine.state.exit = true; - } + // Engine module's exit handler + pub fn exit(world: *World) !void { + try world.send(null, .exit, .{}); + world.mod.engine.state.exit = true; + } - pub fn engineBeginPass( - engine: *World.Mod(.engine), - clear_color: gpu.Color, - ) !void { - const back_buffer_view = core.swap_chain.getCurrentTextureView().?; - defer back_buffer_view.release(); + pub fn beginPass( + engine: *World.Mod(.engine), + clear_color: gpu.Color, + ) !void { + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + defer back_buffer_view.release(); - // TODO: expose options - const color_attachment = gpu.RenderPassColorAttachment{ - .view = back_buffer_view, - .clear_value = clear_color, - .load_op = .clear, - .store_op = .store, - }; - const pass_info = gpu.RenderPassDescriptor.init(.{ - .color_attachments = &.{color_attachment}, - }); + // TODO: expose options + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = clear_color, + .load_op = .clear, + .store_op = .store, + }; + const pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{color_attachment}, + }); - engine.state.pass = engine.state.encoder.beginRenderPass(&pass_info); - } + engine.state.pass = engine.state.encoder.beginRenderPass(&pass_info); + } - pub fn engineEndPass( - engine: *World.Mod(.engine), - ) !void { - // End this pass - engine.state.pass.end(); - engine.state.pass.release(); + pub fn endPass( + engine: *World.Mod(.engine), + ) !void { + // End this pass + engine.state.pass.end(); + engine.state.pass.release(); - var command = engine.state.encoder.finish(null); - defer command.release(); - engine.state.encoder.release(); - engine.state.queue.submit(&[_]*gpu.CommandBuffer{command}); + var command = engine.state.encoder.finish(null); + defer command.release(); + engine.state.encoder.release(); + engine.state.queue.submit(&[_]*gpu.CommandBuffer{command}); - // Prepare for next pass - engine.state.encoder = engine.state.device.createCommandEncoder(&gpu.CommandEncoder.Descriptor{ - .label = "engine.state.encoder", - }); - } + // Prepare for next pass + engine.state.encoder = engine.state.device.createCommandEncoder(&gpu.CommandEncoder.Descriptor{ + .label = "engine.state.encoder", + }); + } - pub fn enginePresent() !void { - core.swap_chain.present(); - } + pub fn present() !void { + core.swap_chain.present(); + } + }; }; pub const App = struct { @@ -97,15 +100,15 @@ pub const App = struct { pub fn init(app: *@This()) !void { app.* = .{ .world = try World.init(allocator) }; - try app.world.send(.engineInit, .{}); + try app.world.send(.engine, .init, .{}); } pub fn deinit(app: *@This()) void { - try app.world.send(.engineDeinit, .{}); + try app.world.send(.engine, .deinit, .{}); } pub fn update(app: *@This()) !bool { - try app.world.send(.tick, .{}); + try app.world.send(null, .tick, .{}); return app.world.mod.engine.state.exit; } }; diff --git a/src/gfx/Sprite.zig b/src/gfx/Sprite.zig index 87959a69..6ab94487 100644 --- a/src/gfx/Sprite.zig +++ b/src/gfx/Sprite.zig @@ -120,249 +120,251 @@ pub const PipelineOptions = struct { pipeline_layout: ?*gpu.PipelineLayout = null, }; -pub fn machGfxSpriteInit( - sprite_mod: *mach.Mod(.mach_gfx_sprite), -) !void { - sprite_mod.state = .{ - // TODO: struct default value initializers don't work - .pipelines = .{}, - }; -} - -pub fn machGfxSpriteInitPipeline( - engine: *mach.Mod(.engine), - sprite_mod: *mach.Mod(.mach_gfx_sprite), - opt: PipelineOptions, -) !void { - const device = engine.state.device; - - const pipeline = try sprite_mod.state.pipelines.getOrPut(engine.allocator, opt.pipeline); - if (pipeline.found_existing) { - pipeline.value_ptr.*.deinit(); - } - - // Storage buffers - const sprite_buffer_cap = 1024 * 512; // TODO: allow user to specify preallocation - const transforms = device.createBuffer(&.{ - .usage = .{ .storage = true, .copy_dst = true }, - .size = @sizeOf(Mat4x4) * sprite_buffer_cap, - .mapped_at_creation = .false, - }); - const uv_transforms = device.createBuffer(&.{ - .usage = .{ .storage = true, .copy_dst = true }, - .size = @sizeOf(Mat3x3) * sprite_buffer_cap, - .mapped_at_creation = .false, - }); - const sizes = device.createBuffer(&.{ - .usage = .{ .storage = true, .copy_dst = true }, - .size = @sizeOf(Vec2) * sprite_buffer_cap, - .mapped_at_creation = .false, - }); - - const texture_sampler = opt.texture_sampler orelse device.createSampler(&.{ - .mag_filter = .nearest, - .min_filter = .nearest, - }); - const uniforms = device.createBuffer(&.{ - .usage = .{ .copy_dst = true, .uniform = true }, - .size = @sizeOf(Uniforms), - .mapped_at_creation = .false, - }); - const bind_group_layout = opt.bind_group_layout orelse device.createBindGroupLayout( - &gpu.BindGroupLayout.Descriptor.init(.{ - .entries = &.{ - gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true }, .uniform, false, 0), - gpu.BindGroupLayout.Entry.buffer(1, .{ .vertex = true }, .read_only_storage, false, 0), - gpu.BindGroupLayout.Entry.buffer(2, .{ .vertex = true }, .read_only_storage, false, 0), - gpu.BindGroupLayout.Entry.buffer(3, .{ .vertex = true }, .read_only_storage, false, 0), - gpu.BindGroupLayout.Entry.sampler(4, .{ .fragment = true }, .filtering), - gpu.BindGroupLayout.Entry.texture(5, .{ .fragment = true }, .float, .dimension_2d, false), - gpu.BindGroupLayout.Entry.texture(6, .{ .fragment = true }, .float, .dimension_2d, false), - gpu.BindGroupLayout.Entry.texture(7, .{ .fragment = true }, .float, .dimension_2d, false), - gpu.BindGroupLayout.Entry.texture(8, .{ .fragment = true }, .float, .dimension_2d, false), - }, - }), - ); - defer bind_group_layout.release(); - - const texture_view = opt.texture.createView(&gpu.TextureView.Descriptor{}); - const texture2_view = if (opt.texture2) |tex| tex.createView(&gpu.TextureView.Descriptor{}) else texture_view; - const texture3_view = if (opt.texture3) |tex| tex.createView(&gpu.TextureView.Descriptor{}) else texture_view; - const texture4_view = if (opt.texture4) |tex| tex.createView(&gpu.TextureView.Descriptor{}) else texture_view; - defer texture_view.release(); - defer texture2_view.release(); - defer texture3_view.release(); - defer texture4_view.release(); - - const bind_group = opt.bind_group orelse device.createBindGroup( - &gpu.BindGroup.Descriptor.init(.{ - .layout = bind_group_layout, - .entries = &.{ - gpu.BindGroup.Entry.buffer(0, uniforms, 0, @sizeOf(Uniforms)), - gpu.BindGroup.Entry.buffer(1, transforms, 0, @sizeOf(Mat4x4) * sprite_buffer_cap), - gpu.BindGroup.Entry.buffer(2, uv_transforms, 0, @sizeOf(Mat3x3) * sprite_buffer_cap), - gpu.BindGroup.Entry.buffer(3, sizes, 0, @sizeOf(Vec2) * sprite_buffer_cap), - gpu.BindGroup.Entry.sampler(4, texture_sampler), - gpu.BindGroup.Entry.textureView(5, texture_view), - gpu.BindGroup.Entry.textureView(6, texture2_view), - gpu.BindGroup.Entry.textureView(7, texture3_view), - gpu.BindGroup.Entry.textureView(8, texture4_view), - }, - }), - ); - - const blend_state = opt.blend_state orelse gpu.BlendState{ - .color = .{ - .operation = .add, - .src_factor = .src_alpha, - .dst_factor = .one_minus_src_alpha, - }, - .alpha = .{ - .operation = .add, - .src_factor = .one, - .dst_factor = .zero, - }, - }; - - const shader_module = opt.shader orelse device.createShaderModuleWGSL("sprite.wgsl", @embedFile("sprite.wgsl")); - defer shader_module.release(); - - const color_target = opt.color_target_state orelse gpu.ColorTargetState{ - .format = core.descriptor.format, - .blend = &blend_state, - .write_mask = gpu.ColorWriteMaskFlags.all, - }; - const fragment = opt.fragment_state orelse gpu.FragmentState.init(.{ - .module = shader_module, - .entry_point = "fragMain", - .targets = &.{color_target}, - }); - - const bind_group_layouts = [_]*gpu.BindGroupLayout{bind_group_layout}; - const pipeline_layout = opt.pipeline_layout orelse device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{ - .bind_group_layouts = &bind_group_layouts, - })); - defer pipeline_layout.release(); - const render = 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, - .texture_sampler = texture_sampler, - .texture = opt.texture, - .texture2 = opt.texture2, - .texture3 = opt.texture3, - .texture4 = opt.texture4, - .bind_group = bind_group, - .uniforms = uniforms, - .num_sprites = 0, - .transforms = transforms, - .uv_transforms = uv_transforms, - .sizes = sizes, - }; - pipeline.value_ptr.reference(); -} - pub fn deinit(sprite_mod: *mach.Mod(.mach_gfx_sprite)) !void { for (sprite_mod.state.pipelines.entries.items(.value)) |*pipeline| pipeline.deinit(); sprite_mod.state.pipelines.deinit(sprite_mod.allocator); } -pub fn machGfxSpriteUpdated( - engine: *mach.Mod(.engine), - sprite_mod: *mach.Mod(.mach_gfx_sprite), - pipeline_id: u32, -) !void { - const pipeline = sprite_mod.state.pipelines.getPtr(pipeline_id).?; - const device = engine.state.device; - - // TODO: make sure these entities only belong to the given pipeline - // we need a better tagging mechanism - var archetypes_iter = engine.entities.query(.{ .all = &.{ - .{ .mach_gfx_sprite = &.{ - .uv_transform, - .transform, - .size, - .pipeline, - } }, - } }); - - const encoder = device.createCommandEncoder(null); - defer encoder.release(); - - pipeline.num_sprites = 0; - var transforms_offset: usize = 0; - var uv_transforms_offset: usize = 0; - var sizes_offset: usize = 0; - while (archetypes_iter.next()) |archetype| { - var transforms = archetype.slice(.mach_gfx_sprite, .transform); - var uv_transforms = archetype.slice(.mach_gfx_sprite, .uv_transform); - var sizes = archetype.slice(.mach_gfx_sprite, .size); - - // TODO: confirm the lifetime of these slices is OK for writeBuffer, how long do they need - // to live? - encoder.writeBuffer(pipeline.transforms, transforms_offset, transforms); - encoder.writeBuffer(pipeline.uv_transforms, uv_transforms_offset, uv_transforms); - encoder.writeBuffer(pipeline.sizes, sizes_offset, sizes); - - transforms_offset += transforms.len; - uv_transforms_offset += uv_transforms.len; - sizes_offset += sizes.len; - pipeline.num_sprites += @intCast(transforms.len); +pub const local = struct { + pub fn init( + sprite_mod: *mach.Mod(.mach_gfx_sprite), + ) !void { + sprite_mod.state = .{ + // TODO: struct default value initializers don't work + .pipelines = .{}, + }; } - var command = encoder.finish(null); - defer command.release(); + pub fn initPipeline( + engine: *mach.Mod(.engine), + sprite_mod: *mach.Mod(.mach_gfx_sprite), + opt: PipelineOptions, + ) !void { + const device = engine.state.device; - engine.state.queue.submit(&[_]*gpu.CommandBuffer{command}); -} + const pipeline = try sprite_mod.state.pipelines.getOrPut(engine.allocator, opt.pipeline); + if (pipeline.found_existing) { + pipeline.value_ptr.*.deinit(); + } -pub fn machGfxSpritePreRender( - engine: *mach.Mod(.engine), - sprite_mod: *mach.Mod(.mach_gfx_sprite), - pipeline_id: u32, -) !void { - const pipeline = sprite_mod.state.pipelines.get(pipeline_id).?; + // Storage buffers + const sprite_buffer_cap = 1024 * 512; // TODO: allow user to specify preallocation + const transforms = device.createBuffer(&.{ + .usage = .{ .storage = true, .copy_dst = true }, + .size = @sizeOf(Mat4x4) * sprite_buffer_cap, + .mapped_at_creation = .false, + }); + const uv_transforms = device.createBuffer(&.{ + .usage = .{ .storage = true, .copy_dst = true }, + .size = @sizeOf(Mat3x3) * sprite_buffer_cap, + .mapped_at_creation = .false, + }); + const sizes = device.createBuffer(&.{ + .usage = .{ .storage = true, .copy_dst = true }, + .size = @sizeOf(Vec2) * sprite_buffer_cap, + .mapped_at_creation = .false, + }); - // Update uniform buffer - const ortho = Mat4x4.ortho( - -@as(f32, @floatFromInt(core.size().width)) / 2, - @as(f32, @floatFromInt(core.size().width)) / 2, - -@as(f32, @floatFromInt(core.size().height)) / 2, - @as(f32, @floatFromInt(core.size().height)) / 2, - -0.1, - 100000, - ); - const uniforms = Uniforms{ - .view_projection = ortho, - // TODO: dimensions of other textures, number of textures present - .texture_size = vec2( - @as(f32, @floatFromInt(pipeline.texture.getWidth())), - @as(f32, @floatFromInt(pipeline.texture.getHeight())), - ), - }; + 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(); - engine.state.encoder.writeBuffer(pipeline.uniforms, 0, &[_]Uniforms{uniforms}); -} + const texture_view = opt.texture.createView(&gpu.TextureView.Descriptor{}); + const texture2_view = if (opt.texture2) |tex| tex.createView(&gpu.TextureView.Descriptor{}) else texture_view; + const texture3_view = if (opt.texture3) |tex| tex.createView(&gpu.TextureView.Descriptor{}) else texture_view; + const texture4_view = if (opt.texture4) |tex| tex.createView(&gpu.TextureView.Descriptor{}) else texture_view; + defer texture_view.release(); + defer texture2_view.release(); + defer texture3_view.release(); + defer texture4_view.release(); -pub fn machGfxSpriteRender( - engine: *mach.Mod(.engine), - sprite_mod: *mach.Mod(.mach_gfx_sprite), - pipeline_id: u32, -) !void { - const pipeline = sprite_mod.state.pipelines.get(pipeline_id).?; + const bind_group = opt.bind_group orelse device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = bind_group_layout, + .entries = &.{ + gpu.BindGroup.Entry.buffer(0, uniforms, 0, @sizeOf(Uniforms)), + gpu.BindGroup.Entry.buffer(1, transforms, 0, @sizeOf(Mat4x4) * sprite_buffer_cap), + gpu.BindGroup.Entry.buffer(2, uv_transforms, 0, @sizeOf(Mat3x3) * sprite_buffer_cap), + gpu.BindGroup.Entry.buffer(3, sizes, 0, @sizeOf(Vec2) * sprite_buffer_cap), + gpu.BindGroup.Entry.sampler(4, texture_sampler), + gpu.BindGroup.Entry.textureView(5, texture_view), + gpu.BindGroup.Entry.textureView(6, texture2_view), + gpu.BindGroup.Entry.textureView(7, texture3_view), + gpu.BindGroup.Entry.textureView(8, texture4_view), + }, + }), + ); - // Draw the sprite batch - const pass = engine.state.pass; - const total_vertices = pipeline.num_sprites * 6; - pass.setPipeline(pipeline.render); - // TODO: remove dynamic offsets? - pass.setBindGroup(0, pipeline.bind_group, &.{}); - pass.draw(total_vertices, 1, 0, 0); -} + 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("sprite.wgsl", @embedFile("sprite.wgsl")); + defer shader_module.release(); + + const color_target = opt.color_target_state orelse gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &blend_state, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + const fragment = opt.fragment_state orelse gpu.FragmentState.init(.{ + .module = shader_module, + .entry_point = "fragMain", + .targets = &.{color_target}, + }); + + const bind_group_layouts = [_]*gpu.BindGroupLayout{bind_group_layout}; + const pipeline_layout = opt.pipeline_layout orelse device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{ + .bind_group_layouts = &bind_group_layouts, + })); + defer pipeline_layout.release(); + const render_pipeline = device.createRenderPipeline(&gpu.RenderPipeline.Descriptor{ + .fragment = &fragment, + .layout = pipeline_layout, + .vertex = gpu.VertexState{ + .module = shader_module, + .entry_point = "vertMain", + }, + }); + + pipeline.value_ptr.* = Pipeline{ + .render = render_pipeline, + .texture_sampler = texture_sampler, + .texture = opt.texture, + .texture2 = opt.texture2, + .texture3 = opt.texture3, + .texture4 = opt.texture4, + .bind_group = bind_group, + .uniforms = uniforms, + .num_sprites = 0, + .transforms = transforms, + .uv_transforms = uv_transforms, + .sizes = sizes, + }; + pipeline.value_ptr.reference(); + } + + pub fn updated( + engine: *mach.Mod(.engine), + sprite_mod: *mach.Mod(.mach_gfx_sprite), + pipeline_id: u32, + ) !void { + const pipeline = sprite_mod.state.pipelines.getPtr(pipeline_id).?; + const device = engine.state.device; + + // TODO: make sure these entities only belong to the given pipeline + // we need a better tagging mechanism + var archetypes_iter = engine.entities.query(.{ .all = &.{ + .{ .mach_gfx_sprite = &.{ + .uv_transform, + .transform, + .size, + .pipeline, + } }, + } }); + + const encoder = device.createCommandEncoder(null); + defer encoder.release(); + + pipeline.num_sprites = 0; + var transforms_offset: usize = 0; + var uv_transforms_offset: usize = 0; + var sizes_offset: usize = 0; + while (archetypes_iter.next()) |archetype| { + var transforms = archetype.slice(.mach_gfx_sprite, .transform); + var uv_transforms = archetype.slice(.mach_gfx_sprite, .uv_transform); + var sizes = archetype.slice(.mach_gfx_sprite, .size); + + // TODO: confirm the lifetime of these slices is OK for writeBuffer, how long do they need + // to live? + encoder.writeBuffer(pipeline.transforms, transforms_offset, transforms); + encoder.writeBuffer(pipeline.uv_transforms, uv_transforms_offset, uv_transforms); + encoder.writeBuffer(pipeline.sizes, sizes_offset, sizes); + + transforms_offset += transforms.len; + uv_transforms_offset += uv_transforms.len; + sizes_offset += sizes.len; + pipeline.num_sprites += @intCast(transforms.len); + } + + var command = encoder.finish(null); + defer command.release(); + + engine.state.queue.submit(&[_]*gpu.CommandBuffer{command}); + } + + pub fn preRender( + engine: *mach.Mod(.engine), + sprite_mod: *mach.Mod(.mach_gfx_sprite), + pipeline_id: u32, + ) !void { + const pipeline = sprite_mod.state.pipelines.get(pipeline_id).?; + + // Update uniform buffer + const ortho = Mat4x4.ortho( + -@as(f32, @floatFromInt(core.size().width)) / 2, + @as(f32, @floatFromInt(core.size().width)) / 2, + -@as(f32, @floatFromInt(core.size().height)) / 2, + @as(f32, @floatFromInt(core.size().height)) / 2, + -0.1, + 100000, + ); + const uniforms = Uniforms{ + .view_projection = ortho, + // TODO: dimensions of other textures, number of textures present + .texture_size = vec2( + @as(f32, @floatFromInt(pipeline.texture.getWidth())), + @as(f32, @floatFromInt(pipeline.texture.getHeight())), + ), + }; + + engine.state.encoder.writeBuffer(pipeline.uniforms, 0, &[_]Uniforms{uniforms}); + } + + pub fn render( + engine: *mach.Mod(.engine), + sprite_mod: *mach.Mod(.mach_gfx_sprite), + pipeline_id: u32, + ) !void { + const pipeline = sprite_mod.state.pipelines.get(pipeline_id).?; + + // Draw the sprite batch + const pass = engine.state.pass; + const total_vertices = pipeline.num_sprites * 6; + pass.setPipeline(pipeline.render); + // TODO: remove dynamic offsets? + pass.setBindGroup(0, pipeline.bind_group, &.{}); + pass.draw(total_vertices, 1, 0, 0); + } +}; diff --git a/src/gfx/Text.zig b/src/gfx/Text.zig index 7e8a462d..96c30acc 100644 --- a/src/gfx/Text.zig +++ b/src/gfx/Text.zig @@ -170,374 +170,376 @@ pub const PipelineOptions = struct { pipeline_layout: ?*gpu.PipelineLayout = null, }; -pub fn machGfxTextInit( - text_mod: *mach.Mod(.mach_gfx_text), -) !void { - text_mod.state = .{ - // TODO: struct default value initializers don't work - .pipelines = .{}, - }; -} - -pub fn machGfxTextInitPipeline( - engine: *mach.Mod(.engine), - text_mod: *mach.Mod(.mach_gfx_text), - opt: PipelineOptions, -) !void { - const device = engine.state.device; - - const pipeline = try text_mod.state.pipelines.getOrPut(engine.allocator, opt.pipeline); - if (pipeline.found_existing) { - pipeline.value_ptr.*.deinit(engine.allocator); - } - - // Prepare texture for the font atlas. - const img_size = gpu.Extent3D{ .width = 1024, .height = 1024 }; - const texture = device.createTexture(&.{ - .size = img_size, - .format = .rgba8_unorm, - .usage = .{ - .texture_binding = true, - .copy_dst = true, - .render_attachment = true, - }, - }); - const texture_atlas = try gfx.Atlas.init( - engine.allocator, - img_size.width, - .rgba, - ); - - // 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 = 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, - .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(); -} - pub fn deinit(text_mod: *mach.Mod(.mach_gfx_text)) !void { for (text_mod.state.pipelines.entries.items(.value)) |*pipeline| pipeline.deinit(text_mod.allocator); text_mod.state.pipelines.deinit(text_mod.allocator); } -pub fn machGfxTextUpdated( - engine: *mach.Mod(.engine), - text_mod: *mach.Mod(.mach_gfx_text), - pipeline_id: u32, -) !void { - const pipeline = text_mod.state.pipelines.getPtr(pipeline_id).?; - const device = engine.state.device; +pub const local = struct { + pub fn init( + text_mod: *mach.Mod(.mach_gfx_text), + ) !void { + text_mod.state = .{ + // TODO: struct default value initializers don't work + .pipelines = .{}, + }; + } - // 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, - } }, - } }); + pub fn initPipeline( + engine: *mach.Mod(.engine), + text_mod: *mach.Mod(.mach_gfx_text), + opt: PipelineOptions, + ) !void { + const device = engine.state.device; - const encoder = device.createCommandEncoder(null); - defer encoder.release(); + const pipeline = try text_mod.state.pipelines.getOrPut(engine.allocator, opt.pipeline); + if (pipeline.found_existing) { + pipeline.value_ptr.*.deinit(engine.allocator); + } - pipeline.num_texts = 0; - pipeline.num_glyphs = 0; - var glyphs = std.ArrayListUnmanaged(Glyph){}; - var transforms_offset: usize = 0; - // var colors_offset: usize = 0; - var texture_update = false; - while (archetypes_iter.next()) |archetype| { - var transforms = archetype.slice(.mach_gfx_text, .transform); - // var colors = archetype.slice(.mach_gfx_text, .color); + // Prepare texture for the font atlas. + const img_size = gpu.Extent3D{ .width = 1024, .height = 1024 }; + const texture = device.createTexture(&.{ + .size = img_size, + .format = .rgba8_unorm, + .usage = .{ + .texture_binding = true, + .copy_dst = true, + .render_attachment = true, + }, + }); + const texture_atlas = try gfx.Atlas.init( + engine.allocator, + img_size.width, + .rgba, + ); - // 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); + // 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, + }); - transforms_offset += transforms.len; - // colors_offset += colors.len; - pipeline.num_texts += @intCast(transforms.len); + 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(); - // 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; - // var font_names = archetype.slice(.mach_gfx_text, .font_name); - // var font_sizes = archetype.slice(.mach_gfx_text, .font_size); - var texts = archetype.slice(.mach_gfx_text, .text); - for (texts) |text| { - var origin_x: f32 = 0.0; - var origin_y: f32 = 0.0; + 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(); - for (text) |segment| { - // Load a font - // TODO: resolve font by name, not hard-code - const font_bytes = @import("font-assets").fira_sans_regular_ttf; - var font = try gfx.Font.initBytes(font_bytes); - defer font.deinit(engine.allocator); + 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), + }, + }), + ); - // Create a text shaper - var run = try gfx.TextRun.init(); - run.font_size_px = segment.style.font_size; - run.px_density = 2; // TODO + 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, + }, + }; - defer run.deinit(); + const shader_module = opt.shader orelse device.createShaderModuleWGSL("text.wgsl", @embedFile("text.wgsl")); + defer shader_module.release(); - run.addText(segment.string); - try font.shape(&run); + 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}, + }); - while (run.next()) |glyph| { - const codepoint = segment.string[glyph.cluster]; - // TODO: use flags(?) to detect newline, or at least something more reliable? - if (codepoint != '\n') { - var region = try pipeline.regions.getOrPut(engine.allocator, .{ - .index = glyph.glyph_index, - .size = @bitCast(segment.style.font_size), - }); - if (!region.found_existing) { - const rendered_glyph = try font.render(engine.allocator, glyph.glyph_index, .{ - .font_size_px = run.font_size_px, + 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(); + } + + pub fn updated( + engine: *mach.Mod(.engine), + text_mod: *mach.Mod(.mach_gfx_text), + pipeline_id: u32, + ) !void { + const pipeline = text_mod.state.pipelines.getPtr(pipeline_id).?; + const device = engine.state.device; + + // TODO: make sure these entities only belong to the given pipeline + // we need a better tagging mechanism + var archetypes_iter = engine.entities.query(.{ .all = &.{ + .{ .mach_gfx_text = &.{ + .pipeline, + .transform, + .text, + } }, + } }); + + const encoder = device.createCommandEncoder(null); + defer encoder.release(); + + pipeline.num_texts = 0; + pipeline.num_glyphs = 0; + var glyphs = std.ArrayListUnmanaged(Glyph){}; + var transforms_offset: usize = 0; + // var colors_offset: usize = 0; + var texture_update = false; + while (archetypes_iter.next()) |archetype| { + var transforms = archetype.slice(.mach_gfx_text, .transform); + // var colors = archetype.slice(.mach_gfx_text, .color); + + // TODO: confirm the lifetime of these slices is OK for writeBuffer, how long do they need + // to live? + encoder.writeBuffer(pipeline.transforms, transforms_offset, transforms); + // encoder.writeBuffer(pipeline.colors, colors_offset, colors); + + transforms_offset += transforms.len; + // colors_offset += colors.len; + pipeline.num_texts += @intCast(transforms.len); + + // 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; + // var font_names = archetype.slice(.mach_gfx_text, .font_name); + // var font_sizes = archetype.slice(.mach_gfx_text, .font_size); + var texts = archetype.slice(.mach_gfx_text, .text); + for (texts) |text| { + var origin_x: f32 = 0.0; + var origin_y: f32 = 0.0; + + for (text) |segment| { + // Load a font + // TODO: resolve font by name, not hard-code + const font_bytes = @import("font-assets").fira_sans_regular_ttf; + var font = try gfx.Font.initBytes(font_bytes); + defer font.deinit(engine.allocator); + + // Create a text shaper + var run = try gfx.TextRun.init(); + run.font_size_px = segment.style.font_size; + run.px_density = 2; // TODO + + defer run.deinit(); + + run.addText(segment.string); + try font.shape(&run); + + while (run.next()) |glyph| { + const codepoint = segment.string[glyph.cluster]; + // TODO: use flags(?) to detect newline, or at least something more reliable? + if (codepoint != '\n') { + var region = try pipeline.regions.getOrPut(engine.allocator, .{ + .index = glyph.glyph_index, + .size = @bitCast(segment.style.font_size), }); - if (rendered_glyph.bitmap) |bitmap| { - var glyph_atlas_region = try pipeline.texture_atlas.reserve(engine.allocator, rendered_glyph.width, rendered_glyph.height); - pipeline.texture_atlas.set(glyph_atlas_region, @as([*]const u8, @ptrCast(bitmap.ptr))[0 .. bitmap.len * 4]); - texture_update = true; + if (!region.found_existing) { + const rendered_glyph = try font.render(engine.allocator, glyph.glyph_index, .{ + .font_size_px = run.font_size_px, + }); + if (rendered_glyph.bitmap) |bitmap| { + var glyph_atlas_region = try pipeline.texture_atlas.reserve(engine.allocator, rendered_glyph.width, rendered_glyph.height); + pipeline.texture_atlas.set(glyph_atlas_region, @as([*]const u8, @ptrCast(bitmap.ptr))[0 .. bitmap.len * 4]); + texture_update = true; - // Exclude the 1px blank space margin when describing the region of the texture - // that actually represents the glyph. - const margin = 1; - glyph_atlas_region.x += margin; - glyph_atlas_region.y += margin; - glyph_atlas_region.width -= margin * 2; - glyph_atlas_region.height -= margin * 2; - region.value_ptr.* = glyph_atlas_region; - } else { - // whitespace - region.value_ptr.* = gfx.Atlas.Region{ - .width = 0, - .height = 0, - .x = 0, - .y = 0, - }; + // Exclude the 1px blank space margin when describing the region of the texture + // that actually represents the glyph. + const margin = 1; + glyph_atlas_region.x += margin; + glyph_atlas_region.y += margin; + glyph_atlas_region.width -= margin * 2; + glyph_atlas_region.height -= margin * 2; + region.value_ptr.* = glyph_atlas_region; + } else { + // whitespace + region.value_ptr.* = gfx.Atlas.Region{ + .width = 0, + .height = 0, + .x = 0, + .y = 0, + }; + } } + + const r = region.value_ptr.*; + const size = vec2(@floatFromInt(r.width), @floatFromInt(r.height)); + try glyphs.append(engine.allocator, .{ + .pos = vec2( + origin_x + glyph.offset.x(), + origin_y - (size.y() - glyph.offset.y()), + ).divScalar(px_density), + .size = size.divScalar(px_density), + .text_index = 0, + .uv_pos = vec2(@floatFromInt(r.x), @floatFromInt(r.y)), + }); + pipeline.num_glyphs += 1; } - const r = region.value_ptr.*; - const size = vec2(@floatFromInt(r.width), @floatFromInt(r.height)); - try glyphs.append(engine.allocator, .{ - .pos = vec2( - origin_x + glyph.offset.x(), - origin_y - (size.y() - glyph.offset.y()), - ).divScalar(px_density), - .size = size.divScalar(px_density), - .text_index = 0, - .uv_pos = vec2(@floatFromInt(r.x), @floatFromInt(r.y)), - }); - pipeline.num_glyphs += 1; - } - - if (codepoint == '\n') { - origin_x = 0; - origin_y -= segment.style.font_size; - } else { - origin_x += glyph.advance.x(); + if (codepoint == '\n') { + origin_x = 0; + origin_y -= segment.style.font_size; + } else { + origin_x += glyph.advance.x(); + } } } } } + + // TODO: could writeBuffer check for zero? + if (glyphs.items.len > 0) encoder.writeBuffer(pipeline.glyphs, 0, glyphs.items); + defer glyphs.deinit(engine.allocator); + if (texture_update) { + // rgba32_pixels + // TODO: use proper texture dimensions here + const img_size = gpu.Extent3D{ .width = 1024, .height = 1024 }; + const data_layout = gpu.Texture.DataLayout{ + .bytes_per_row = @as(u32, @intCast(img_size.width * 4)), + .rows_per_image = @as(u32, @intCast(img_size.height)), + }; + engine.state.queue.writeTexture( + &.{ .texture = pipeline.texture }, + &data_layout, + &img_size, + pipeline.texture_atlas.data, + ); + } + + var command = encoder.finish(null); + defer command.release(); + + engine.state.queue.submit(&[_]*gpu.CommandBuffer{command}); } - // TODO: could writeBuffer check for zero? - if (glyphs.items.len > 0) encoder.writeBuffer(pipeline.glyphs, 0, glyphs.items); - defer glyphs.deinit(engine.allocator); - if (texture_update) { - // rgba32_pixels - // TODO: use proper texture dimensions here - const img_size = gpu.Extent3D{ .width = 1024, .height = 1024 }; - const data_layout = gpu.Texture.DataLayout{ - .bytes_per_row = @as(u32, @intCast(img_size.width * 4)), - .rows_per_image = @as(u32, @intCast(img_size.height)), - }; - engine.state.queue.writeTexture( - &.{ .texture = pipeline.texture }, - &data_layout, - &img_size, - pipeline.texture_atlas.data, + pub fn preRender( + engine: *mach.Mod(.engine), + text_mod: *mach.Mod(.mach_gfx_text), + pipeline_id: u32, + ) !void { + const pipeline = text_mod.state.pipelines.get(pipeline_id).?; + + // Update uniform buffer + const ortho = Mat4x4.ortho( + -@as(f32, @floatFromInt(core.size().width)) / 2, + @as(f32, @floatFromInt(core.size().width)) / 2, + -@as(f32, @floatFromInt(core.size().height)) / 2, + @as(f32, @floatFromInt(core.size().height)) / 2, + -0.1, + 100000, ); + const uniforms = Uniforms{ + .view_projection = ortho, + // 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}); } - var command = encoder.finish(null); - defer command.release(); + pub fn render( + engine: *mach.Mod(.engine), + text_mod: *mach.Mod(.mach_gfx_text), + pipeline_id: u32, + ) !void { + const pipeline = text_mod.state.pipelines.get(pipeline_id).?; - engine.state.queue.submit(&[_]*gpu.CommandBuffer{command}); -} - -pub fn machGfxTextPreRender( - engine: *mach.Mod(.engine), - text_mod: *mach.Mod(.mach_gfx_text), - pipeline_id: u32, -) !void { - const pipeline = text_mod.state.pipelines.get(pipeline_id).?; - - // Update uniform buffer - const ortho = Mat4x4.ortho( - -@as(f32, @floatFromInt(core.size().width)) / 2, - @as(f32, @floatFromInt(core.size().width)) / 2, - -@as(f32, @floatFromInt(core.size().height)) / 2, - @as(f32, @floatFromInt(core.size().height)) / 2, - -0.1, - 100000, - ); - const uniforms = Uniforms{ - .view_projection = ortho, - // TODO: dimensions of other textures, number of textures present - .texture_size = vec2( - @as(f32, @floatFromInt(pipeline.texture.getWidth())), - @as(f32, @floatFromInt(pipeline.texture.getHeight())), - ), - }; - - engine.state.encoder.writeBuffer(pipeline.uniforms, 0, &[_]Uniforms{uniforms}); -} - -pub fn machGfxTextRender( - engine: *mach.Mod(.engine), - text_mod: *mach.Mod(.mach_gfx_text), - pipeline_id: u32, -) !void { - const pipeline = text_mod.state.pipelines.get(pipeline_id).?; - - // Draw the text batch - const pass = engine.state.pass; - const total_vertices = pipeline.num_glyphs * 6; - pass.setPipeline(pipeline.render); - // TODO: remove dynamic offsets? - pass.setBindGroup(0, pipeline.bind_group, &.{}); - pass.draw(total_vertices, 1, 0, 0); -} + // 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); + } +};