gfx: rewrite Sprite module to use object system
Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
This commit is contained in:
parent
c6602a7286
commit
47a8a0d98c
4 changed files with 435 additions and 505 deletions
|
|
@ -13,102 +13,394 @@ const Vec3 = math.Vec3;
|
||||||
const Mat3x3 = math.Mat3x3;
|
const Mat3x3 = math.Mat3x3;
|
||||||
const Mat4x4 = math.Mat4x4;
|
const Mat4x4 = math.Mat4x4;
|
||||||
|
|
||||||
|
const Sprite = @This();
|
||||||
|
|
||||||
pub const mach_module = .mach_gfx_sprite;
|
pub const mach_module = .mach_gfx_sprite;
|
||||||
|
|
||||||
// TODO(object)
|
pub const mach_systems = .{.tick};
|
||||||
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.
|
|
||||||
},
|
|
||||||
|
|
||||||
.uv_transform = .{ .type = Mat3x3, .description =
|
const Uniforms = extern struct {
|
||||||
\\ UV coordinate transformation matrix describing top-left corner / origin of sprite, in pixels.
|
/// The view * orthographic projection matrix
|
||||||
},
|
view_projection: math.Mat4x4 align(16),
|
||||||
|
|
||||||
.size = .{ .type = Vec2, .description =
|
/// Total size of the sprite texture in pixels
|
||||||
\\ The size of the sprite, in pixels.
|
texture_size: math.Vec2 align(16),
|
||||||
},
|
|
||||||
|
|
||||||
.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 systems = .{
|
pub const BuiltPipeline = struct {
|
||||||
.update = .{ .handler = update },
|
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(
|
const sprite_buffer_cap = 1024 * 512; // TODO(sprite): allow user to specify preallocation
|
||||||
entities: *mach.Entities.Mod,
|
|
||||||
core: *mach.Core.Mod,
|
pub var cp_transforms: [sprite_buffer_cap]math.Mat4x4 = undefined;
|
||||||
sprite_pipeline: *gfx.SpritePipeline.Mod,
|
// TODO(d3d12): uv_transform should be a Mat3x3 but our D3D12/HLSL backend cannot handle it.
|
||||||
) !void {
|
pub var cp_uv_transforms: [sprite_buffer_cap]math.Mat4x4 = undefined;
|
||||||
var q = try entities.query(.{
|
pub var cp_sizes: [sprite_buffer_cap]math.Vec2 = undefined;
|
||||||
.ids = mach.Entities.Mod.read(.id),
|
|
||||||
.built_pipelines = gfx.SpritePipeline.Mod.read(.built),
|
sprites: mach.Objects(.{ .track_fields = true }, struct {
|
||||||
});
|
/// The sprite model transformation matrix. A sprite is measured in pixel units, starting from
|
||||||
while (q.next()) |v| {
|
/// (0, 0) at the top-left corner and extending to the size of the sprite. By default, the world
|
||||||
for (v.ids, v.built_pipelines) |pipeline_id, built| {
|
/// origin (0, 0) lives at the center of the window.
|
||||||
try updatePipeline(entities, core, sprite_pipeline, pipeline_id, &built);
|
///
|
||||||
|
/// 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(
|
fn rebuildPipeline(
|
||||||
entities: *mach.Entities.Mod,
|
core: *mach.Core,
|
||||||
core: *mach.Core.Mod,
|
sprite: *Sprite,
|
||||||
sprite_pipeline: *gfx.SpritePipeline.Mod,
|
pipeline_id: mach.ObjectID,
|
||||||
pipeline_id: mach.EntityID,
|
) void {
|
||||||
built: *const gfx.SpritePipeline.BuiltPipeline,
|
// Destroy the current pipeline, if built.
|
||||||
) !void {
|
var pipeline = sprite.pipelines.getValue(pipeline_id);
|
||||||
const device = core.state().device;
|
defer sprite.pipelines.setValueRaw(pipeline_id, pipeline);
|
||||||
const label = @tagName(name) ++ ".updatePipeline";
|
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 });
|
const encoder = device.createCommandEncoder(&.{ .label = label });
|
||||||
defer encoder.release();
|
defer encoder.release();
|
||||||
|
|
||||||
var num_sprites: u32 = 0;
|
var i: u32 = 0;
|
||||||
var i: usize = 0;
|
for (pipeline_children) |sprite_id| {
|
||||||
var q = try entities.query(.{
|
if (!sprite.sprites.is(sprite_id)) continue;
|
||||||
.transforms = Mod.read(.transform),
|
const s = sprite.sprites.getValue(sprite_id);
|
||||||
.uv_transforms = Mod.read(.uv_transform),
|
|
||||||
.sizes = Mod.read(.size),
|
cp_transforms[i] = s.transform;
|
||||||
.pipelines = Mod.read(.pipeline),
|
|
||||||
});
|
// TODO(d3d12): uv_transform should be a Mat3x3 but our D3D12/HLSL backend cannot handle it.
|
||||||
while (q.next()) |v| {
|
const uv = s.uv_transform;
|
||||||
for (v.transforms, v.uv_transforms, v.sizes, v.pipelines) |transform, uv_transform, size, sprite_pipeline_id| {
|
cp_uv_transforms[i].v[0] = vec4(uv.v[0].x(), uv.v[0].y(), uv.v[0].z(), 0.0);
|
||||||
// TODO: currently we cannot query all sprites which have a _single_ pipeline component
|
cp_uv_transforms[i].v[1] = vec4(uv.v[1].x(), uv.v[1].y(), uv.v[1].z(), 0.0);
|
||||||
// value and get back contiguous memory for all of them. This is because all sprites with
|
cp_uv_transforms[i].v[2] = vec4(uv.v[2].x(), uv.v[2].y(), uv.v[2].z(), 0.0);
|
||||||
// possibly different pipeline component values are stored as the same archetype. If we
|
cp_uv_transforms[i].v[3] = vec4(0.0, 0.0, 0.0, 0.0);
|
||||||
// introduce a new concept of tagging-by-value to our entity storage then we can enforce
|
cp_sizes[i] = s.size;
|
||||||
// that all entities with the same pipeline value are stored in contiguous memory, and
|
i += 1;
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort sprites back-to-front for draw order, alpha blending
|
// Sort sprites back-to-front for draw order, alpha blending
|
||||||
const Context = struct {
|
const Context = struct {
|
||||||
// TODO(d3d12): #1217
|
// TODO(d3d12): uv_transform should be a Mat3x3 but our D3D12/HLSL backend cannot handle it.
|
||||||
// changed the uv_transform to 4x4. The 3x3 causes issues with d3d12/hlsl
|
|
||||||
transforms: []Mat4x4,
|
transforms: []Mat4x4,
|
||||||
uv_transforms: []Mat4x4,
|
uv_transforms: []Mat4x4,
|
||||||
sizes: []Vec2,
|
sizes: []Vec2,
|
||||||
|
|
@ -122,27 +414,72 @@ fn updatePipeline(
|
||||||
|
|
||||||
pub fn swap(ctx: @This(), a: usize, b: usize) void {
|
pub fn swap(ctx: @This(), a: usize, b: usize) void {
|
||||||
std.mem.swap(Mat4x4, &ctx.transforms[a], &ctx.transforms[b]);
|
std.mem.swap(Mat4x4, &ctx.transforms[a], &ctx.transforms[b]);
|
||||||
// TODO(d3d12): #1217
|
// TODO(d3d12): uv_transform should be a Mat3x3 but our D3D12/HLSL backend cannot handle it.
|
||||||
// changed the uv_transform to 4x4. The 3x3 causes issues with d3d12/hlsl
|
|
||||||
std.mem.swap(Mat4x4, &ctx.uv_transforms[a], &ctx.uv_transforms[b]);
|
std.mem.swap(Mat4x4, &ctx.uv_transforms[a], &ctx.uv_transforms[b]);
|
||||||
std.mem.swap(Vec2, &ctx.sizes[a], &ctx.sizes[b]);
|
std.mem.swap(Vec2, &ctx.sizes[a], &ctx.sizes[b]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
std.sort.pdqContext(0, i, Context{
|
std.sort.pdqContext(0, i, Context{
|
||||||
.transforms = gfx.SpritePipeline.cp_transforms[0..i],
|
.transforms = cp_transforms[0..i],
|
||||||
.uv_transforms = gfx.SpritePipeline.cp_uv_transforms[0..i],
|
.uv_transforms = cp_uv_transforms[0..i],
|
||||||
.sizes = gfx.SpritePipeline.cp_sizes[0..i],
|
.sizes = cp_sizes[0..i],
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: optimize by removing this component set call and instead use a .write() query
|
sprite.pipelines.set(pipeline_id, .num_sprites, i);
|
||||||
try sprite_pipeline.set(pipeline_id, .num_sprites, num_sprites);
|
if (i > 0) {
|
||||||
if (num_sprites > 0) {
|
encoder.writeBuffer(pipeline.built.?.transforms, 0, cp_transforms[0..i]);
|
||||||
encoder.writeBuffer(built.transforms, 0, gfx.SpritePipeline.cp_transforms[0..i]);
|
encoder.writeBuffer(pipeline.built.?.uv_transforms, 0, cp_uv_transforms[0..i]);
|
||||||
encoder.writeBuffer(built.uv_transforms, 0, gfx.SpritePipeline.cp_uv_transforms[0..i]);
|
encoder.writeBuffer(pipeline.built.?.sizes, 0, cp_sizes[0..i]);
|
||||||
encoder.writeBuffer(built.sizes, 0, gfx.SpritePipeline.cp_sizes[0..i]);
|
|
||||||
|
|
||||||
var command = encoder.finish(&.{ .label = label });
|
var command = encoder.finish(&.{ .label = label });
|
||||||
defer command.release();
|
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);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +1,16 @@
|
||||||
pub const util = @import("util.zig"); // TODO: banish 2-level deep namespaces
|
pub const util = @import("util.zig"); // TODO: banish 2-level deep namespaces
|
||||||
pub const Atlas = @import("atlas/Atlas.zig");
|
pub const Atlas = @import("atlas/Atlas.zig");
|
||||||
|
|
||||||
// ECS modules
|
// Mach modules
|
||||||
pub const Sprite = @import("Sprite.zig");
|
pub const Sprite = @import("Sprite.zig");
|
||||||
pub const SpritePipeline = @import("SpritePipeline.zig");
|
// TODO(text): rewrite this using object system
|
||||||
pub const Text = @import("Text.zig");
|
// pub const Text = @import("Text.zig");
|
||||||
pub const TextPipeline = @import("TextPipeline.zig");
|
// pub const TextPipeline = @import("TextPipeline.zig");
|
||||||
pub const TextStyle = @import("TextStyle.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 };
|
|
||||||
|
|
||||||
/// All graphics modules
|
/// All graphics modules
|
||||||
pub const modules = .{ sprite_modules, text_modules };
|
// TODO(text): add Text module here
|
||||||
|
pub const modules = .{Sprite};
|
||||||
|
|
||||||
// Fonts
|
// Fonts
|
||||||
pub const Font = @import("font/main.zig").Font;
|
pub const Font = @import("font/main.zig").Font;
|
||||||
|
|
@ -30,7 +25,7 @@ test {
|
||||||
// TODO: refactor code so we can use this here:
|
// TODO: refactor code so we can use this here:
|
||||||
// std.testing.refAllDeclsRecursive(@This());
|
// std.testing.refAllDeclsRecursive(@This());
|
||||||
std.testing.refAllDeclsRecursive(util);
|
std.testing.refAllDeclsRecursive(util);
|
||||||
// std.testing.refAllDeclsRecursive(Sprite);
|
std.testing.refAllDeclsRecursive(Sprite);
|
||||||
std.testing.refAllDeclsRecursive(Atlas);
|
std.testing.refAllDeclsRecursive(Atlas);
|
||||||
// std.testing.refAllDeclsRecursive(Text);
|
// std.testing.refAllDeclsRecursive(Text);
|
||||||
std.testing.refAllDeclsRecursive(Font);
|
std.testing.refAllDeclsRecursive(Font);
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,7 @@ pub const Core = if (build_options.want_core) @import("Core.zig") else struct {}
|
||||||
|
|
||||||
// note: gamemode requires libc on linux
|
// note: gamemode requires libc on linux
|
||||||
pub const gamemode = if (builtin.os.tag != .linux or builtin.link_libc) @import("gamemode.zig");
|
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 Audio = if (build_options.want_sysaudio) @import("Audio.zig") else struct {};
|
||||||
pub const math = @import("math/main.zig");
|
pub const math = @import("math/main.zig");
|
||||||
pub const testing = @import("testing.zig");
|
pub const testing = @import("testing.zig");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue