gfx: rewrite Text module to use new object system
Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
This commit is contained in:
parent
fda85f8268
commit
68251d95b7
4 changed files with 583 additions and 727 deletions
700
src/gfx/Text.zig
700
src/gfx/Text.zig
|
|
@ -5,244 +5,540 @@ const gfx = mach.gfx;
|
|||
|
||||
const math = mach.math;
|
||||
const vec2 = math.vec2;
|
||||
const Vec2 = math.Vec2;
|
||||
const Vec3 = math.Vec3;
|
||||
const Vec4 = math.Vec4;
|
||||
const vec4 = math.vec4;
|
||||
const Mat3x3 = math.Mat3x3;
|
||||
const Vec4 = math.Vec4;
|
||||
const Mat4x4 = math.Mat4x4;
|
||||
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
const Text = @This();
|
||||
|
||||
pub const mach_module = .mach_gfx_text;
|
||||
|
||||
// TODO(object)
|
||||
pub const components = .{
|
||||
.transform = .{ .type = Mat4x4, .description =
|
||||
\\ The text model transformation matrix. Text is measured in pixel units, starting from
|
||||
\\ (0, 0) at the top-left corner and extending to the size of the text. By default, the world
|
||||
\\ origin (0, 0) lives at the center of the window.
|
||||
},
|
||||
pub const mach_systems = .{ .tick, .init };
|
||||
|
||||
.text = .{ .type = []const []const u8, .description =
|
||||
\\ String segments of UTF-8 encoded text to render.
|
||||
\\
|
||||
\\ Expected to match the length of the style component.
|
||||
},
|
||||
// TODO(text): currently not handling deinit properly
|
||||
|
||||
.style = .{ .type = []const mach.EntityID, .description =
|
||||
\\ The style to apply to each segment of text.
|
||||
\\
|
||||
\\ Expected to match the length of the text component.
|
||||
},
|
||||
const buffer_cap = 1024 * 512; // TODO(text): allow user to specify preallocation
|
||||
|
||||
.dirty = .{ .type = bool, .description =
|
||||
\\ If true, the underlying glyph buffers, texture atlas, and transform buffers will be updated
|
||||
\\ as needed to reflect the latest component values.
|
||||
\\
|
||||
\\ This lets rendering be static if no changes have occurred.
|
||||
},
|
||||
var cp_transforms: [buffer_cap]math.Mat4x4 = undefined;
|
||||
var cp_colors: [buffer_cap]math.Vec4 = undefined;
|
||||
var cp_glyphs: [buffer_cap]Glyph = undefined;
|
||||
|
||||
.pipeline = .{ .type = mach.EntityID, .description =
|
||||
\\ Which render pipeline to use for rendering the text.
|
||||
\\
|
||||
\\ This determines which shader, textures, etc. are used for rendering the text.
|
||||
},
|
||||
const Uniforms = extern struct {
|
||||
/// The view * orthographic projection matrix
|
||||
view_projection: math.Mat4x4 align(16),
|
||||
|
||||
.built = .{ .type = BuiltText, .description = "internal" },
|
||||
/// Total size of the font atlas texture in pixels
|
||||
texture_size: math.Vec2 align(16),
|
||||
};
|
||||
|
||||
pub const systems = .{
|
||||
.init = .{ .handler = init },
|
||||
.deinit = .{ .handler = deinit },
|
||||
.update = .{ .handler = update },
|
||||
const BuiltPipeline = struct {
|
||||
render: *gpu.RenderPipeline,
|
||||
texture_sampler: *gpu.Sampler,
|
||||
texture: *gpu.Texture,
|
||||
bind_group: *gpu.BindGroup,
|
||||
uniforms: *gpu.Buffer,
|
||||
texture_atlas: gfx.Atlas,
|
||||
regions: RegionMap = .{},
|
||||
|
||||
// Storage buffers
|
||||
transforms: *gpu.Buffer,
|
||||
colors: *gpu.Buffer,
|
||||
glyphs: *gpu.Buffer,
|
||||
|
||||
fn deinit(p: *BuiltPipeline, allocator: std.mem.Allocator) void {
|
||||
p.render.release();
|
||||
p.texture_sampler.release();
|
||||
p.texture.release();
|
||||
p.bind_group.release();
|
||||
p.uniforms.release();
|
||||
p.texture_atlas.deinit(allocator);
|
||||
p.regions.deinit(allocator);
|
||||
p.transforms.release();
|
||||
p.colors.release();
|
||||
p.glyphs.release();
|
||||
}
|
||||
};
|
||||
|
||||
const BuiltText = struct {
|
||||
glyphs: std.ArrayListUnmanaged(gfx.TextPipeline.Glyph),
|
||||
glyphs: std.ArrayListUnmanaged(Glyph),
|
||||
};
|
||||
|
||||
const Glyph = extern struct {
|
||||
/// Position of this glyph (top-left corner.)
|
||||
pos: math.Vec2,
|
||||
|
||||
/// Width of the glyph in pixels.
|
||||
size: math.Vec2,
|
||||
|
||||
/// Normalized position of the top-left UV coordinate
|
||||
uv_pos: math.Vec2,
|
||||
|
||||
/// Which text this glyph belongs to; this is the index for transforms[i], colors[i].
|
||||
text_index: u32,
|
||||
|
||||
// TODO(d3d12): this is a hack, having 7 floats before the color vec causes an error
|
||||
text_padding: u32,
|
||||
|
||||
/// Color of the glyph
|
||||
color: math.Vec4,
|
||||
};
|
||||
|
||||
const GlyphKey = struct {
|
||||
index: u32,
|
||||
// Auto Hashing doesn't work for floats, so we bitcast to integer.
|
||||
size: u32,
|
||||
};
|
||||
|
||||
const RegionMap = std.AutoArrayHashMapUnmanaged(GlyphKey, gfx.Atlas.Region);
|
||||
|
||||
pub const Segment = struct {
|
||||
/// UTF-8 encoded string of text to render
|
||||
text: []const u8,
|
||||
|
||||
/// Style to apply when rendering the text
|
||||
style: mach.ObjectID,
|
||||
};
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
glyph_update_buffer: ?std.ArrayListUnmanaged(Glyph) = null,
|
||||
font_once: ?gfx.Font = null,
|
||||
|
||||
pub fn init(text: *Mod) void {
|
||||
text.init(.{ .allocator = gpa.allocator() });
|
||||
}
|
||||
styles: mach.Objects(.{ .track_fields = true }, struct {
|
||||
// TODO(text): not currently implemented
|
||||
// TODO(text): ship a default font
|
||||
/// Desired font to render text with
|
||||
font_name: []const u8 = "",
|
||||
|
||||
pub fn deinit(text: *Mod) void {
|
||||
_ = text;
|
||||
// TODO: help with cleaning up allocPrintText, which is currently a little difficult to track
|
||||
// since it is a per-entity allocation
|
||||
}
|
||||
/// Font size in pixels
|
||||
/// e.g. 12 * mach.gfx.px_per_pt for 12pt font size
|
||||
font_size: f32 = 12 * gfx.px_per_pt,
|
||||
|
||||
/// Helper to set text components on an entity simply/easily via heap allocating strings
|
||||
// TODO(text): not currently implemented
|
||||
/// Font weight
|
||||
font_weight: u16 = gfx.font_weight_normal,
|
||||
|
||||
// TODO(text): not currently implemented
|
||||
/// Fill color of text
|
||||
color: math.Vec4 = vec4(0, 0, 0, 1.0), // black
|
||||
|
||||
// TODO(text): not currently implemented
|
||||
/// Italic style
|
||||
italic: bool = false,
|
||||
|
||||
// TODO(text): allow user to specify projection matrix (3d-space flat text etc.)
|
||||
}),
|
||||
|
||||
objects: mach.Objects(.{ .track_fields = true }, struct {
|
||||
/// The text model transformation matrix. Text is measured in pixel units, starting from
|
||||
/// (0, 0) at the top-left corner and extending to the size of the text. By default, the world
|
||||
/// origin (0, 0) lives at the center of the window.
|
||||
transform: Mat4x4,
|
||||
|
||||
/// The segments of text
|
||||
segments: []const Segment,
|
||||
|
||||
/// Internal text object state.
|
||||
built: ?BuiltText = null,
|
||||
}),
|
||||
|
||||
/// A text pipeline renders all text objects 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,
|
||||
|
||||
/// View*Projection matrix to use when rendering with this pipeline. This controls both
|
||||
/// the size of the 'virtual canvas' which is rendered onto, as well as the 'camera position'.
|
||||
///
|
||||
/// 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:
|
||||
///
|
||||
/// ```
|
||||
/// try mach.Text.allocPrintText(text, my_text_entity, my_style_entity, "Hello, {s}!", .{"Mach"});
|
||||
/// 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)));
|
||||
/// ```
|
||||
pub fn allocPrintText(
|
||||
text: *Mod,
|
||||
id: mach.EntityID,
|
||||
style: mach.EntityID,
|
||||
comptime fmt: []const u8,
|
||||
args: anytype,
|
||||
view_projection: ?Mat4x4 = null,
|
||||
|
||||
/// Shader program to use when rendering
|
||||
///
|
||||
/// If null, defaults to text.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 text objects this pipeline will render.
|
||||
/// Read-only, updated as part of Text.tick
|
||||
num_texts: u32 = 0,
|
||||
|
||||
/// Number of text segments this pipeline will render.
|
||||
/// Read-only, updated as part of Text.tick
|
||||
num_segments: u32 = 0,
|
||||
|
||||
/// Total number of glyphs this pipeline will render.
|
||||
/// Read-only, updated as part of Text.tick
|
||||
num_glyphs: u32 = 0,
|
||||
|
||||
/// Internal pipeline state.
|
||||
built: ?BuiltPipeline = null,
|
||||
}),
|
||||
|
||||
pub fn init(text: *Text) !void {
|
||||
// TODO(allocator): find a better way to get an allocator here
|
||||
const allocator = std.heap.c_allocator;
|
||||
|
||||
text.* = .{
|
||||
.allocator = allocator,
|
||||
.styles = text.styles,
|
||||
.objects = text.objects,
|
||||
.pipelines = text.pipelines,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn tick(text: *Text, core: *mach.Core) !void {
|
||||
var pipelines = text.pipelines.slice();
|
||||
while (pipelines.next()) |pipeline_id| {
|
||||
// Is this pipeline usable for rendering? If not, no need to process it.
|
||||
const pipeline = text.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:
|
||||
_ = text.pipelines.updated(pipeline_id, .window);
|
||||
_ = text.pipelines.updated(pipeline_id, .render_pass);
|
||||
_ = text.pipelines.updated(pipeline_id, .view_projection);
|
||||
|
||||
// If any other fields of the pipeline have been updated, a pipeline rebuild is required.
|
||||
if (text.pipelines.anyUpdated(pipeline_id)) try rebuildPipeline(core, text, pipeline_id);
|
||||
|
||||
// Find text objects parented to this pipeline.
|
||||
var pipeline_children = try text.pipelines.getChildren(pipeline_id);
|
||||
defer pipeline_children.deinit();
|
||||
|
||||
// If any text objects were updated, we update the pipeline's storage buffers to have the new
|
||||
// information.
|
||||
const any_updated = blk: {
|
||||
for (pipeline_children.items) |text_id| {
|
||||
if (!text.objects.is(text_id)) continue;
|
||||
if (text.objects.anyUpdated(text_id)) break :blk true;
|
||||
}
|
||||
break :blk false;
|
||||
};
|
||||
if (any_updated) try updatePipelineBuffers(text, core, pipeline_id, pipeline_children.items);
|
||||
|
||||
// // Do we actually have any sprites to render?
|
||||
// pipeline = text.pipelines.getValue(pipeline_id);
|
||||
// if (pipeline.num_sprites == 0) continue;
|
||||
|
||||
// TODO(text): need a way to specify order of rendering with multiple pipelines
|
||||
renderPipeline(text, core, pipeline_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn rebuildPipeline(
|
||||
core: *mach.Core,
|
||||
text: *Text,
|
||||
pipeline_id: mach.ObjectID,
|
||||
) !void {
|
||||
freeText(text, id);
|
||||
const str = try std.fmt.allocPrint(text.state().allocator, fmt, args);
|
||||
// Destroy the current pipeline, if built.
|
||||
var pipeline = text.pipelines.getValue(pipeline_id);
|
||||
defer text.pipelines.setValueRaw(pipeline_id, pipeline);
|
||||
if (pipeline.built) |*built| built.deinit(text.allocator);
|
||||
|
||||
const styles = try text.state().allocator.alloc(mach.EntityID, 1);
|
||||
styles[0] = style;
|
||||
const strings = try text.state().allocator.alloc([]const u8, 1);
|
||||
strings[0] = str;
|
||||
// Reference any user-provided objects.
|
||||
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();
|
||||
|
||||
try text.set(id, .style, styles);
|
||||
try text.set(id, .text, strings);
|
||||
try text.set(id, .dirty, true);
|
||||
}
|
||||
const window = core.windows.getValue(pipeline.window.?);
|
||||
const device = window.device;
|
||||
|
||||
/// Free's an entity's .text and .style slices that were previously allocated via e.g. allocPrintText
|
||||
pub fn freeText(text: *Mod, id: mach.EntityID) void {
|
||||
if (text.get(id, .text)) |slice| {
|
||||
text.state().allocator.free(slice[0]);
|
||||
text.state().allocator.free(slice);
|
||||
}
|
||||
if (text.get(id, .style)) |slice| text.state().allocator.free(slice);
|
||||
}
|
||||
const label = @tagName(mach_module) ++ ".rebuildPipeline";
|
||||
|
||||
fn update(
|
||||
entities: *mach.Entities.Mod,
|
||||
text: *Mod,
|
||||
text_style: *gfx.TextStyle.Mod,
|
||||
core: *mach.Core.Mod,
|
||||
text_pipeline: *gfx.TextPipeline.Mod,
|
||||
) !void {
|
||||
var q = try entities.query(.{
|
||||
.ids = mach.Entities.Mod.read(.id),
|
||||
.built_pipelines = gfx.TextPipeline.Mod.write(.built),
|
||||
// Prepare texture for the font atlas.
|
||||
// TODO(text): dynamic texture re-allocation when not large enough
|
||||
// TODO(text): better default allocation size
|
||||
const img_size = gpu.Extent3D{ .width = 1024, .height = 1024 };
|
||||
const texture = device.createTexture(&.{
|
||||
.label = label,
|
||||
.size = img_size,
|
||||
.format = .rgba8_unorm,
|
||||
.usage = .{
|
||||
.texture_binding = true,
|
||||
.copy_dst = true,
|
||||
},
|
||||
});
|
||||
while (q.next()) |v| {
|
||||
for (v.ids, v.built_pipelines) |pipeline_id, *built| {
|
||||
try updatePipeline(entities, text, text_style, core, text_pipeline, pipeline_id, built);
|
||||
}
|
||||
}
|
||||
const texture_atlas = try gfx.Atlas.init(
|
||||
text.allocator,
|
||||
img_size.width,
|
||||
.rgba,
|
||||
);
|
||||
|
||||
// Storage buffers
|
||||
const transforms = device.createBuffer(&.{
|
||||
.label = label ++ " transforms",
|
||||
.usage = .{ .storage = true, .copy_dst = true },
|
||||
.size = @sizeOf(math.Mat4x4) * buffer_cap,
|
||||
.mapped_at_creation = .false,
|
||||
});
|
||||
const colors = device.createBuffer(&.{
|
||||
.label = label ++ " colors",
|
||||
.usage = .{ .storage = true, .copy_dst = true },
|
||||
.size = @sizeOf(math.Vec4) * buffer_cap,
|
||||
.mapped_at_creation = .false,
|
||||
});
|
||||
const glyphs = device.createBuffer(&.{
|
||||
.label = label ++ " glyphs",
|
||||
.usage = .{ .storage = true, .copy_dst = true },
|
||||
.size = @sizeOf(Glyph) * 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),
|
||||
},
|
||||
}),
|
||||
);
|
||||
defer bind_group_layout.release();
|
||||
|
||||
const texture_view = texture.createView(&gpu.TextureView.Descriptor{ .label = label });
|
||||
defer texture_view.release();
|
||||
|
||||
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) * buffer_cap, @sizeOf(math.Mat4x4)),
|
||||
gpu.BindGroup.Entry.initBuffer(2, colors, 0, @sizeOf(math.Vec4) * buffer_cap, @sizeOf(math.Vec4)),
|
||||
gpu.BindGroup.Entry.initBuffer(3, glyphs, 0, @sizeOf(Glyph) * buffer_cap, @sizeOf(Glyph)),
|
||||
gpu.BindGroup.Entry.initSampler(4, texture_sampler),
|
||||
gpu.BindGroup.Entry.initTextureView(5, texture_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("text.wgsl", @embedFile("text.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 = texture,
|
||||
.bind_group = bind_group,
|
||||
.uniforms = uniforms,
|
||||
.transforms = transforms,
|
||||
.colors = colors,
|
||||
.glyphs = glyphs,
|
||||
.texture_atlas = texture_atlas,
|
||||
};
|
||||
pipeline.num_texts = 0;
|
||||
pipeline.num_segments = 0;
|
||||
pipeline.num_glyphs = 0;
|
||||
}
|
||||
|
||||
var font_once: ?gfx.Font = null;
|
||||
|
||||
fn updatePipeline(
|
||||
entities: *mach.Entities.Mod,
|
||||
text: *Mod,
|
||||
text_style: *gfx.TextStyle.Mod,
|
||||
core: *mach.Core.Mod,
|
||||
text_pipeline: *gfx.TextPipeline.Mod,
|
||||
pipeline_id: mach.EntityID,
|
||||
built: *gfx.TextPipeline.BuiltPipeline,
|
||||
fn updatePipelineBuffers(
|
||||
text: *Text,
|
||||
core: *mach.Core,
|
||||
pipeline_id: mach.ObjectID,
|
||||
pipeline_children: []const mach.ObjectID,
|
||||
) !void {
|
||||
const device = core.state().device;
|
||||
const label = @tagName(name) ++ ".updatePipeline";
|
||||
var pipeline = text.pipelines.getValue(pipeline_id);
|
||||
defer text.pipelines.setValueRaw(pipeline_id, pipeline);
|
||||
const window = core.windows.getValue(pipeline.window.?);
|
||||
const device = window.device;
|
||||
const queue = window.queue;
|
||||
|
||||
const label = @tagName(mach_module) ++ ".updatePipelineBuffers";
|
||||
const encoder = device.createCommandEncoder(&.{ .label = label });
|
||||
defer encoder.release();
|
||||
|
||||
const allocator = text_pipeline.state().allocator;
|
||||
var glyphs = if (text_pipeline.state().glyph_update_buffer) |*b| b else blk: {
|
||||
var glyphs = if (text.glyph_update_buffer) |*b| b else blk: {
|
||||
// TODO(text): better default allocation size
|
||||
const b = try std.ArrayListUnmanaged(gfx.TextPipeline.Glyph).initCapacity(allocator, 256);
|
||||
text_pipeline.state().glyph_update_buffer = b;
|
||||
break :blk &text_pipeline.state().glyph_update_buffer.?;
|
||||
const b = try std.ArrayListUnmanaged(Glyph).initCapacity(text.allocator, 256);
|
||||
text.glyph_update_buffer = b;
|
||||
break :blk &text.glyph_update_buffer.?;
|
||||
};
|
||||
glyphs.clearRetainingCapacity();
|
||||
|
||||
var texture_update = false;
|
||||
var num_texts: u32 = 0;
|
||||
var removes = try std.ArrayListUnmanaged(mach.EntityID).initCapacity(allocator, 8);
|
||||
var num_segments: u32 = 0;
|
||||
var i: u32 = 0;
|
||||
for (pipeline_children) |text_id| {
|
||||
if (!text.objects.is(text_id)) continue;
|
||||
var t = text.objects.getValue(text_id);
|
||||
num_segments += @intCast(t.segments.len);
|
||||
|
||||
var q = try entities.query(.{
|
||||
.ids = mach.Entities.Mod.read(.id),
|
||||
.transforms = Mod.read(.transform),
|
||||
.segment_slices = Mod.read(.text),
|
||||
.style_slices = Mod.read(.style),
|
||||
.pipelines = Mod.read(.pipeline),
|
||||
});
|
||||
while (q.next()) |v| {
|
||||
for (v.ids, v.transforms, v.segment_slices, v.style_slices, v.pipelines) |id, transform, segments, styles, text_pipeline_id| {
|
||||
// TODO: currently we cannot query all texts which have a _single_ pipeline component
|
||||
// value and get back contiguous memory for all of them. This is because all texts 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 (text_pipeline_id != pipeline_id) continue;
|
||||
cp_transforms[i] = t.transform;
|
||||
|
||||
gfx.TextPipeline.cp_transforms[num_texts] = transform;
|
||||
// Changing these fields shouldn't trigger a pipeline rebuild, so clear their update values:
|
||||
_ = text.objects.updated(text_id, .transform);
|
||||
|
||||
if (text.get(id, .dirty) == null) {
|
||||
// We do not need to rebuild this specific entity, so use cached glyph information
|
||||
// from its previous build.
|
||||
const built_text = text.get(id, .built).?;
|
||||
for (built_text.glyphs.items) |*glyph| glyph.text_index = num_texts;
|
||||
try glyphs.appendSlice(allocator, built_text.glyphs.items);
|
||||
num_texts += 1;
|
||||
// If the text has been built before, and nothing about it has changed, then we can just use
|
||||
// what we built already.
|
||||
if (t.built != null and !text.objects.anyUpdated(text_id)) {
|
||||
for (t.built.?.glyphs.items) |*glyph| glyph.text_index = i;
|
||||
try glyphs.appendSlice(text.allocator, t.built.?.glyphs.items);
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Where we will store the built glyphs for this text entity.
|
||||
var built_text = if (text.get(id, .built)) |bt| bt else BuiltText{
|
||||
// TODO: better default allocations
|
||||
.glyphs = try std.ArrayListUnmanaged(gfx.TextPipeline.Glyph).initCapacity(allocator, 64),
|
||||
var built_text = if (t.built) |bt| bt else BuiltText{
|
||||
// TODO(text): better default allocations
|
||||
.glyphs = try std.ArrayListUnmanaged(Glyph).initCapacity(text.allocator, 64),
|
||||
};
|
||||
built_text.glyphs.clearRetainingCapacity();
|
||||
|
||||
const px_density = 2.0; // TODO(text): do not hard-code pixel density
|
||||
var origin_x: f32 = 0.0;
|
||||
var origin_y: f32 = 0.0;
|
||||
|
||||
for (segments, styles) |segment, style| {
|
||||
for (t.segments) |segment| {
|
||||
// Load the font
|
||||
// TODO(text): allow specifying a custom font
|
||||
// TODO(text): keep fonts around for reuse later
|
||||
// const font_name = text_style.get(style, .font_name).?;
|
||||
// _ = font_name; // TODO: actually use font name
|
||||
// _ = font_name; // TODO(text): actually use font name
|
||||
const font_bytes = @import("font-assets").fira_sans_regular_ttf;
|
||||
var font = if (font_once) |f| f else blk: {
|
||||
font_once = try gfx.Font.initBytes(font_bytes);
|
||||
break :blk font_once.?;
|
||||
var font = if (text.font_once) |f| f else blk: {
|
||||
text.font_once = try gfx.Font.initBytes(font_bytes);
|
||||
break :blk text.font_once.?;
|
||||
};
|
||||
// TODO(text)
|
||||
// defer font.deinit(allocator);
|
||||
|
||||
const font_size = text_style.get(style, .font_size).?;
|
||||
const font_color = text_style.get(style, .font_color) orelse vec4(0, 0, 0, 1.0);
|
||||
// TODO(text): respect these style parameters
|
||||
// const font_weight = text_style.get(style, .font_weight).?;
|
||||
// const italic = text_style.get(style, .italic).?;
|
||||
const style = text.styles.getValue(segment.style);
|
||||
|
||||
// Create a text shaper
|
||||
var run = try gfx.TextRun.init();
|
||||
run.font_size_px = font_size;
|
||||
run.font_size_px = style.font_size;
|
||||
run.px_density = px_density;
|
||||
defer run.deinit();
|
||||
|
||||
run.addText(segment);
|
||||
run.addText(segment.text);
|
||||
try font.shape(&run);
|
||||
|
||||
while (run.next()) |glyph| {
|
||||
const codepoint = segment[glyph.cluster];
|
||||
// TODO: use flags(?) to detect newline, or at least something more reliable?
|
||||
const codepoint = segment.text[glyph.cluster];
|
||||
// TODO(text): use flags(?) to detect newline, or at least something more reliable?
|
||||
if (codepoint == '\n') {
|
||||
origin_x = 0;
|
||||
origin_y -= font_size;
|
||||
origin_y -= style.font_size;
|
||||
continue;
|
||||
}
|
||||
|
||||
const region = try built.regions.getOrPut(allocator, .{
|
||||
const region = try pipeline.built.?.regions.getOrPut(text.allocator, .{
|
||||
.index = glyph.glyph_index,
|
||||
.size = @bitCast(font_size),
|
||||
.size = @bitCast(style.font_size),
|
||||
});
|
||||
if (!region.found_existing) {
|
||||
const rendered_glyph = try font.render(allocator, glyph.glyph_index, .{
|
||||
const rendered_glyph = try font.render(text.allocator, glyph.glyph_index, .{
|
||||
.font_size_px = run.font_size_px,
|
||||
});
|
||||
if (rendered_glyph.bitmap) |bitmap| {
|
||||
var glyph_atlas_region = try built.texture_atlas.reserve(allocator, rendered_glyph.width, rendered_glyph.height);
|
||||
built.texture_atlas.set(glyph_atlas_region, @as([*]const u8, @ptrCast(bitmap.ptr))[0 .. bitmap.len * 4]);
|
||||
var glyph_atlas_region = try pipeline.built.?.texture_atlas.reserve(text.allocator, rendered_glyph.width, rendered_glyph.height);
|
||||
pipeline.built.?.texture_atlas.set(glyph_atlas_region, @as([*]const u8, @ptrCast(bitmap.ptr))[0 .. bitmap.len * 4]);
|
||||
texture_update = true;
|
||||
|
||||
// Exclude the 1px blank space margin when describing the region of the texture
|
||||
|
|
@ -266,48 +562,36 @@ fn updatePipeline(
|
|||
|
||||
const r = region.value_ptr.*;
|
||||
const size = vec2(@floatFromInt(r.width), @floatFromInt(r.height));
|
||||
try built_text.glyphs.append(allocator, .{
|
||||
try built_text.glyphs.append(text.allocator, .{
|
||||
.pos = vec2(
|
||||
origin_x + glyph.offset.x(),
|
||||
origin_y - (size.y() - glyph.offset.y()),
|
||||
).divScalar(px_density),
|
||||
.size = size.divScalar(px_density),
|
||||
.text_index = num_texts,
|
||||
// TODO(d3d12): #1217
|
||||
// Added padding for d3d12/hlsl. Having 7 floats before the color vec caused and error.
|
||||
.text_index = i,
|
||||
// TODO(d3d12): this is a hack, having 7 floats before the color vec causes an error
|
||||
.text_padding = 0,
|
||||
.uv_pos = vec2(@floatFromInt(r.x), @floatFromInt(r.y)),
|
||||
.color = font_color,
|
||||
.color = style.color,
|
||||
});
|
||||
origin_x += glyph.advance.x();
|
||||
}
|
||||
}
|
||||
// Update the text entity's built form
|
||||
try text.set(id, .built, built_text);
|
||||
// TODO(text): see below
|
||||
// try text.remove(id, .dirty);
|
||||
try removes.append(allocator, id);
|
||||
t.built = built_text;
|
||||
text.objects.setValueRaw(text_id, t);
|
||||
|
||||
// Add to the entire set of glyphs for this pipeline
|
||||
try glyphs.appendSlice(allocator, built_text.glyphs.items);
|
||||
num_texts += 1;
|
||||
}
|
||||
|
||||
// TODO(important): removing components within an iter() currently produces undefined behavior
|
||||
// (entity may exist in current iteration, plus a future iteration as the iterator moves
|
||||
// on to the next archetype where the entity is now located.)
|
||||
for (removes.items) |remove_id| {
|
||||
try text.remove(remove_id, .dirty);
|
||||
}
|
||||
try glyphs.appendSlice(text.allocator, built_text.glyphs.items);
|
||||
i += 1;
|
||||
}
|
||||
|
||||
// Every pipeline update, we copy updated glyph and text buffers to the GPU.
|
||||
try text_pipeline.set(pipeline_id, .num_texts, num_texts);
|
||||
try text_pipeline.set(pipeline_id, .num_glyphs, @intCast(glyphs.items.len));
|
||||
if (glyphs.items.len > 0) encoder.writeBuffer(built.glyphs, 0, glyphs.items);
|
||||
if (num_texts > 0) {
|
||||
encoder.writeBuffer(built.transforms, 0, gfx.TextPipeline.cp_transforms[0..num_texts]);
|
||||
}
|
||||
pipeline.num_texts = i;
|
||||
pipeline.num_segments = num_segments;
|
||||
pipeline.num_glyphs = @intCast(glyphs.items.len);
|
||||
if (glyphs.items.len > 0) encoder.writeBuffer(pipeline.built.?.glyphs, 0, glyphs.items);
|
||||
if (i > 0) encoder.writeBuffer(pipeline.built.?.transforms, 0, cp_transforms[0..i]);
|
||||
|
||||
if (texture_update) {
|
||||
// TODO(text): do not assume texture's data_layout and img_size here, instead get it from
|
||||
|
|
@ -319,17 +603,63 @@ fn updatePipeline(
|
|||
.bytes_per_row = @as(u32, @intCast(img_size.width * 4)),
|
||||
.rows_per_image = @as(u32, @intCast(img_size.height)),
|
||||
};
|
||||
core.state().queue.writeTexture(
|
||||
&.{ .texture = built.texture },
|
||||
queue.writeTexture(
|
||||
&.{ .texture = pipeline.built.?.texture },
|
||||
&data_layout,
|
||||
&img_size,
|
||||
built.texture_atlas.data,
|
||||
pipeline.built.?.texture_atlas.data,
|
||||
);
|
||||
}
|
||||
|
||||
if (num_texts > 0 or glyphs.items.len > 0) {
|
||||
if (i > 0 or glyphs.items.len > 0) {
|
||||
var command = encoder.finish(&.{ .label = label });
|
||||
defer command.release();
|
||||
core.state().queue.submit(&[_]*gpu.CommandBuffer{command});
|
||||
queue.submit(&[_]*gpu.CommandBuffer{command});
|
||||
}
|
||||
}
|
||||
|
||||
fn renderPipeline(
|
||||
text: *Text,
|
||||
core: *mach.Core,
|
||||
pipeline_id: mach.ObjectID,
|
||||
) void {
|
||||
const pipeline = text.pipelines.getValue(pipeline_id);
|
||||
const window = core.windows.getValue(pipeline.window.?);
|
||||
const device = window.device;
|
||||
|
||||
const label = @tagName(mach_module) ++ ".renderPipeline";
|
||||
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,
|
||||
.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 text batch
|
||||
const total_vertices = pipeline.num_glyphs * 6;
|
||||
pipeline.render_pass.?.setPipeline(pipeline.built.?.render);
|
||||
// TODO(text): 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,432 +0,0 @@
|
|||
const std = @import("std");
|
||||
const mach = @import("../main.zig");
|
||||
|
||||
const gfx = mach.gfx;
|
||||
const gpu = mach.gpu;
|
||||
const math = mach.math;
|
||||
|
||||
pub const mach_module = .mach_gfx_text_pipeline;
|
||||
|
||||
// TODO(object)
|
||||
pub const components = .{
|
||||
.is_pipeline = .{ .type = void, .description =
|
||||
\\ Tag to indicate an entity represents a text pipeline.
|
||||
},
|
||||
|
||||
.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 text_pipeline.set(my_text_pipeline, .view_projection, view_projection);
|
||||
\\ ```
|
||||
},
|
||||
|
||||
.shader = .{ .type = *gpu.ShaderModule, .description =
|
||||
\\ Shader program to use when rendering
|
||||
\\ Defaults to text.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_texts = .{ .type = u32, .description =
|
||||
\\ Number of texts this pipeline will render.
|
||||
\\ Read-only, updated as part of Text.update
|
||||
},
|
||||
.num_glyphs = .{ .type = u32, .description =
|
||||
\\ Number of glyphs this pipeline will render.
|
||||
\\ Read-only, updated as part of Text.update
|
||||
},
|
||||
|
||||
.built = .{ .type = BuiltPipeline, .description = "internal" },
|
||||
};
|
||||
|
||||
pub const systems = .{
|
||||
.init = .{ .handler = fn () void },
|
||||
.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 font atlas texture in pixels
|
||||
texture_size: math.Vec2 align(16),
|
||||
};
|
||||
|
||||
const texts_buffer_cap = 1024 * 512; // TODO(text): allow user to specify preallocation
|
||||
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
|
||||
// TODO(text): eliminate these, see Text.updatePipeline for details on why these exist
|
||||
// currently.
|
||||
pub var cp_transforms: [texts_buffer_cap]math.Mat4x4 = undefined;
|
||||
pub var cp_colors: [texts_buffer_cap]math.Vec4 = undefined;
|
||||
pub var cp_glyphs: [texts_buffer_cap]Glyph = undefined;
|
||||
|
||||
/// Which render pass should be used during .render
|
||||
render_pass: ?*gpu.RenderPassEncoder = null,
|
||||
|
||||
glyph_update_buffer: ?std.ArrayListUnmanaged(Glyph) = null,
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
pub const Glyph = extern struct {
|
||||
/// Position of this glyph (top-left corner.)
|
||||
pos: math.Vec2,
|
||||
|
||||
/// Width of the glyph in pixels.
|
||||
size: math.Vec2,
|
||||
|
||||
/// Normalized position of the top-left UV coordinate
|
||||
uv_pos: math.Vec2,
|
||||
|
||||
/// Which text this glyph belongs to; this is the index for transforms[i], colors[i].
|
||||
text_index: u32,
|
||||
// TODO(d3d12): #1217
|
||||
// Added padding for d3d12/hlsl. Having 7 floats before the color vec caused and error.
|
||||
text_padding: u32,
|
||||
|
||||
/// Color of the glyph
|
||||
color: math.Vec4,
|
||||
};
|
||||
|
||||
const GlyphKey = struct {
|
||||
index: u32,
|
||||
// Auto Hashing doesn't work for floats, so we bitcast to integer.
|
||||
size: u32,
|
||||
};
|
||||
const RegionMap = std.AutoArrayHashMapUnmanaged(GlyphKey, gfx.Atlas.Region);
|
||||
|
||||
pub const BuiltPipeline = struct {
|
||||
render: *gpu.RenderPipeline,
|
||||
texture_sampler: *gpu.Sampler,
|
||||
texture: *gpu.Texture,
|
||||
bind_group: *gpu.BindGroup,
|
||||
uniforms: *gpu.Buffer,
|
||||
texture_atlas: gfx.Atlas,
|
||||
regions: RegionMap = .{},
|
||||
|
||||
// Storage buffers
|
||||
transforms: *gpu.Buffer,
|
||||
colors: *gpu.Buffer,
|
||||
glyphs: *gpu.Buffer,
|
||||
|
||||
pub fn deinit(p: *BuiltPipeline, allocator: std.mem.Allocator) void {
|
||||
p.render.release();
|
||||
p.texture_sampler.release();
|
||||
p.texture.release();
|
||||
p.bind_group.release();
|
||||
p.uniforms.release();
|
||||
p.transforms.release();
|
||||
p.colors.release();
|
||||
p.glyphs.release();
|
||||
p.texture_atlas.deinit(allocator);
|
||||
p.regions.deinit(allocator);
|
||||
}
|
||||
};
|
||||
|
||||
fn deinit(entities: *mach.Entities.Mod, text_pipeline: *Mod) !void {
|
||||
var q = try entities.query(.{
|
||||
.built_pipelines = Mod.write(.built),
|
||||
});
|
||||
while (q.next()) |v| {
|
||||
for (v.built_pipelines) |*built| {
|
||||
built.deinit(text_pipeline.state().allocator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update(entities: *mach.Entities.Mod, core: *mach.Core.Mod, text_pipeline: *Mod) !void {
|
||||
text_pipeline.init(.{
|
||||
.allocator = gpa.allocator(),
|
||||
});
|
||||
|
||||
// Destroy all text render pipelines. We will rebuild them all.
|
||||
try deinit(entities, text_pipeline);
|
||||
|
||||
var q = try entities.query(.{
|
||||
.ids = mach.Entities.Mod.read(.id),
|
||||
.is_pipelines = Mod.read(.is_pipeline),
|
||||
});
|
||||
while (q.next()) |v| {
|
||||
for (v.ids) |pipeline_id| {
|
||||
try buildPipeline(core, text_pipeline, pipeline_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn buildPipeline(
|
||||
core: *mach.Core.Mod,
|
||||
text_pipeline: *Mod,
|
||||
pipeline_id: mach.EntityID,
|
||||
) !void {
|
||||
// TODO: optimize by removing the component get/set calls in this function where possible
|
||||
// and instead use .write() queries
|
||||
const opt_shader = text_pipeline.get(pipeline_id, .shader);
|
||||
const opt_texture_sampler = text_pipeline.get(pipeline_id, .texture_sampler);
|
||||
const opt_blend_state = text_pipeline.get(pipeline_id, .blend_state);
|
||||
const opt_bind_group_layout = text_pipeline.get(pipeline_id, .bind_group_layout);
|
||||
const opt_bind_group = text_pipeline.get(pipeline_id, .bind_group);
|
||||
const opt_color_target_state = text_pipeline.get(pipeline_id, .color_target_state);
|
||||
const opt_fragment_state = text_pipeline.get(pipeline_id, .fragment_state);
|
||||
const opt_layout = text_pipeline.get(pipeline_id, .layout);
|
||||
|
||||
const device = core.state().device;
|
||||
|
||||
// Prepare texture for the font atlas.
|
||||
// TODO(text): dynamic texture re-allocation when not large enough
|
||||
// TODO(text): better default allocation size
|
||||
const label = @tagName(name) ++ ".buildPipeline";
|
||||
const img_size = gpu.Extent3D{ .width = 1024, .height = 1024 };
|
||||
const texture = device.createTexture(&.{
|
||||
.label = label,
|
||||
.size = img_size,
|
||||
.format = .rgba8_unorm,
|
||||
.usage = .{
|
||||
.texture_binding = true,
|
||||
.copy_dst = true,
|
||||
},
|
||||
});
|
||||
const texture_atlas = try gfx.Atlas.init(
|
||||
text_pipeline.state().allocator,
|
||||
img_size.width,
|
||||
.rgba,
|
||||
);
|
||||
|
||||
// Storage buffers
|
||||
const transforms = device.createBuffer(&.{
|
||||
.label = label ++ " transforms",
|
||||
.usage = .{ .storage = true, .copy_dst = true },
|
||||
.size = @sizeOf(math.Mat4x4) * texts_buffer_cap,
|
||||
.mapped_at_creation = .false,
|
||||
});
|
||||
const colors = device.createBuffer(&.{
|
||||
.label = label ++ " colors",
|
||||
.usage = .{ .storage = true, .copy_dst = true },
|
||||
.size = @sizeOf(math.Vec4) * texts_buffer_cap,
|
||||
.mapped_at_creation = .false,
|
||||
});
|
||||
const glyphs = device.createBuffer(&.{
|
||||
.label = label ++ " glyphs",
|
||||
.usage = .{ .storage = true, .copy_dst = true },
|
||||
.size = @sizeOf(Glyph) * texts_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),
|
||||
},
|
||||
}),
|
||||
);
|
||||
defer bind_group_layout.release();
|
||||
|
||||
const texture_view = texture.createView(&gpu.TextureView.Descriptor{ .label = label });
|
||||
defer texture_view.release();
|
||||
|
||||
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) * texts_buffer_cap, @sizeOf(math.Mat4x4)),
|
||||
gpu.BindGroup.Entry.initBuffer(2, colors, 0, @sizeOf(math.Vec4) * texts_buffer_cap, @sizeOf(math.Vec4)),
|
||||
gpu.BindGroup.Entry.initBuffer(3, glyphs, 0, @sizeOf(Glyph) * texts_buffer_cap, @sizeOf(Glyph)),
|
||||
gpu.BindGroup.Entry.initSampler(4, texture_sampler),
|
||||
gpu.BindGroup.Entry.initTextureView(5, texture_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("text.wgsl", @embedFile("text.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,
|
||||
.bind_group = bind_group,
|
||||
.uniforms = uniforms,
|
||||
.transforms = transforms,
|
||||
.colors = colors,
|
||||
.glyphs = glyphs,
|
||||
.texture_atlas = texture_atlas,
|
||||
};
|
||||
try text_pipeline.set(pipeline_id, .built, built);
|
||||
try text_pipeline.set(pipeline_id, .num_texts, 0);
|
||||
try text_pipeline.set(pipeline_id, .num_glyphs, 0);
|
||||
}
|
||||
|
||||
fn preRender(entities: *mach.Entities.Mod, core: *mach.Core.Mod, text_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 = text_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(text): 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(entities: *mach.Entities.Mod, text_pipeline: *Mod) !void {
|
||||
const render_pass = if (text_pipeline.state().render_pass) |rp| rp else std.debug.panic("text_pipeline.state().render_pass must be specified", .{});
|
||||
text_pipeline.state().render_pass = null;
|
||||
|
||||
// TODO(text): 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 text batch
|
||||
const total_vertices = text_pipeline.get(pipeline_id, .num_glyphs).? * 6;
|
||||
render_pass.setPipeline(built.render);
|
||||
// TODO(text): remove dynamic offsets?
|
||||
render_pass.setBindGroup(0, built.bind_group, &.{});
|
||||
render_pass.draw(total_vertices, 1, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
const mach = @import("../main.zig");
|
||||
const math = mach.math;
|
||||
|
||||
pub const mach_module = .mach_gfx_text_style;
|
||||
|
||||
// TODO(object)
|
||||
pub const components = .{
|
||||
// // TODO: ship a default font
|
||||
// .font_name = .{ .type = []const u8, .description =
|
||||
// \\ Desired font to render text with.
|
||||
// \\ TODO(text): this is not currently implemented
|
||||
// },
|
||||
|
||||
.font_size = .{ .type = f32, .description =
|
||||
\\ Font size in pixels
|
||||
\\ e.g. 12 * mach.gfx.px_per_pt for 12pt font size
|
||||
},
|
||||
|
||||
// // e.g. mach.gfx.font_weight_normal
|
||||
// .font_weight = .{ .type = u16, .description =
|
||||
// \\ Font weight
|
||||
// \\ TODO(text): this is not currently implemented
|
||||
// },
|
||||
|
||||
// // e.g. false
|
||||
// .italic = .{ .type = bool, .description =
|
||||
// \\ Italic text
|
||||
// \\ TODO(text): this is not currently implemented
|
||||
// },
|
||||
|
||||
.font_color = .{ .type = math.Vec4, .description =
|
||||
\\ Fill color of text
|
||||
\\ e.g. vec4(0.0, 0.0, 0.0, 1.0) // black
|
||||
\\ e.g. vec4(1.0, 1.0, 1.0, 1.0) // white
|
||||
},
|
||||
|
||||
// TODO(text): allow user to specify projection matrix (3d-space flat text etc.)
|
||||
};
|
||||
|
|
@ -3,14 +3,10 @@ pub const Atlas = @import("atlas/Atlas.zig");
|
|||
|
||||
// Mach modules
|
||||
pub const Sprite = @import("Sprite.zig");
|
||||
// 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");
|
||||
pub const Text = @import("Text.zig");
|
||||
|
||||
/// All graphics modules
|
||||
// TODO(text): add Text module here
|
||||
pub const modules = .{Sprite};
|
||||
pub const modules = .{ Sprite, Text };
|
||||
|
||||
// Fonts
|
||||
pub const Font = @import("font/main.zig").Font;
|
||||
|
|
@ -27,7 +23,7 @@ test {
|
|||
std.testing.refAllDeclsRecursive(util);
|
||||
std.testing.refAllDeclsRecursive(Sprite);
|
||||
std.testing.refAllDeclsRecursive(Atlas);
|
||||
// std.testing.refAllDeclsRecursive(Text);
|
||||
std.testing.refAllDeclsRecursive(Text);
|
||||
std.testing.refAllDeclsRecursive(Font);
|
||||
std.testing.refAllDeclsRecursive(TextRun);
|
||||
std.testing.refAllDeclsRecursive(Glyph);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue