mach/src/gfx/SpritePipeline.zig
Stephen Gutekanst 541ce9e7c0 all: rename mach.Entity.Mod -> mach.Entities.Mod
Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
2024-05-07 23:40:56 -07:00

387 lines
16 KiB
Zig

const std = @import("std");
const mach = @import("../main.zig");
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 events = .{
.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;
pub var cp_uv_transforms: [sprite_buffer_cap]math.Mat3x3 = undefined;
pub var cp_sizes: [sprite_buffer_cap]math.Vec2 = undefined;
/// Which render pass should be used during .render
render_pass: ?*gpu.RenderPassEncoder = null,
pub const BuiltPipeline = struct {
render: *gpu.RenderPipeline,
texture_sampler: *gpu.Sampler,
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: *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(sprite_pipeline: *Mod) void {
var archetypes_iter = sprite_pipeline.__entities.queryDeprecated(.{ .all = &.{
.{ .mach_gfx_sprite_pipeline = &.{
.built,
} },
} });
while (archetypes_iter.next()) |archetype| {
for (archetype.slice(.mach_gfx_sprite_pipeline, .built)) |*p| p.deinit();
}
}
fn update(core: *mach.Core.Mod, sprite_pipeline: *Mod) !void {
// Destroy all sprite render pipelines. We will rebuild them all.
deinit(sprite_pipeline);
var archetypes_iter = sprite_pipeline.__entities.queryDeprecated(.{ .all = &.{
.{ .mach_gfx_sprite_pipeline = &.{
.texture,
} },
} });
while (archetypes_iter.next()) |archetype| {
const ids = archetype.slice(.entities, .id);
const textures = archetype.slice(.mach_gfx_sprite_pipeline, .texture);
for (ids, 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 {
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 },
.size = @sizeOf(math.Mat3x3) * 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.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{ .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();
const bind_group = opt_bind_group orelse device.createBindGroup(
&gpu.BindGroup.Descriptor.init(.{
.label = label,
.layout = bind_group_layout,
.entries = &.{
if (mach.use_sysgpu)
gpu.BindGroup.Entry.buffer(0, uniforms, 0, @sizeOf(Uniforms), @sizeOf(Uniforms))
else
gpu.BindGroup.Entry.buffer(0, uniforms, 0, @sizeOf(Uniforms)),
if (mach.use_sysgpu)
gpu.BindGroup.Entry.buffer(1, transforms, 0, @sizeOf(math.Mat4x4) * sprite_buffer_cap, @sizeOf(math.Mat4x4))
else
gpu.BindGroup.Entry.buffer(1, transforms, 0, @sizeOf(math.Mat4x4) * sprite_buffer_cap),
if (mach.use_sysgpu)
gpu.BindGroup.Entry.buffer(2, uv_transforms, 0, @sizeOf(math.Mat3x3) * sprite_buffer_cap, @sizeOf(math.Mat3x3))
else
gpu.BindGroup.Entry.buffer(2, uv_transforms, 0, @sizeOf(math.Mat3x3) * sprite_buffer_cap),
if (mach.use_sysgpu)
gpu.BindGroup.Entry.buffer(3, sizes, 0, @sizeOf(math.Vec2) * sprite_buffer_cap, @sizeOf(math.Vec2))
else
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.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(sprite_pipeline: *Mod, core: *mach.Core.Mod) void {
const label = @tagName(name) ++ ".preRender";
const encoder = core.state().device.createCommandEncoder(&.{ .label = label });
defer encoder.release();
var archetypes_iter = sprite_pipeline.__entities.queryDeprecated(.{ .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(.{
// TODO(Core)
.left = -@as(f32, @floatFromInt(mach.core.size().width)) / 2,
.right = @as(f32, @floatFromInt(mach.core.size().width)) / 2,
.bottom = -@as(f32, @floatFromInt(mach.core.size().height)) / 2,
.top = @as(f32, @floatFromInt(mach.core.size().height)) / 2,
.near = -0.1,
.far = 100000,
});
// Update uniform buffer
const uniforms = Uniforms{
.view_projection = proj,
// TODO(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())),
),
};
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(sprite_pipeline: *Mod) !void {
const render_pass = if (sprite_pipeline.state().render_pass) |rp| rp else std.debug.panic("sprite_pipeline.state().render_pass must be specified", .{});
sprite_pipeline.state().render_pass = null;
// TODO(sprite): need a way to specify order of rendering with multiple pipelines
var archetypes_iter = sprite_pipeline.__entities.queryDeprecated(.{ .all = &.{
.{ .mach_gfx_sprite_pipeline = &.{
.built,
} },
} });
while (archetypes_iter.next()) |archetype| {
const ids = archetype.slice(.entities, .id);
const built_pipelines = archetype.slice(.mach_gfx_sprite_pipeline, .built);
for (ids, 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);
}
}
}