diff --git a/src/gfx/Text.zig b/src/gfx/Text.zig index c8d59d59..72231d94 100644 --- a/src/gfx/Text.zig +++ b/src/gfx/Text.zig @@ -5,309 +5,593 @@ 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 = "", + + /// Font size in pixels + /// e.g. 12 * mach.gfx.px_per_pt for 12pt font size + font_size: f32 = 12 * gfx.px_per_pt, + + // 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: + /// + /// ``` + /// 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))); + /// ``` + 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 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 -} +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; -/// Helper to set text components on an entity simply/easily via heap allocating strings -/// -/// ``` -/// try mach.Text.allocPrintText(text, my_text_entity, my_style_entity, "Hello, {s}!", .{"Mach"}); -/// ``` -pub fn allocPrintText( - text: *Mod, - id: mach.EntityID, - style: mach.EntityID, - comptime fmt: []const u8, - args: anytype, -) !void { - freeText(text, id); - const str = try std.fmt.allocPrint(text.state().allocator, fmt, args); + // 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); - 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; + // 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); - try text.set(id, .style, styles); - try text.set(id, .text, strings); - try text.set(id, .dirty, true); -} + // Find text objects parented to this pipeline. + var pipeline_children = try text.pipelines.getChildren(pipeline_id); + defer pipeline_children.deinit(); -/// 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 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); } - if (text.get(id, .style)) |slice| text.state().allocator.free(slice); } -fn update( - entities: *mach.Entities.Mod, - text: *Mod, - text_style: *gfx.TextStyle.Mod, - core: *mach.Core.Mod, - text_pipeline: *gfx.TextPipeline.Mod, +fn rebuildPipeline( + core: *mach.Core, + text: *Text, + pipeline_id: mach.ObjectID, ) !void { - var q = try entities.query(.{ - .ids = mach.Entities.Mod.read(.id), - .built_pipelines = gfx.TextPipeline.Mod.write(.built), + // 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); + + // 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(); + + const window = core.windows.getValue(pipeline.window.?); + const device = window.device; + + const label = @tagName(mach_module) ++ ".rebuildPipeline"; + + // 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; - continue; - } + // 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), + // Where we will store the built glyphs for this text entity. + 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 (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(text): actually use font name + const font_bytes = @import("font-assets").fira_sans_regular_ttf; + var font = if (text.font_once) |f| f else blk: { + text.font_once = try gfx.Font.initBytes(font_bytes); + break :blk text.font_once.?; }; - built_text.glyphs.clearRetainingCapacity(); + // TODO(text) + // defer font.deinit(allocator); - 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; + const style = text.styles.getValue(segment.style); - for (segments, styles) |segment, style| { - // 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 - 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.?; - }; - // defer font.deinit(allocator); + // Create a text shaper + var run = try gfx.TextRun.init(); + run.font_size_px = style.font_size; + run.px_density = px_density; + defer run.deinit(); - 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).?; + run.addText(segment.text); + try font.shape(&run); - // Create a text shaper - var run = try gfx.TextRun.init(); - run.font_size_px = font_size; - run.px_density = px_density; - defer run.deinit(); - - run.addText(segment); - 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? - if (codepoint == '\n') { - origin_x = 0; - origin_y -= font_size; - continue; - } - - const region = try built.regions.getOrPut(allocator, .{ - .index = glyph.glyph_index, - .size = @bitCast(font_size), - }); - if (!region.found_existing) { - const rendered_glyph = try font.render(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]); - texture_update = true; - - // Exclude the 1px blank space margin when describing the region of the texture - // that actually represents the glyph. - const margin = 1; - glyph_atlas_region.x += margin; - glyph_atlas_region.y += margin; - glyph_atlas_region.width -= margin * 2; - glyph_atlas_region.height -= margin * 2; - region.value_ptr.* = glyph_atlas_region; - } else { - // whitespace - region.value_ptr.* = gfx.Atlas.Region{ - .width = 0, - .height = 0, - .x = 0, - .y = 0, - }; - } - } - - const r = region.value_ptr.*; - const size = vec2(@floatFromInt(r.width), @floatFromInt(r.height)); - try built_text.glyphs.append(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_padding = 0, - .uv_pos = vec2(@floatFromInt(r.x), @floatFromInt(r.y)), - .color = font_color, - }); - origin_x += glyph.advance.x(); + while (run.next()) |glyph| { + 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 -= style.font_size; + continue; } + + const region = try pipeline.built.?.regions.getOrPut(text.allocator, .{ + .index = glyph.glyph_index, + .size = @bitCast(style.font_size), + }); + if (!region.found_existing) { + 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 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 + // that actually represents the glyph. + const margin = 1; + glyph_atlas_region.x += margin; + glyph_atlas_region.y += margin; + glyph_atlas_region.width -= margin * 2; + glyph_atlas_region.height -= margin * 2; + region.value_ptr.* = glyph_atlas_region; + } else { + // whitespace + region.value_ptr.* = gfx.Atlas.Region{ + .width = 0, + .height = 0, + .x = 0, + .y = 0, + }; + } + } + + const r = region.value_ptr.*; + const size = vec2(@floatFromInt(r.width), @floatFromInt(r.height)); + 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 = 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 = 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); - - // Add to the entire set of glyphs for this pipeline - try glyphs.appendSlice(allocator, built_text.glyphs.items); - num_texts += 1; } + // Update the text entity's built form + t.built = built_text; + text.objects.setValueRaw(text_id, t); - // 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); - } + // Add to the entire set of glyphs for this pipeline + 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); +} diff --git a/src/gfx/TextPipeline.zig b/src/gfx/TextPipeline.zig deleted file mode 100644 index e9f88381..00000000 --- a/src/gfx/TextPipeline.zig +++ /dev/null @@ -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); - } - } -} diff --git a/src/gfx/TextStyle.zig b/src/gfx/TextStyle.zig deleted file mode 100644 index 2a1dafaa..00000000 --- a/src/gfx/TextStyle.zig +++ /dev/null @@ -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.) -}; diff --git a/src/gfx/main.zig b/src/gfx/main.zig index 3abcea44..24c742fb 100644 --- a/src/gfx/main.zig +++ b/src/gfx/main.zig @@ -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);