From 16a895240d1402b1158cbfe706e48d643b223658 Mon Sep 17 00:00:00 2001 From: Stephen Gutekanst Date: Tue, 16 Apr 2024 10:10:34 -0700 Subject: [PATCH] gfx: improve Sprite module pipeline management * No longer abuse event arguments for pipeline information. * Store pipeline information as entities/components. Signed-off-by: Stephen Gutekanst --- examples/glyphs/Game.zig | 70 +++---- examples/glyphs/main.zig | 1 + examples/sprite/Game.zig | 40 ++-- examples/sprite/main.zig | 1 + src/gfx/Sprite.zig | 375 ++++++------------------------------- src/gfx/SpritePipeline.zig | 373 ++++++++++++++++++++++++++++++++++++ src/gfx/main.zig | 1 + src/main.zig | 2 +- 8 files changed, 486 insertions(+), 377 deletions(-) create mode 100644 src/gfx/SpritePipeline.zig diff --git a/examples/glyphs/Game.zig b/examples/glyphs/Game.zig index 40560989..a8401087 100644 --- a/examples/glyphs/Game.zig +++ b/examples/glyphs/Game.zig @@ -3,7 +3,7 @@ const std = @import("std"); const mach = @import("mach"); const core = mach.core; const gpu = mach.gpu; -const Sprite = mach.gfx.Sprite; +const gfx = mach.gfx; const math = mach.math; const vec2 = math.vec2; const vec3 = math.vec3; @@ -24,6 +24,7 @@ frame_count: usize, sprites: usize, rand: std.rand.DefaultPrng, time: f32, +pipeline: mach.EntityID, const d0 = 0.000001; @@ -49,48 +50,28 @@ pub const local_events = .{ .after_sprite_init = .{ .handler = afterSpriteInit }, }; -pub const Pipeline = enum(u32) { - default, - text, -}; - fn init( - sprite_mod: *Sprite.Mod, + sprite_mod: *gfx.Sprite.Mod, + sprite_pipeline: *gfx.SpritePipeline.Mod, text_mod: *Text.Mod, game: *Mod, ) !void { // The Mach .core is where we set window options, etc. core.setTitle("gfx.Sprite example"); - // Tell sprite_mod to use the texture + // Create a sprite rendering pipeline const texture = text_mod.state().texture; - sprite_mod.send(.init_pipeline, .{Sprite.PipelineOptions{ - .pipeline = @intFromEnum(Pipeline.text), - .texture = texture, - }}); + const pipeline = try sprite_pipeline.newEntity(); + try sprite_pipeline.set(pipeline, .texture, texture); + sprite_pipeline.send(.update, .{}); - // Run the rest of our init code after sprite_mod's .init_pipeline - // TODO(important): relying on this event ordering is not good - game.send(.after_sprite_init, .{}); -} - -fn afterSpriteInit( - engine: *mach.Engine.Mod, - sprite_mod: *Sprite.Mod, - text_mod: *Text.Mod, - game: *Mod, -) !void { // We can create entities, and set components on them. Note that components live in a module // namespace, e.g. the `Sprite` module could have a 3D `.location` component with a different // type than the `.physics2d` module's `.location` component if you desire. - const r = text_mod.state().regions.get('?').?; - const player = try engine.newEntity(); + const player = try sprite_mod.newEntity(); try sprite_mod.set(player, .transform, Mat4x4.translate(vec3(-0.02, 0, 0))); - try sprite_mod.set(player, .size, vec2(@floatFromInt(r.width), @floatFromInt(r.height))); - try sprite_mod.set(player, .uv_transform, Mat3x3.translate(vec2(@floatFromInt(r.x), @floatFromInt(r.y)))); - try sprite_mod.set(player, .pipeline, @intFromEnum(Pipeline.text)); - sprite_mod.send(.updated, .{@intFromEnum(Pipeline.text)}); + try sprite_mod.set(player, .pipeline, pipeline); game.init(.{ .timer = try mach.Timer.start(), @@ -101,12 +82,32 @@ fn afterSpriteInit( .sprites = 0, .rand = std.rand.DefaultPrng.init(1337), .time = 0, + .pipeline = pipeline, }); + + // TODO(important): text module should not use global init, so that game can instruct it more clearly and then + // this after_init would be more clear. Also after_sprite_init should be renamed to after_text_init and the comment + // below is wrong: + // + // Run the rest of our init code after sprite_mod's .init_pipeline + game.send(.after_sprite_init, .{}); +} + +fn afterSpriteInit( + sprite_mod: *gfx.Sprite.Mod, + text_mod: *Text.Mod, + game: *Mod, +) !void { + const r = text_mod.state().regions.get('?').?; + try sprite_mod.set(game.state().player, .size, vec2(@floatFromInt(r.width), @floatFromInt(r.height))); + try sprite_mod.set(game.state().player, .uv_transform, Mat3x3.translate(vec2(@floatFromInt(r.x), @floatFromInt(r.y)))); + sprite_mod.send(.update, .{}); } fn tick( engine: *mach.Engine.Mod, - sprite_mod: *Sprite.Mod, + sprite_mod: *gfx.Sprite.Mod, + sprite_pipeline: *gfx.SpritePipeline.Mod, text_mod: *Text.Mod, game: *Mod, ) !void { @@ -160,7 +161,7 @@ fn tick( try sprite_mod.set(new_entity, .transform, Mat4x4.translate(new_pos).mul(&Mat4x4.scaleScalar(0.3))); try sprite_mod.set(new_entity, .size, vec2(@floatFromInt(r.width), @floatFromInt(r.height))); try sprite_mod.set(new_entity, .uv_transform, Mat3x3.translate(vec2(@floatFromInt(r.x), @floatFromInt(r.y)))); - try sprite_mod.set(new_entity, .pipeline, @intFromEnum(Pipeline.text)); + try sprite_mod.set(new_entity, .pipeline, game.state().pipeline); game.state().sprites += 1; } } @@ -204,15 +205,14 @@ fn tick( &Mat4x4.scale(Vec3.splat(1.0)), ); try sprite_mod.set(game.state().player, .transform, player_transform); - - sprite_mod.send(.updated, .{@intFromEnum(Pipeline.text)}); + sprite_mod.send(.update, .{}); // Perform pre-render work - sprite_mod.send(.pre_render, .{@intFromEnum(Pipeline.text)}); + 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 }}); - sprite_mod.send(.render, .{@intFromEnum(Pipeline.text)}); + sprite_pipeline.send(.render, .{}); engine.send(.end_pass, .{}); engine.send(.frame_done, .{}); // Present the frame diff --git a/examples/glyphs/main.zig b/examples/glyphs/main.zig index 43925255..32f1c6db 100644 --- a/examples/glyphs/main.zig +++ b/examples/glyphs/main.zig @@ -11,6 +11,7 @@ const Text = @import("Text.zig"); pub const modules = .{ mach.Engine, mach.gfx.Sprite, + mach.gfx.SpritePipeline, Text, Game, }; diff --git a/examples/sprite/Game.zig b/examples/sprite/Game.zig index 7fd337cb..54b51fb6 100644 --- a/examples/sprite/Game.zig +++ b/examples/sprite/Game.zig @@ -5,7 +5,7 @@ const assets = @import("assets"); const mach = @import("mach"); const core = mach.core; const gpu = mach.gpu; -const Sprite = mach.gfx.Sprite; +const gfx = mach.gfx; const math = mach.math; const vec2 = math.vec2; @@ -28,6 +28,7 @@ sprites: usize, rand: std.rand.DefaultPrng, time: f32, allocator: std.mem.Allocator, +pipeline: mach.EntityID, const d0 = 0.000001; @@ -49,13 +50,10 @@ pub const global_events = .{ .tick = .{ .handler = tick }, }; -pub const Pipeline = enum(u32) { - default, -}; - fn init( engine: *mach.Engine.Mod, - sprite_mod: *Sprite.Mod, + sprite_mod: *gfx.Sprite.Mod, + sprite_pipeline: *gfx.SpritePipeline.Mod, game: *Mod, ) !void { // The Mach .core is where we set window options, etc. @@ -65,18 +63,19 @@ fn init( // namespace, e.g. the `.mach_gfx_sprite` module could have a 3D `.location` component with a different // type than the `.physics2d` module's `.location` component if you desire. + // Create a sprite rendering pipeline + const allocator = gpa.allocator(); + const pipeline = try engine.newEntity(); + try sprite_pipeline.set(pipeline, .texture, try loadTexture(engine, allocator)); + sprite_pipeline.send(.update, .{}); + + // Create our player sprite const player = try engine.newEntity(); try sprite_mod.set(player, .transform, Mat4x4.translate(vec3(-0.02, 0, 0))); try sprite_mod.set(player, .size, vec2(32, 32)); try sprite_mod.set(player, .uv_transform, Mat3x3.translate(vec2(0, 0))); - try sprite_mod.set(player, .pipeline, @intFromEnum(Pipeline.default)); - - const allocator = gpa.allocator(); - sprite_mod.send(.init_pipeline, .{Sprite.PipelineOptions{ - .pipeline = @intFromEnum(Pipeline.default), - .texture = try loadTexture(engine, allocator), - }}); - sprite_mod.send(.updated, .{@intFromEnum(Pipeline.default)}); + try sprite_mod.set(player, .pipeline, pipeline); + sprite_mod.send(.update, .{}); game.init(.{ .timer = try mach.Timer.start(), @@ -88,12 +87,14 @@ fn init( .rand = std.rand.DefaultPrng.init(1337), .time = 0, .allocator = allocator, + .pipeline = pipeline, }); } fn tick( engine: *mach.Engine.Mod, - sprite_mod: *Sprite.Mod, + sprite_mod: *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. @@ -143,7 +144,7 @@ fn tick( try sprite_mod.set(new_entity, .transform, Mat4x4.translate(new_pos).mul(&Mat4x4.scale(Vec3.splat(0.3)))); try sprite_mod.set(new_entity, .size, vec2(32, 32)); try sprite_mod.set(new_entity, .uv_transform, Mat3x3.translate(vec2(0, 0))); - try sprite_mod.set(new_entity, .pipeline, @intFromEnum(Pipeline.default)); + try sprite_mod.set(new_entity, .pipeline, game.state().pipeline); game.state().sprites += 1; } } @@ -181,14 +182,15 @@ fn tick( player_pos.v[0] += direction.x() * speed * delta_time; player_pos.v[1] += direction.y() * speed * delta_time; try sprite_mod.set(game.state().player, .transform, Mat4x4.translate(player_pos)); - sprite_mod.send(.updated, .{@intFromEnum(Pipeline.default)}); + sprite_mod.send(.update, .{}); // Perform pre-render work - sprite_mod.send(.pre_render, .{@intFromEnum(Pipeline.default)}); + 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 }}); - sprite_mod.send(.render, .{@intFromEnum(Pipeline.default)}); + + sprite_pipeline.send(.render, .{}); engine.send(.end_pass, .{}); engine.send(.frame_done, .{}); // Present the frame diff --git a/examples/sprite/main.zig b/examples/sprite/main.zig index 77757bca..b858ceee 100644 --- a/examples/sprite/main.zig +++ b/examples/sprite/main.zig @@ -10,6 +10,7 @@ const Game = @import("Game.zig"); pub const modules = .{ mach.Engine, mach.gfx.Sprite, + mach.gfx.SpritePipeline, Game, }; diff --git a/src/gfx/Sprite.zig b/src/gfx/Sprite.zig index 1174544c..78d73d5c 100644 --- a/src/gfx/Sprite.zig +++ b/src/gfx/Sprite.zig @@ -2,6 +2,7 @@ const std = @import("std"); const mach = @import("../main.zig"); const core = mach.core; const gpu = mach.core.gpu; +const gfx = mach.gfx; const Engine = mach.Engine; const math = mach.math; @@ -11,22 +12,10 @@ const Vec3 = math.Vec3; 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_sprite; pub const Mod = mach.Mod(@This()); pub const components = .{ - .pipeline = .{ .type = u8, .description = - \\ The ID of the pipeline this sprite belongs to. By default, zero. - \\ - \\ This determines which shader, textures, etc. are used for rendering the sprite. - }, - .transform = .{ .type = Mat4x4, .description = \\ The sprite model transformation matrix. A sprite is measured in pixel units, starting from \\ (0, 0) at the top-left corner and extending to the size of the sprite. By default, the world @@ -43,264 +32,45 @@ pub const components = .{ .size = .{ .type = Vec2, .description = \\ The size of the sprite, in pixels. }, -}; -pub const global_events = .{ - .deinit = .{ .handler = deinit }, - .init = .{ .handler = init }, + .pipeline = .{ .type = mach.EntityID, .description = + \\ Which render pipeline to use for rendering the sprite. + \\ + \\ This determines which shader, textures, etc. are used for rendering the sprite. + }, }; 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 sprite texture in pixels - texture_size: Vec2 align(16), -}; - -const Pipeline = struct { - render: *gpu.RenderPipeline, - texture_sampler: *gpu.Sampler, - texture: *gpu.Texture, - texture2: ?*gpu.Texture, - texture3: ?*gpu.Texture, - texture4: ?*gpu.Texture, - bind_group: *gpu.BindGroup, - uniforms: *gpu.Buffer, - - // Storage buffers - num_sprites: u32, - transforms: *gpu.Buffer, - uv_transforms: *gpu.Buffer, - sizes: *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.uv_transforms.reference(); - p.sizes.reference(); +fn update(engine: *Engine.Mod, sprite_mod: *Mod, sprite_pipeline: *gfx.SpritePipeline.Mod) !void { + var archetypes_iter = sprite_pipeline.entities.query(.{ .all = &.{ + .{ .mach_gfx_sprite_pipeline = &.{ + .built, + } }, + } }); + while (archetypes_iter.next()) |archetype| { + 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_mod, sprite_pipeline, pipeline_id, built); + } } - - pub fn deinit(p: *Pipeline) void { - p.render.release(); - p.texture_sampler.release(); - p.texture.release(); - 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.transforms.release(); - p.uv_transforms.release(); - p.sizes.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. - texture: *gpu.Texture, - 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 init(sprite_mod: *Mod) void { - sprite_mod.init(.{ - .allocator = gpa.allocator(), - }); } -fn deinit(sprite_mod: *Mod) !void { - for (sprite_mod.state().pipelines.entries.items(.value)) |*pipeline| pipeline.deinit(); - sprite_mod.state().pipelines.deinit(sprite_mod.state().allocator); -} - -fn initPipeline( +fn updatePipeline( engine: *Engine.Mod, sprite_mod: *Mod, - opt: PipelineOptions, + sprite_pipeline: *gfx.SpritePipeline.Mod, + pipeline_id: mach.EntityID, + built: *gfx.SpritePipeline.BuiltPipeline, ) !void { const device = engine.state().device; + const encoder = device.createCommandEncoder(null); + defer encoder.release(); - const pipeline = try sprite_mod.state().pipelines.getOrPut(sprite_mod.state().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_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(); -} - -fn updated( - engine: *Engine.Mod, - sprite_mod: *Mod, - 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 = &.{ + var archetypes_iter = sprite_mod.entities.query(.{ .all = &.{ .{ .mach_gfx_sprite = &.{ .uv_transform, .transform, @@ -308,77 +78,38 @@ fn updated( .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; + var num_sprites: u32 = 0; + var i: usize = 0; while (archetypes_iter.next()) |archetype| { const transforms = archetype.slice(.mach_gfx_sprite, .transform); const uv_transforms = archetype.slice(.mach_gfx_sprite, .uv_transform); const sizes = archetype.slice(.mach_gfx_sprite, .size); + const pipelines = archetype.slice(.mach_gfx_sprite, .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.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); + // TODO: currently we cannot query all sprites which have a _single_ pipeline component + // value and get back contiguous memory for all of them. This is because all sprites 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 (transforms, uv_transforms, sizes, pipelines) |transform, uv_transform, size, sprite_pipeline_id| { + if (sprite_pipeline_id == pipeline_id) { + gfx.SpritePipeline.cp_transforms[i] = transform; + gfx.SpritePipeline.cp_uv_transforms[i] = uv_transform; + gfx.SpritePipeline.cp_sizes[i] = size; + i += 1; + num_sprites += 1; + } + } } - var command = encoder.finish(null); - defer command.release(); - - engine.state().queue.submit(&[_]*gpu.CommandBuffer{command}); -} - -fn preRender( - engine: *Engine.Mod, - sprite_mod: *Mod, - pipeline_id: u32, -) !void { - const pipeline = sprite_mod.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}); -} - -fn render( - engine: *Engine.Mod, - sprite_mod: *Mod, - 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); + try sprite_pipeline.set(pipeline_id, .num_sprites, num_sprites); + if (num_sprites > 0) { + encoder.writeBuffer(built.transforms, 0, gfx.SpritePipeline.cp_transforms[0..i]); + encoder.writeBuffer(built.uv_transforms, 0, gfx.SpritePipeline.cp_uv_transforms[0..i]); + 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}); + } } diff --git a/src/gfx/SpritePipeline.zig b/src/gfx/SpritePipeline.zig new file mode 100644 index 00000000..2c4526a7 --- /dev/null +++ b/src/gfx/SpritePipeline.zig @@ -0,0 +1,373 @@ +const std = @import("std"); +const mach = @import("../main.zig"); +const core = mach.core; + +const gpu = mach.gpu; +const math = mach.math; + +pub const name = .mach_gfx_sprite_pipeline; +pub const Mod = mach.Mod(@This()); + +pub const components = .{ + .texture = .{ .type = *gpu.Texture, .description = + \\ Texture to use when rendering. The default shader can handle only one texture input. + \\ Must be specified for a pipeline entity to be valid. + }, + + .texture2 = .{ .type = *gpu.Texture }, + .texture3 = .{ .type = *gpu.Texture }, + .texture4 = .{ .type = *gpu.Texture }, + + .shader = .{ .type = *gpu.ShaderModule, .description = + \\ Shader program to use when rendering + \\ Defaults to sprite.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_sprites = .{ .type = u32, .description = + \\ Number of sprites this pipeline will render. + \\ Read-only, updated as part of Sprite.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 sprite texture in pixels + texture_size: math.Vec2 align(16), +}; + +const sprite_buffer_cap = 1024 * 512; // TODO(sprite): allow user to specify preallocation + +// TODO(sprite): eliminate these, see Sprite.updatePipeline for details on why these exist +// currently. +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; + +pub const BuiltPipeline = struct { + render: *gpu.RenderPipeline, + texture_sampler: *gpu.Sampler, + texture: *gpu.Texture, + texture2: ?*gpu.Texture, + texture3: ?*gpu.Texture, + texture4: ?*gpu.Texture, + bind_group: *gpu.BindGroup, + uniforms: *gpu.Buffer, + + // Storage buffers + transforms: *gpu.Buffer, + uv_transforms: *gpu.Buffer, + sizes: *gpu.Buffer, + + pub fn reference(p: *BuiltPipeline) 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.uv_transforms.reference(); + p.sizes.reference(); + } + + pub fn deinit(p: *BuiltPipeline) void { + p.render.release(); + p.texture_sampler.release(); + p.texture.release(); + 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.transforms.release(); + p.uv_transforms.release(); + p.sizes.release(); + } +}; + +fn deinit(sprite_pipeline: *Mod) void { + var archetypes_iter = sprite_pipeline.entities.query(.{ .all = &.{ + .{ .mach_gfx_sprite_pipeline = &.{ + .built, + } }, + } }); + while (archetypes_iter.next()) |archetype| { + for (archetype.slice(.mach_gfx_sprite_pipeline, .built)) |*p| p.deinit(); + } +} + +fn update(engine: *mach.Engine.Mod, sprite_pipeline: *Mod) !void { + // Destroy all sprite render pipelines. We will rebuild them all. + deinit(sprite_pipeline); + + var archetypes_iter = sprite_pipeline.entities.query(.{ .all = &.{ + .{ .mach_gfx_sprite_pipeline = &.{ + .texture, + } }, + } }); + while (archetypes_iter.next()) |archetype| { + const ids = archetype.slice(.entity, .id); + const textures = archetype.slice(.mach_gfx_sprite_pipeline, .texture); + + for (ids, textures) |pipeline_id, texture| { + try buildPipeline(engine, sprite_pipeline, pipeline_id, texture); + } + } +} + +fn buildPipeline( + engine: *mach.Engine.Mod, + sprite_pipeline: *Mod, + pipeline_id: mach.EntityID, + texture: *gpu.Texture, +) !void { + const opt_texture2 = sprite_pipeline.get(pipeline_id, .texture2); + const opt_texture3 = sprite_pipeline.get(pipeline_id, .texture3); + const opt_texture4 = sprite_pipeline.get(pipeline_id, .texture4); + const opt_shader = sprite_pipeline.get(pipeline_id, .shader); + const opt_texture_sampler = sprite_pipeline.get(pipeline_id, .texture_sampler); + const opt_blend_state = sprite_pipeline.get(pipeline_id, .blend_state); + const opt_bind_group_layout = sprite_pipeline.get(pipeline_id, .bind_group_layout); + const opt_bind_group = sprite_pipeline.get(pipeline_id, .bind_group); + const opt_color_target_state = sprite_pipeline.get(pipeline_id, .color_target_state); + 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; + + // Storage buffers + const transforms = device.createBuffer(&.{ + .usage = .{ .storage = true, .copy_dst = true }, + .size = @sizeOf(math.Mat4x4) * sprite_buffer_cap, + .mapped_at_creation = .false, + }); + const uv_transforms = device.createBuffer(&.{ + .usage = .{ .storage = true, .copy_dst = true }, + .size = @sizeOf(math.Mat3x3) * sprite_buffer_cap, + .mapped_at_creation = .false, + }); + const sizes = device.createBuffer(&.{ + .usage = .{ .storage = true, .copy_dst = true }, + .size = @sizeOf(math.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 = 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(math.Mat4x4) * sprite_buffer_cap), + gpu.BindGroup.Entry.buffer(2, uv_transforms, 0, @sizeOf(math.Mat3x3) * sprite_buffer_cap), + gpu.BindGroup.Entry.buffer(3, sizes, 0, @sizeOf(math.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_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, + .texture2 = opt_texture2, + .texture3 = opt_texture3, + .texture4 = opt_texture4, + .bind_group = bind_group, + .uniforms = uniforms, + .transforms = transforms, + .uv_transforms = uv_transforms, + .sizes = sizes, + }; + built.reference(); + try sprite_pipeline.set(pipeline_id, .built, built); + try sprite_pipeline.set(pipeline_id, .num_sprites, 0); +} + +fn preRender( + engine: *mach.Engine.Mod, + sprite_pipeline: *Mod, +) void { + var archetypes_iter = sprite_pipeline.entities.query(.{ .all = &.{ + .{ .mach_gfx_sprite_pipeline = &.{ + .built, + } }, + } }); + while (archetypes_iter.next()) |archetype| { + const built_pipelines = archetype.slice(.mach_gfx_sprite_pipeline, .built); + for (built_pipelines) |built| { + // 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, + .near = -0.1, + .far = 100000, + }); + + // Update uniform buffer + const uniforms = Uniforms{ + .view_projection = proj, + // TODO(sprite): dimensions of other textures, number of textures present + .texture_size = math.vec2( + @as(f32, @floatFromInt(built.texture.getWidth())), + @as(f32, @floatFromInt(built.texture.getHeight())), + ), + }; + engine.state().encoder.writeBuffer(built.uniforms, 0, &[_]Uniforms{uniforms}); + } + } +} + +fn render( + engine: *mach.Engine.Mod, + sprite_pipeline: *Mod, +) !void { + var archetypes_iter = sprite_pipeline.entities.query(.{ .all = &.{ + .{ .mach_gfx_sprite_pipeline = &.{ + .built, + } }, + } }); + while (archetypes_iter.next()) |archetype| { + const ids = archetype.slice(.entity, .id); + 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); + // TODO(sprite): remove dynamic offsets? + pass.setBindGroup(0, built.bind_group, &.{}); + pass.draw(total_vertices, 1, 0, 0); + } + } +} diff --git a/src/gfx/main.zig b/src/gfx/main.zig index 5b52b51b..2fda105c 100644 --- a/src/gfx/main.zig +++ b/src/gfx/main.zig @@ -3,6 +3,7 @@ pub const Atlas = @import("atlas/Atlas.zig"); // ECS modules pub const Sprite = @import("Sprite.zig"); +pub const SpritePipeline = @import("SpritePipeline.zig"); pub const Text = @import("Text.zig"); pub const TextStyle = @import("TextStyle.zig"); diff --git a/src/main.zig b/src/main.zig index 6284629e..efcf3675 100644 --- a/src/main.zig +++ b/src/main.zig @@ -32,7 +32,7 @@ pub const modules = blk: { pub const ModSet = @import("module/main.zig").ModSet; pub const Modules = @import("module/main.zig").Modules(modules); pub const Mod = ModSet(modules).Mod; -pub const EntityID = @import("module/main.zig").EntityID; +pub const EntityID = @import("module/main.zig").EntityID; // TODO: rename to just Entity? pub const Archetype = @import("module/main.zig").Archetype; test {