diff --git a/src/gfx/Sprite.zig b/src/gfx/Sprite.zig index 44ddd2ab..b0385ff5 100644 --- a/src/gfx/Sprite.zig +++ b/src/gfx/Sprite.zig @@ -13,102 +13,394 @@ const Vec3 = math.Vec3; const Mat3x3 = math.Mat3x3; const Mat4x4 = math.Mat4x4; +const Sprite = @This(); + pub const mach_module = .mach_gfx_sprite; -// TODO(object) -pub const components = .{ - .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 - \\ origin (0, 0) lives at the center of the window. - \\ - \\ Example: in a 500px by 500px window, a sprite located at (0, 0) with size (250, 250) will - \\ cover the top-right hand corner of the window. - }, +pub const mach_systems = .{.tick}; - .uv_transform = .{ .type = Mat3x3, .description = - \\ UV coordinate transformation matrix describing top-left corner / origin of sprite, in pixels. - }, +const Uniforms = extern struct { + /// The view * orthographic projection matrix + view_projection: math.Mat4x4 align(16), - .size = .{ .type = Vec2, .description = - \\ The size of the sprite, in pixels. - }, - - .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. - }, + /// Total size of the sprite texture in pixels + texture_size: math.Vec2 align(16), }; -pub const systems = .{ - .update = .{ .handler = update }, +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 deinit(p: *const 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 update( - entities: *mach.Entities.Mod, - core: *mach.Core.Mod, - sprite_pipeline: *gfx.SpritePipeline.Mod, -) !void { - var q = try entities.query(.{ - .ids = mach.Entities.Mod.read(.id), - .built_pipelines = gfx.SpritePipeline.Mod.read(.built), - }); - while (q.next()) |v| { - for (v.ids, v.built_pipelines) |pipeline_id, built| { - try updatePipeline(entities, core, sprite_pipeline, pipeline_id, &built); +const sprite_buffer_cap = 1024 * 512; // TODO(sprite): allow user to specify preallocation + +pub var cp_transforms: [sprite_buffer_cap]math.Mat4x4 = undefined; +// TODO(d3d12): uv_transform should be a Mat3x3 but our D3D12/HLSL backend cannot handle it. +pub var cp_uv_transforms: [sprite_buffer_cap]math.Mat4x4 = undefined; +pub var cp_sizes: [sprite_buffer_cap]math.Vec2 = undefined; + +sprites: mach.Objects(.{ .track_fields = true }, struct { + /// 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 + /// origin (0, 0) lives at the center of the window. + /// + /// Example: in a 500px by 500px window, a sprite located at (0, 0) with size (250, 250) will + /// cover the top-right hand corner of the window. + transform: Mat4x4, + + /// UV coordinate transformation matrix describing top-left corner / origin of sprite, in pixels. + uv_transform: Mat3x3, + + /// The size of the sprite, in pixels. + size: Vec2, +}), + +/// A sprite pipeline renders all sprites that are parented to it. +pipelines: mach.Objects(.{ .track_fields = true }, struct { + /// Which window (device/queue) to use. If not set, this pipeline will not be rendered. + window: ?mach.ObjectID = null, + + /// Which render pass should be used during rendering. If not set, this pipeline will not be + /// rendered. + render_pass: ?*gpu.RenderPassEncoder = null, + + /// Texture to use when rendering. The default shader can handle only one texture input. + /// Must be specified for a pipeline entity to be valid. + texture: *gpu.Texture, + + /// View*Projection matrix to use when rendering text with this pipeline. This controls both + /// the size of the 'virtual canvas' which is rendered onto, as well as the 'camera position'. + /// + /// This should be configured before .pre_render runs. + /// + /// By default, the size is configured to be equal to the window size in virtual pixels (e.g. + /// if the window size is 1920x1080, the virtual canvas will also be that size even if ran on a + /// HiDPI / Retina display where the actual framebuffer is larger than that.) The origin (0, 0) + /// is configured to be the center of the window: + /// + /// ``` + /// const width_px: f32 = @floatFromInt(window.width); + /// const height_px: f32 = @floatFromInt(window.height); + /// const projection = math.Mat4x4.projection2D(.{ + /// .left = -width_px / 2.0, + /// .right = width_px / 2.0, + /// .bottom = -height_px / 2.0, + /// .top = height_px / 2.0, + /// .near = -0.1, + /// .far = 100000, + /// }); + /// const view_projection = projection.mul(&Mat4x4.translate(vec3(0, 0, 0))); + /// try sprite_pipeline.set(my_sprite_pipeline, .view_projection, view_projection); + /// ``` + view_projection: ?Mat4x4 = null, + + /// Optional multi-texturing. + texture2: ?*gpu.Texture = null, + texture3: ?*gpu.Texture = null, + texture4: ?*gpu.Texture = null, + + /// Shader program to use when rendering + /// + /// If null, defaults to sprite.wgsl + shader: ?*gpu.ShaderModule = null, + + /// Whether to use linear (blurry) or nearest (pixelated) upscaling/downscaling. + /// + /// If null, defaults to nearest (pixelated) + texture_sampler: ?*gpu.Sampler = null, + + /// Alpha and color blending options + /// + /// If null, defaults to + /// .{ + /// .color = .{ .operation = .add, .src_factor = .src_alpha .dst_factor = .one_minus_src_alpha }, + /// .alpha = .{ .operation = .add, .src_factor = .one, .dst_factor = .zero }, + /// } + blend_state: ?gpu.BlendState = null, + + /// Override to enable passing additional data to your shader program. + bind_group_layout: ?*gpu.BindGroupLayout = null, + + /// Override to enable passing additional data to your shader program. + bind_group: ?*gpu.BindGroup = null, + + /// Override to enable custom color target state for render pipeline. + color_target_state: ?gpu.ColorTargetState = null, + + /// Override to enable custom fragment state for render pipeline. + fragment_state: ?gpu.FragmentState = null, + + /// Override to enable custom pipeline layout. + layout: ?*gpu.PipelineLayout = null, + + /// Number of sprites this pipeline will render. + /// Read-only, updated as part of Sprite.update + num_sprites: u32 = 0, + + /// Internal pipeline state. + built: ?BuiltPipeline = null, +}), + +pub fn tick(sprite: *Sprite, core: *mach.Core) !void { + var pipelines = sprite.pipelines.slice(); + while (pipelines.next()) |pipeline_id| { + // Is this pipeline usable for rendering? If not, no need to process it. + var pipeline = sprite.pipelines.getValue(pipeline_id); + if (pipeline.window == null or pipeline.render_pass == null) continue; + + // Changing these fields shouldn't trigger a pipeline rebuild, so clear their update values: + _ = sprite.pipelines.updated(pipeline_id, .window); + _ = sprite.pipelines.updated(pipeline_id, .render_pass); + _ = sprite.pipelines.updated(pipeline_id, .view_projection); + + // If any other fields of the pipeline have been updated, a pipeline rebuild is required. + if (sprite.pipelines.anyUpdated(pipeline_id)) { + rebuildPipeline(core, sprite, pipeline_id); } + + // Find sprites parented to this pipeline. + var pipeline_children = try sprite.pipelines.getChildren(pipeline_id); + defer pipeline_children.deinit(); + + // If any sprites were updated, we update the pipeline's storage buffers to have the new + // information for all its sprites. + const any_sprites_updated = blk: { + for (pipeline_children.items) |sprite_id| { + if (!sprite.sprites.is(sprite_id)) continue; + if (sprite.sprites.anyUpdated(sprite_id)) break :blk true; + } + break :blk false; + }; + if (any_sprites_updated) updatePipelineSprites(sprite, core, pipeline_id, pipeline_children.items); + + // Do we actually have any sprites to render? + pipeline = sprite.pipelines.getValue(pipeline_id); + if (pipeline.num_sprites == 0) continue; + + // TODO(sprite): need a way to specify order of rendering with multiple pipelines + renderSprites(sprite, core, pipeline_id); } } -fn updatePipeline( - entities: *mach.Entities.Mod, - core: *mach.Core.Mod, - sprite_pipeline: *gfx.SpritePipeline.Mod, - pipeline_id: mach.EntityID, - built: *const gfx.SpritePipeline.BuiltPipeline, -) !void { - const device = core.state().device; - const label = @tagName(name) ++ ".updatePipeline"; +fn rebuildPipeline( + core: *mach.Core, + sprite: *Sprite, + pipeline_id: mach.ObjectID, +) void { + // Destroy the current pipeline, if built. + var pipeline = sprite.pipelines.getValue(pipeline_id); + defer sprite.pipelines.setValueRaw(pipeline_id, pipeline); + if (pipeline.built) |built| built.deinit(); + + // Reference any user-provided objects. + pipeline.texture.reference(); + if (pipeline.texture2) |v| v.reference(); + if (pipeline.texture3) |v| v.reference(); + if (pipeline.texture4) |v| v.reference(); + if (pipeline.shader) |v| v.reference(); + if (pipeline.texture_sampler) |v| v.reference(); + if (pipeline.bind_group_layout) |v| v.reference(); + if (pipeline.bind_group) |v| v.reference(); + if (pipeline.layout) |v| v.reference(); + + const window = core.windows.getValue(pipeline.window.?); + const device = window.device; + + const label = @tagName(mach_module) ++ ".rebuildPipeline"; + + // Storage buffers + const transforms = device.createBuffer(&.{ + .label = label ++ " transforms", + .usage = .{ .storage = true, .copy_dst = true }, + .size = @sizeOf(math.Mat4x4) * sprite_buffer_cap, + .mapped_at_creation = .false, + }); + const uv_transforms = device.createBuffer(&.{ + .label = label ++ " uv_transforms", + .usage = .{ .storage = true, .copy_dst = true }, + // TODO(d3d12): uv_transform should be a Mat3x3 but our D3D12/HLSL backend cannot handle it. + .size = @sizeOf(math.Mat4x4) * sprite_buffer_cap, + .mapped_at_creation = .false, + }); + const sizes = device.createBuffer(&.{ + .label = label ++ " sizes", + .usage = .{ .storage = true, .copy_dst = true }, + .size = @sizeOf(math.Vec2) * sprite_buffer_cap, + .mapped_at_creation = .false, + }); + + const texture_sampler = pipeline.texture_sampler orelse device.createSampler(&.{ + .label = label ++ " sampler", + .mag_filter = .nearest, + .min_filter = .nearest, + }); + const uniforms = device.createBuffer(&.{ + .label = label ++ " uniforms", + .usage = .{ .copy_dst = true, .uniform = true }, + .size = @sizeOf(Uniforms), + .mapped_at_creation = .false, + }); + const bind_group_layout = pipeline.bind_group_layout orelse device.createBindGroupLayout( + &gpu.BindGroupLayout.Descriptor.init(.{ + .label = label, + .entries = &.{ + gpu.BindGroupLayout.Entry.initBuffer(0, .{ .vertex = true }, .uniform, false, 0), + gpu.BindGroupLayout.Entry.initBuffer(1, .{ .vertex = true }, .read_only_storage, false, 0), + gpu.BindGroupLayout.Entry.initBuffer(2, .{ .vertex = true }, .read_only_storage, false, 0), + gpu.BindGroupLayout.Entry.initBuffer(3, .{ .vertex = true }, .read_only_storage, false, 0), + gpu.BindGroupLayout.Entry.initSampler(4, .{ .fragment = true }, .filtering), + gpu.BindGroupLayout.Entry.initTexture(5, .{ .fragment = true }, .float, .dimension_2d, false), + gpu.BindGroupLayout.Entry.initTexture(6, .{ .fragment = true }, .float, .dimension_2d, false), + gpu.BindGroupLayout.Entry.initTexture(7, .{ .fragment = true }, .float, .dimension_2d, false), + gpu.BindGroupLayout.Entry.initTexture(8, .{ .fragment = true }, .float, .dimension_2d, false), + }, + }), + ); + defer bind_group_layout.release(); + + const texture_view = pipeline.texture.createView(&gpu.TextureView.Descriptor{ .label = label }); + const texture2_view = if (pipeline.texture2) |tex| tex.createView(&gpu.TextureView.Descriptor{ .label = label }) else texture_view; + const texture3_view = if (pipeline.texture3) |tex| tex.createView(&gpu.TextureView.Descriptor{ .label = label }) else texture_view; + const texture4_view = if (pipeline.texture4) |tex| tex.createView(&gpu.TextureView.Descriptor{ .label = label }) else texture_view; + defer texture_view.release(); + // TODO: texture views 2-4 leak + + const bind_group = pipeline.bind_group orelse device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .label = label, + .layout = bind_group_layout, + .entries = &.{ + gpu.BindGroup.Entry.initBuffer(0, uniforms, 0, @sizeOf(Uniforms), @sizeOf(Uniforms)), + gpu.BindGroup.Entry.initBuffer(1, transforms, 0, @sizeOf(math.Mat4x4) * sprite_buffer_cap, @sizeOf(math.Mat4x4)), + gpu.BindGroup.Entry.initBuffer(2, uv_transforms, 0, @sizeOf(math.Mat3x3) * sprite_buffer_cap, @sizeOf(math.Mat3x3)), + gpu.BindGroup.Entry.initBuffer(3, sizes, 0, @sizeOf(math.Vec2) * sprite_buffer_cap, @sizeOf(math.Vec2)), + gpu.BindGroup.Entry.initSampler(4, texture_sampler), + gpu.BindGroup.Entry.initTextureView(5, texture_view), + gpu.BindGroup.Entry.initTextureView(6, texture2_view), + gpu.BindGroup.Entry.initTextureView(7, texture3_view), + gpu.BindGroup.Entry.initTextureView(8, texture4_view), + }, + }), + ); + + const blend_state = pipeline.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 = pipeline.shader orelse device.createShaderModuleWGSL("sprite.wgsl", @embedFile("sprite.wgsl")); + defer shader_module.release(); + + const color_target = pipeline.color_target_state orelse gpu.ColorTargetState{ + .format = window.framebuffer_format, + .blend = &blend_state, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + const fragment = pipeline.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 = pipeline.layout orelse device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{ + .label = label, + .bind_group_layouts = &bind_group_layouts, + })); + defer pipeline_layout.release(); + const render_pipeline = device.createRenderPipeline(&gpu.RenderPipeline.Descriptor{ + .label = label, + .fragment = &fragment, + .layout = pipeline_layout, + .vertex = gpu.VertexState{ + .module = shader_module, + .entry_point = "vertMain", + }, + }); + + pipeline.built = BuiltPipeline{ + .render = render_pipeline, + .texture_sampler = texture_sampler, + .texture = pipeline.texture, + .texture2 = pipeline.texture2, + .texture3 = pipeline.texture3, + .texture4 = pipeline.texture4, + .bind_group = bind_group, + .uniforms = uniforms, + .transforms = transforms, + .uv_transforms = uv_transforms, + .sizes = sizes, + }; + pipeline.num_sprites = 0; +} + +fn updatePipelineSprites( + sprite: *Sprite, + core: *mach.Core, + pipeline_id: mach.ObjectID, + pipeline_children: []const mach.ObjectID, +) void { + const pipeline = sprite.pipelines.getValue(pipeline_id); + const window = core.windows.getValue(pipeline.window.?); + const device = window.device; + + const label = @tagName(mach_module) ++ ".updatePipelineSprites"; const encoder = device.createCommandEncoder(&.{ .label = label }); defer encoder.release(); - var num_sprites: u32 = 0; - var i: usize = 0; - var q = try entities.query(.{ - .transforms = Mod.read(.transform), - .uv_transforms = Mod.read(.uv_transform), - .sizes = Mod.read(.size), - .pipelines = Mod.read(.pipeline), - }); - while (q.next()) |v| { - for (v.transforms, v.uv_transforms, v.sizes, v.pipelines) |transform, uv_transform, size, sprite_pipeline_id| { - // 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. - if (sprite_pipeline_id == pipeline_id) { - const uv = uv_transform; - gfx.SpritePipeline.cp_transforms[i] = transform; - // TODO(d3d12): #1217 - // changed the uv_transform to 4x4. The 3x3 causes issues with d3d12/hlsl - gfx.SpritePipeline.cp_uv_transforms[i].v[0] = vec4(uv.v[0].x(), uv.v[0].y(), uv.v[0].z(), 0.0); - gfx.SpritePipeline.cp_uv_transforms[i].v[1] = vec4(uv.v[1].x(), uv.v[1].y(), uv.v[1].z(), 0.0); - gfx.SpritePipeline.cp_uv_transforms[i].v[2] = vec4(uv.v[2].x(), uv.v[2].y(), uv.v[2].z(), 0.0); - gfx.SpritePipeline.cp_uv_transforms[i].v[3] = vec4(0.0, 0.0, 0.0, 0.0); - gfx.SpritePipeline.cp_sizes[i] = size; - i += 1; - num_sprites += 1; - } - } + var i: u32 = 0; + for (pipeline_children) |sprite_id| { + if (!sprite.sprites.is(sprite_id)) continue; + const s = sprite.sprites.getValue(sprite_id); + + cp_transforms[i] = s.transform; + + // TODO(d3d12): uv_transform should be a Mat3x3 but our D3D12/HLSL backend cannot handle it. + const uv = s.uv_transform; + cp_uv_transforms[i].v[0] = vec4(uv.v[0].x(), uv.v[0].y(), uv.v[0].z(), 0.0); + cp_uv_transforms[i].v[1] = vec4(uv.v[1].x(), uv.v[1].y(), uv.v[1].z(), 0.0); + cp_uv_transforms[i].v[2] = vec4(uv.v[2].x(), uv.v[2].y(), uv.v[2].z(), 0.0); + cp_uv_transforms[i].v[3] = vec4(0.0, 0.0, 0.0, 0.0); + cp_sizes[i] = s.size; + i += 1; } // Sort sprites back-to-front for draw order, alpha blending const Context = struct { - // TODO(d3d12): #1217 - // changed the uv_transform to 4x4. The 3x3 causes issues with d3d12/hlsl + // TODO(d3d12): uv_transform should be a Mat3x3 but our D3D12/HLSL backend cannot handle it. transforms: []Mat4x4, uv_transforms: []Mat4x4, sizes: []Vec2, @@ -122,27 +414,72 @@ fn updatePipeline( pub fn swap(ctx: @This(), a: usize, b: usize) void { std.mem.swap(Mat4x4, &ctx.transforms[a], &ctx.transforms[b]); - // TODO(d3d12): #1217 - // changed the uv_transform to 4x4. The 3x3 causes issues with d3d12/hlsl + // TODO(d3d12): uv_transform should be a Mat3x3 but our D3D12/HLSL backend cannot handle it. std.mem.swap(Mat4x4, &ctx.uv_transforms[a], &ctx.uv_transforms[b]); std.mem.swap(Vec2, &ctx.sizes[a], &ctx.sizes[b]); } }; std.sort.pdqContext(0, i, Context{ - .transforms = gfx.SpritePipeline.cp_transforms[0..i], - .uv_transforms = gfx.SpritePipeline.cp_uv_transforms[0..i], - .sizes = gfx.SpritePipeline.cp_sizes[0..i], + .transforms = cp_transforms[0..i], + .uv_transforms = cp_uv_transforms[0..i], + .sizes = cp_sizes[0..i], }); - // TODO: optimize by removing this component set call and instead use a .write() query - 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]); + sprite.pipelines.set(pipeline_id, .num_sprites, i); + if (i > 0) { + encoder.writeBuffer(pipeline.built.?.transforms, 0, cp_transforms[0..i]); + encoder.writeBuffer(pipeline.built.?.uv_transforms, 0, cp_uv_transforms[0..i]); + encoder.writeBuffer(pipeline.built.?.sizes, 0, cp_sizes[0..i]); var command = encoder.finish(&.{ .label = label }); defer command.release(); - core.state().queue.submit(&[_]*gpu.CommandBuffer{command}); + window.queue.submit(&[_]*gpu.CommandBuffer{command}); } } + +fn renderSprites( + sprite: *Sprite, + core: *mach.Core, + pipeline_id: mach.ObjectID, +) void { + const pipeline = sprite.pipelines.getValue(pipeline_id); + const window = core.windows.getValue(pipeline.window.?); + const device = window.device; + + const label = @tagName(mach_module) ++ ".renderSprites"; + const encoder = device.createCommandEncoder(&.{ .label = label }); + defer encoder.release(); + + // Update uniform buffer + const view_projection = pipeline.view_projection orelse blk: { + const width_px: f32 = @floatFromInt(window.width); + const height_px: f32 = @floatFromInt(window.height); + break :blk math.Mat4x4.projection2D(.{ + .left = -width_px / 2, + .right = width_px / 2, + .bottom = -height_px / 2, + .top = height_px / 2, + .near = -0.1, + .far = 100000, + }); + }; + const uniforms = Uniforms{ + .view_projection = view_projection, + // TODO(sprite): dimensions of multi-textures, number of multi-textures present + .texture_size = math.vec2( + @as(f32, @floatFromInt(pipeline.built.?.texture.getWidth())), + @as(f32, @floatFromInt(pipeline.built.?.texture.getHeight())), + ), + }; + encoder.writeBuffer(pipeline.built.?.uniforms, 0, &[_]Uniforms{uniforms}); + var command = encoder.finish(&.{ .label = label }); + defer command.release(); + window.queue.submit(&[_]*gpu.CommandBuffer{command}); + + // Draw the sprite batch + const total_vertices = pipeline.num_sprites * 6; + pipeline.render_pass.?.setPipeline(pipeline.built.?.render); + // TODO(sprite): can we remove unused dynamic offsets? + pipeline.render_pass.?.setBindGroup(0, pipeline.built.?.bind_group, &.{}); + pipeline.render_pass.?.draw(total_vertices, 1, 0, 0); +} diff --git a/src/gfx/SpritePipeline.zig b/src/gfx/SpritePipeline.zig deleted file mode 100644 index 907f0ffe..00000000 --- a/src/gfx/SpritePipeline.zig +++ /dev/null @@ -1,401 +0,0 @@ -const std = @import("std"); -const mach = @import("../main.zig"); - -const gpu = mach.gpu; -const math = mach.math; - -pub const mach_module = .mach_gfx_sprite_pipeline; - -// TODO(object) -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 }, - - .view_projection = .{ .type = math.Mat4x4, .description = - \\ View*Projection matrix to use when rendering text with this pipeline. This controls both - \\ the size of the 'virtual canvas' which is rendered onto, as well as the 'camera position'. - \\ - \\ This should be configured before .pre_render runs. - \\ - \\ By default, the size is configured to be equal to the window size in virtual pixels (e.g. - \\ if the window size is 1920x1080, the virtual canvas will also be that size even if ran on a - \\ HiDPI / Retina display where the actual framebuffer is larger than that.) The origin (0, 0) - \\ is configured to be the center of the window: - \\ - \\ ``` - \\ const width_px: f32 = @floatFromInt(core.state().size().width); - \\ const height_px: f32 = @floatFromInt(core.state().size().height); - \\ const projection = math.Mat4x4.projection2D(.{ - \\ .left = -width_px / 2.0, - \\ .right = width_px / 2.0, - \\ .bottom = -height_px / 2.0, - \\ .top = height_px / 2.0, - \\ .near = -0.1, - \\ .far = 100000, - \\ }); - \\ const view_projection = projection.mul(&Mat4x4.translate(vec3(0, 0, 0))); - \\ try sprite_pipeline.set(my_sprite_pipeline, .view_projection, view_projection); - \\ ``` - }, - - .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 systems = .{ - .init = .{ .handler = init }, - .deinit = .{ .handler = deinit }, - .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; -// TODO(d3d12): #1217 -// changed the uv_transform to 4x4. The 3x3 causes issues with d3d12/hlsl -pub var cp_uv_transforms: [sprite_buffer_cap]math.Mat4x4 = 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, - 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 deinit(p: *const 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 init(sprite_pipeline: *Mod) void { - sprite_pipeline.init(.{}); -} - -fn deinit(entities: *mach.Entities.Mod) !void { - var q = try entities.query(.{ - .built_pipelines = Mod.read(.built), - }); - while (q.next()) |v| { - for (v.built_pipelines) |built| { - built.deinit(); - } - } -} - -fn update(entities: *mach.Entities.Mod, core: *mach.Core.Mod, sprite_pipeline: *Mod) !void { - // Destroy all sprite render pipelines. We will rebuild them all. - try deinit(entities); - - var q = try entities.query(.{ - .ids = mach.Entities.Mod.read(.id), - .textures = Mod.read(.texture), - }); - while (q.next()) |v| { - for (v.ids, v.textures) |pipeline_id, texture| { - try buildPipeline(core, sprite_pipeline, pipeline_id, texture); - } - } -} - -fn buildPipeline( - core: *mach.Core.Mod, - sprite_pipeline: *Mod, - pipeline_id: mach.EntityID, - texture: *gpu.Texture, -) !void { - // TODO: optimize by removing the component get/set calls in this function where possible - // and instead use .write() queries - 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 = core.state().device; - const label = @tagName(name) ++ ".buildPipeline"; - - // Storage buffers - const transforms = device.createBuffer(&.{ - .label = label ++ " transforms", - .usage = .{ .storage = true, .copy_dst = true }, - .size = @sizeOf(math.Mat4x4) * sprite_buffer_cap, - .mapped_at_creation = .false, - }); - const uv_transforms = device.createBuffer(&.{ - .label = label ++ " uv_transforms", - .usage = .{ .storage = true, .copy_dst = true }, - // TODO(d3d12): #1217 - // changed the uv_transform to 4x4. The 3x3 causes issues with d3d12/hlsl - .size = @sizeOf(math.Mat4x4) * sprite_buffer_cap, - .mapped_at_creation = .false, - }); - const sizes = device.createBuffer(&.{ - .label = label ++ " sizes", - .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(&.{ - .label = label ++ " sampler", - .mag_filter = .nearest, - .min_filter = .nearest, - }); - const uniforms = device.createBuffer(&.{ - .label = label ++ " uniforms", - .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(.{ - .label = label, - .entries = &.{ - gpu.BindGroupLayout.Entry.initBuffer(0, .{ .vertex = true }, .uniform, false, 0), - gpu.BindGroupLayout.Entry.initBuffer(1, .{ .vertex = true }, .read_only_storage, false, 0), - gpu.BindGroupLayout.Entry.initBuffer(2, .{ .vertex = true }, .read_only_storage, false, 0), - gpu.BindGroupLayout.Entry.initBuffer(3, .{ .vertex = true }, .read_only_storage, false, 0), - gpu.BindGroupLayout.Entry.initSampler(4, .{ .fragment = true }, .filtering), - gpu.BindGroupLayout.Entry.initTexture(5, .{ .fragment = true }, .float, .dimension_2d, false), - gpu.BindGroupLayout.Entry.initTexture(6, .{ .fragment = true }, .float, .dimension_2d, false), - gpu.BindGroupLayout.Entry.initTexture(7, .{ .fragment = true }, .float, .dimension_2d, false), - gpu.BindGroupLayout.Entry.initTexture(8, .{ .fragment = true }, .float, .dimension_2d, false), - }, - }), - ); - defer bind_group_layout.release(); - - const texture_view = texture.createView(&gpu.TextureView.Descriptor{ .label = label }); - const texture2_view = if (opt_texture2) |tex| tex.createView(&gpu.TextureView.Descriptor{ .label = label }) else texture_view; - const texture3_view = if (opt_texture3) |tex| tex.createView(&gpu.TextureView.Descriptor{ .label = label }) else texture_view; - const texture4_view = if (opt_texture4) |tex| tex.createView(&gpu.TextureView.Descriptor{ .label = label }) else texture_view; - defer texture_view.release(); - // TODO: texture views 2-4 leak - - const bind_group = opt_bind_group orelse device.createBindGroup( - &gpu.BindGroup.Descriptor.init(.{ - .label = label, - .layout = bind_group_layout, - .entries = &.{ - gpu.BindGroup.Entry.initBuffer(0, uniforms, 0, @sizeOf(Uniforms), @sizeOf(Uniforms)), - gpu.BindGroup.Entry.initBuffer(1, transforms, 0, @sizeOf(math.Mat4x4) * sprite_buffer_cap, @sizeOf(math.Mat4x4)), - gpu.BindGroup.Entry.initBuffer(2, uv_transforms, 0, @sizeOf(math.Mat3x3) * sprite_buffer_cap, @sizeOf(math.Mat3x3)), - gpu.BindGroup.Entry.initBuffer(3, sizes, 0, @sizeOf(math.Vec2) * sprite_buffer_cap, @sizeOf(math.Vec2)), - gpu.BindGroup.Entry.initSampler(4, texture_sampler), - gpu.BindGroup.Entry.initTextureView(5, texture_view), - gpu.BindGroup.Entry.initTextureView(6, texture2_view), - gpu.BindGroup.Entry.initTextureView(7, texture3_view), - gpu.BindGroup.Entry.initTextureView(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.get(core.state().main_window, .framebuffer_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(.{ - .label = label, - .bind_group_layouts = &bind_group_layouts, - })); - defer pipeline_layout.release(); - const render_pipeline = device.createRenderPipeline(&gpu.RenderPipeline.Descriptor{ - .label = label, - .fragment = &fragment, - .layout = pipeline_layout, - .vertex = gpu.VertexState{ - .module = shader_module, - .entry_point = "vertMain", - }, - }); - - const 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, - }; - try sprite_pipeline.set(pipeline_id, .built, built); - try sprite_pipeline.set(pipeline_id, .num_sprites, 0); -} - -fn preRender(entities: *mach.Entities.Mod, core: *mach.Core.Mod, sprite_pipeline: *Mod) !void { - const label = @tagName(name) ++ ".preRender"; - const encoder = core.state().device.createCommandEncoder(&.{ .label = label }); - defer encoder.release(); - - var q = try entities.query(.{ - .ids = mach.Entities.Mod.read(.id), - .built_pipelines = Mod.read(.built), - }); - while (q.next()) |v| { - for (v.ids, v.built_pipelines) |id, built| { - const view_projection = sprite_pipeline.get(id, .view_projection) orelse blk: { - const width_px: f32 = @floatFromInt(core.state().size().width); - const height_px: f32 = @floatFromInt(core.state().size().height); - break :blk math.Mat4x4.projection2D(.{ - .left = -width_px / 2, - .right = width_px / 2, - .bottom = -height_px / 2, - .top = height_px / 2, - .near = -0.1, - .far = 100000, - }); - }; - - // Update uniform buffer - const uniforms = Uniforms{ - .view_projection = view_projection, - // TODO(sprite): dimensions of multi-textures, number of multi-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(&.{ .label = label }); - defer command.release(); - core.state().queue.submit(&[_]*gpu.CommandBuffer{command}); -} - -fn render(entities: *mach.Entities.Mod, 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 q = try entities.query(.{ - .ids = mach.Entities.Mod.read(.id), - .built_pipelines = Mod.read(.built), - }); - while (q.next()) |v| { - for (v.ids, v.built_pipelines) |pipeline_id, built| { - // Draw the sprite batch - const total_vertices = sprite_pipeline.get(pipeline_id, .num_sprites).? * 6; - render_pass.setPipeline(built.render); - // TODO(sprite): 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 97a8d56f..3abcea44 100644 --- a/src/gfx/main.zig +++ b/src/gfx/main.zig @@ -1,21 +1,16 @@ pub const util = @import("util.zig"); // TODO: banish 2-level deep namespaces pub const Atlas = @import("atlas/Atlas.zig"); -// ECS modules +// Mach modules 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"); - -/// All Sprite rendering modules -pub const sprite_modules = .{ Sprite, SpritePipeline }; - -/// All Text rendering modules -pub const text_modules = .{ Text, TextPipeline, TextStyle }; +// TODO(text): rewrite this using object system +// pub const Text = @import("Text.zig"); +// pub const TextPipeline = @import("TextPipeline.zig"); +// pub const TextStyle = @import("TextStyle.zig"); /// All graphics modules -pub const modules = .{ sprite_modules, text_modules }; +// TODO(text): add Text module here +pub const modules = .{Sprite}; // Fonts pub const Font = @import("font/main.zig").Font; @@ -30,7 +25,7 @@ test { // TODO: refactor code so we can use this here: // std.testing.refAllDeclsRecursive(@This()); std.testing.refAllDeclsRecursive(util); - // std.testing.refAllDeclsRecursive(Sprite); + std.testing.refAllDeclsRecursive(Sprite); std.testing.refAllDeclsRecursive(Atlas); // std.testing.refAllDeclsRecursive(Text); std.testing.refAllDeclsRecursive(Font); diff --git a/src/main.zig b/src/main.zig index 67c98f65..13de49f5 100644 --- a/src/main.zig +++ b/src/main.zig @@ -11,8 +11,7 @@ pub const Core = if (build_options.want_core) @import("Core.zig") else struct {} // note: gamemode requires libc on linux pub const gamemode = if (builtin.os.tag != .linux or builtin.link_libc) @import("gamemode.zig"); -// TODO(object) -// pub const gfx = if (build_options.want_mach) @import("gfx/main.zig") else struct {}; +pub const gfx = if (build_options.want_mach) @import("gfx/main.zig") else struct {}; pub const Audio = if (build_options.want_sysaudio) @import("Audio.zig") else struct {}; pub const math = @import("math/main.zig"); pub const testing = @import("testing.zig");