gfx2d: redesign Sprite2D

Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
This commit is contained in:
Stephen Gutekanst 2023-09-10 19:09:28 -07:00 committed by Stephen Gutekanst
parent b83b71e2b2
commit 45cbfcf7b6
2 changed files with 174 additions and 118 deletions

View file

@ -12,18 +12,8 @@ const Vec3 = math.Vec3;
const Mat3x3 = math.Mat3x3; const Mat3x3 = math.Mat3x3;
const Mat4x4 = math.Mat4x4; const Mat4x4 = math.Mat4x4;
/// Public state
texture: *gpu.Texture,
/// Internal state /// Internal state
pipeline: *gpu.RenderPipeline, pipelines: std.AutoArrayHashMapUnmanaged(u32, Pipeline),
queue: *gpu.Queue,
bind_group: *gpu.BindGroup,
uniform_buffer: *gpu.Buffer,
sprite_transforms: *gpu.Buffer,
sprite_uv_transforms: *gpu.Buffer,
sprite_sizes: *gpu.Buffer,
texture_size: Vec2,
pub const name = .engine_sprite2d; pub const name = .engine_sprite2d;
@ -59,43 +49,112 @@ const Uniforms = extern struct {
texture_size: Vec2 align(16), 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 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,
};
pub fn engineSprite2dInit( pub fn engineSprite2dInit(
sprite2d: *mach.Mod(.engine_sprite2d),
) !void {
sprite2d.state = .{
// TODO: struct default value initializers don't work
.pipelines = .{},
};
}
pub fn engineSprite2dInitPipeline(
engine: *mach.Mod(.engine), engine: *mach.Mod(.engine),
sprite2d: *mach.Mod(.engine_sprite2d), sprite2d: *mach.Mod(.engine_sprite2d),
opt: PipelineOptions,
) !void { ) !void {
const device = engine.state.device; const device = engine.state.device;
const uniform_buffer = device.createBuffer(&.{ const pipeline = try sprite2d.state.pipelines.getOrPut(engine.allocator, opt.pipeline);
.usage = .{ .copy_dst = true, .uniform = true }, if (pipeline.found_existing) {
.size = @sizeOf(Uniforms), pipeline.value_ptr.*.deinit();
.mapped_at_creation = .false, }
});
// Create a sampler with linear filtering for smooth interpolation.
const queue = device.getQueue();
const texture_sampler = device.createSampler(&.{
.mag_filter = .nearest,
.min_filter = .nearest,
});
// Storage buffers
const sprite_buffer_cap = 1024 * 256; // TODO: allow user to specify preallocation const sprite_buffer_cap = 1024 * 256; // TODO: allow user to specify preallocation
const sprite_transforms = device.createBuffer(&.{ const transforms = device.createBuffer(&.{
.usage = .{ .storage = true, .copy_dst = true }, .usage = .{ .storage = true, .copy_dst = true },
.size = @sizeOf(Mat4x4) * sprite_buffer_cap, .size = @sizeOf(Mat4x4) * sprite_buffer_cap,
.mapped_at_creation = .false, .mapped_at_creation = .false,
}); });
const sprite_uv_transforms = device.createBuffer(&.{ const uv_transforms = device.createBuffer(&.{
.usage = .{ .storage = true, .copy_dst = true }, .usage = .{ .storage = true, .copy_dst = true },
.size = @sizeOf(Mat3x3) * sprite_buffer_cap, .size = @sizeOf(Mat3x3) * sprite_buffer_cap,
.mapped_at_creation = .false, .mapped_at_creation = .false,
}); });
const sprite_sizes = device.createBuffer(&.{ const sizes = device.createBuffer(&.{
.usage = .{ .storage = true, .copy_dst = true }, .usage = .{ .storage = true, .copy_dst = true },
.size = @sizeOf(Vec2) * sprite_buffer_cap, .size = @sizeOf(Vec2) * sprite_buffer_cap,
.mapped_at_creation = .false, .mapped_at_creation = .false,
}); });
const bind_group_layout = device.createBindGroupLayout( 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(.{ &gpu.BindGroupLayout.Descriptor.init(.{
.entries = &.{ .entries = &.{
gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true }, .uniform, false, 0), gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true }, .uniform, false, 0),
@ -104,25 +163,41 @@ pub fn engineSprite2dInit(
gpu.BindGroupLayout.Entry.buffer(3, .{ .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.sampler(4, .{ .fragment = true }, .filtering),
gpu.BindGroupLayout.Entry.texture(5, .{ .fragment = true }, .float, .dimension_2d, false), 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),
}, },
}), }),
); );
var bind_group = device.createBindGroup( 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(.{ &gpu.BindGroup.Descriptor.init(.{
.layout = bind_group_layout, .layout = bind_group_layout,
.entries = &.{ .entries = &.{
gpu.BindGroup.Entry.buffer(0, uniform_buffer, 0, @sizeOf(Uniforms)), gpu.BindGroup.Entry.buffer(0, uniforms, 0, @sizeOf(Uniforms)),
gpu.BindGroup.Entry.buffer(1, sprite_transforms, 0, @sizeOf(Mat4x4) * sprite_buffer_cap), gpu.BindGroup.Entry.buffer(1, transforms, 0, @sizeOf(Mat4x4) * sprite_buffer_cap),
gpu.BindGroup.Entry.buffer(2, sprite_uv_transforms, 0, @sizeOf(Mat3x3) * sprite_buffer_cap), gpu.BindGroup.Entry.buffer(2, uv_transforms, 0, @sizeOf(Mat3x3) * sprite_buffer_cap),
gpu.BindGroup.Entry.buffer(3, sprite_sizes, 0, @sizeOf(Vec2) * 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.sampler(4, texture_sampler),
gpu.BindGroup.Entry.textureView(5, sprite2d.state.texture.createView(&gpu.TextureView.Descriptor{})), 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 shader_module = device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl")); const blend_state = opt.blend_state orelse gpu.BlendState{
const blend = gpu.BlendState{
.color = .{ .color = .{
.operation = .add, .operation = .add,
.src_factor = .src_alpha, .src_factor = .src_alpha,
@ -134,64 +209,62 @@ pub fn engineSprite2dInit(
.dst_factor = .zero, .dst_factor = .zero,
}, },
}; };
const color_target = gpu.ColorTargetState{
const shader_module = opt.shader orelse device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl"));
defer shader_module.release();
const color_target = opt.color_target_state orelse gpu.ColorTargetState{
.format = core.descriptor.format, .format = core.descriptor.format,
.blend = &blend, .blend = &blend_state,
.write_mask = gpu.ColorWriteMaskFlags.all, .write_mask = gpu.ColorWriteMaskFlags.all,
}; };
const fragment = gpu.FragmentState.init(.{ const fragment = opt.fragment_state orelse gpu.FragmentState.init(.{
.module = shader_module, .module = shader_module,
.entry_point = "frag_main", .entry_point = "fragMain",
.targets = &.{color_target}, .targets = &.{color_target},
}); });
const bind_group_layouts = [_]*gpu.BindGroupLayout{bind_group_layout}; const bind_group_layouts = [_]*gpu.BindGroupLayout{bind_group_layout};
const pipeline_layout = device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{ const pipeline_layout = opt.pipeline_layout orelse device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{
.bind_group_layouts = &bind_group_layouts, .bind_group_layouts = &bind_group_layouts,
})); }));
const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ defer pipeline_layout.release();
const render = device.createRenderPipeline(&gpu.RenderPipeline.Descriptor{
.fragment = &fragment, .fragment = &fragment,
.layout = pipeline_layout, .layout = pipeline_layout,
.vertex = gpu.VertexState{ .vertex = gpu.VertexState{
.module = shader_module, .module = shader_module,
.entry_point = "vertex_main", .entry_point = "vertMain",
}, },
}; });
sprite2d.state = .{ pipeline.value_ptr.* = Pipeline{
.pipeline = device.createRenderPipeline(&pipeline_descriptor), .render = render,
.queue = queue, .texture_sampler = texture_sampler,
.texture = opt.texture,
.texture2 = opt.texture2,
.texture3 = opt.texture3,
.texture4 = opt.texture4,
.bind_group = bind_group, .bind_group = bind_group,
.uniform_buffer = uniform_buffer, .uniforms = uniforms,
.sprite_transforms = sprite_transforms, .num_sprites = 0,
.sprite_uv_transforms = sprite_uv_transforms, .transforms = transforms,
.sprite_sizes = sprite_sizes, .uv_transforms = uv_transforms,
.texture_size = vec2( .sizes = sizes,
@as(f32, @floatFromInt(sprite2d.state.texture.getWidth())),
@as(f32, @floatFromInt(sprite2d.state.texture.getHeight())),
),
.texture = sprite2d.state.texture,
}; };
shader_module.release();
} }
pub fn deinit(sprite2d: *mach.Mod(.engine_sprite2d)) !void { pub fn deinit(sprite2d: *mach.Mod(.engine_sprite2d)) !void {
sprite2d.state.texture.release(); for (sprite2d.state.pipelines.entries.items(.value)) |*pipeline| pipeline.deinit();
sprite2d.state.pipeline.release(); sprite2d.state.pipelines.deinit(sprite2d.allocator);
sprite2d.state.queue.release();
sprite2d.state.bind_group.release();
sprite2d.state.uniform_buffer.release();
sprite2d.state.sprite_transforms.release();
sprite2d.state.sprite_uv_transforms.release();
sprite2d.state.sprite_sizes.release();
} }
pub fn engineSprite2dUpdated( pub fn engineSprite2dUpdated(
engine: *mach.Mod(.engine), engine: *mach.Mod(.engine),
sprite2d: *mach.Mod(.engine_sprite2d), sprite2d: *mach.Mod(.engine_sprite2d),
pipeline: u32, pipeline_id: u32,
) !void { ) !void {
_ = pipeline; const pipeline = sprite2d.state.pipelines.getPtr(pipeline_id).?;
const device = engine.state.device; const device = engine.state.device;
// TODO: make sure these entities only belong to the given pipeline // TODO: make sure these entities only belong to the given pipeline
@ -206,6 +279,9 @@ pub fn engineSprite2dUpdated(
} }); } });
const encoder = device.createCommandEncoder(null); const encoder = device.createCommandEncoder(null);
defer encoder.release();
pipeline.num_sprites = 0;
var transforms_offset: usize = 0; var transforms_offset: usize = 0;
var uv_transforms_offset: usize = 0; var uv_transforms_offset: usize = 0;
var sizes_offset: usize = 0; var sizes_offset: usize = 0;
@ -214,40 +290,30 @@ pub fn engineSprite2dUpdated(
var uv_transforms = archetype.slice(.engine_sprite2d, .uv_transform); var uv_transforms = archetype.slice(.engine_sprite2d, .uv_transform);
var sizes = archetype.slice(.engine_sprite2d, .size); var sizes = archetype.slice(.engine_sprite2d, .size);
encoder.writeBuffer(sprite2d.state.sprite_transforms, transforms_offset, transforms); // TODO: confirm the lifetime of these slices is OK for writeBuffer, how long do they need
encoder.writeBuffer(sprite2d.state.sprite_uv_transforms, uv_transforms_offset, uv_transforms); // to live?
encoder.writeBuffer(sprite2d.state.sprite_sizes, sizes_offset, sizes); 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; transforms_offset += transforms.len;
uv_transforms_offset += uv_transforms.len; uv_transforms_offset += uv_transforms.len;
sizes_offset += sizes.len; sizes_offset += sizes.len;
pipeline.num_sprites += @intCast(transforms.len);
} }
var command = encoder.finish(null); var command = encoder.finish(null);
encoder.release(); defer command.release();
sprite2d.state.queue.submit(&[_]*gpu.CommandBuffer{command});
command.release(); engine.state.queue.submit(&[_]*gpu.CommandBuffer{command});
} }
pub fn tick( pub fn engineSprite2dPreRender(
engine: *mach.Mod(.engine), engine: *mach.Mod(.engine),
sprite2d: *mach.Mod(.engine_sprite2d), sprite2d: *mach.Mod(.engine_sprite2d),
pipeline_id: u32,
) !void { ) !void {
const device = engine.state.device; const pipeline = sprite2d.state.pipelines.get(pipeline_id).?;
// Begin our render pass
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
const color_attachment = gpu.RenderPassColorAttachment{
.view = back_buffer_view,
.clear_value = gpu.Color{ .r = 1.0, .g = 1.0, .b = 1.0, .a = 1.0 },
.load_op = .clear,
.store_op = .store,
};
const encoder = device.createCommandEncoder(null);
const render_pass_info = gpu.RenderPassDescriptor.init(.{
.color_attachments = &.{color_attachment},
});
// Update uniform buffer // Update uniform buffer
const ortho = Mat4x4.ortho( const ortho = Mat4x4.ortho(
@ -260,38 +326,28 @@ pub fn tick(
); );
const uniforms = Uniforms{ const uniforms = Uniforms{
.view_projection = ortho, .view_projection = ortho,
.texture_size = sprite2d.state.texture_size, // TODO: dimensions of other textures, number of textures present
.texture_size = vec2(
@as(f32, @floatFromInt(pipeline.texture.getWidth())),
@as(f32, @floatFromInt(pipeline.texture.getHeight())),
),
}; };
encoder.writeBuffer(sprite2d.state.uniform_buffer, 0, &[_]Uniforms{uniforms});
// Calculate number of vertices engine.state.encoder.writeBuffer(pipeline.uniforms, 0, &[_]Uniforms{uniforms});
// TODO: eliminate this }
var total_vertices: u32 = 0;
var archetypes_iter = engine.entities.query(.{ .all = &.{ pub fn engineSprite2dRender(
.{ .engine_sprite2d = &.{ engine: *mach.Mod(.engine),
.pipeline, sprite2d: *mach.Mod(.engine_sprite2d),
} }, pipeline_id: u32,
} }); ) !void {
while (archetypes_iter.next()) |archetype| { const pipeline = sprite2d.state.pipelines.get(pipeline_id).?;
total_vertices += 6;
var pipelines = archetype.slice(.engine_sprite2d, .pipeline);
for (pipelines) |_| total_vertices += 6;
}
// Draw the sprite batch // Draw the sprite batch
const pass = encoder.beginRenderPass(&render_pass_info); const pass = engine.state.pass;
pass.setPipeline(sprite2d.state.pipeline); const total_vertices = pipeline.num_sprites * 6;
pass.setPipeline(pipeline.render);
// TODO: remove dynamic offsets? // TODO: remove dynamic offsets?
pass.setBindGroup(0, sprite2d.state.bind_group, &.{}); pass.setBindGroup(0, pipeline.bind_group, &.{});
pass.draw(total_vertices, 1, 0, 0); pass.draw(total_vertices, 1, 0, 0);
pass.end();
pass.release();
var command = encoder.finish(null);
encoder.release();
sprite2d.state.queue.submit(&[_]*gpu.CommandBuffer{command});
command.release();
core.swap_chain.present();
back_buffer_view.release();
} }

View file

@ -31,7 +31,7 @@ struct Uniforms {
@group(0) @binding(3) var<storage, read> sprite_sizes: array<vec2<f32>>; @group(0) @binding(3) var<storage, read> sprite_sizes: array<vec2<f32>>;
@vertex @vertex
fn vertex_main( fn vertMain(
@builtin(vertex_index) VertexIndex : u32 @builtin(vertex_index) VertexIndex : u32
) -> VertexOutput { ) -> VertexOutput {
// Our vertex shader will be called six times per sprite (2 triangles make up a sprite, so six // Our vertex shader will be called six times per sprite (2 triangles make up a sprite, so six
@ -91,7 +91,7 @@ fn vertex_main(
@group(0) @binding(5) var spriteTexture: texture_2d<f32>; @group(0) @binding(5) var spriteTexture: texture_2d<f32>;
@fragment @fragment
fn frag_main( fn fragMain(
@location(0) fragUV: vec2<f32> @location(0) fragUV: vec2<f32>
) -> @location(0) vec4<f32> { ) -> @location(0) vec4<f32> {
var c = textureSample(spriteTexture, spriteSampler, fragUV); var c = textureSample(spriteTexture, spriteSampler, fragUV);