const std = @import("std"); const mach = @import("../main.zig"); const gfx = mach.gfx; const gpu = mach.gpu; const math = mach.math; pub const name = .mach_gfx_text_pipeline; pub const Mod = mach.Mod(@This()); pub const components = .{ .is_pipeline = .{ .type = void, .description = \\ Tag to indicate an entity represents a text pipeline. }, .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 events = .{ .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, }; 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(text_pipeline: *Mod) void { var archetypes_iter = text_pipeline.entities.query(.{ .all = &.{ .{ .mach_gfx_text_pipeline = &.{ .built, } }, } }); while (archetypes_iter.next()) |archetype| { for (archetype.slice(.mach_gfx_text_pipeline, .built)) |*p| p.deinit(text_pipeline.state().allocator); } } fn update(core: *mach.Core.Mod, text_pipeline: *Mod) !void { text_pipeline.init(.{ .allocator = gpa.allocator(), }); // Destroy all text render pipelines. We will rebuild them all. deinit(text_pipeline); var archetypes_iter = text_pipeline.entities.query(.{ .all = &.{ .{ .mach_gfx_text_pipeline = &.{ .is_pipeline, } }, } }); while (archetypes_iter.next()) |archetype| { const ids = archetype.slice(.entity, .id); for (ids) |pipeline_id| { try buildPipeline(core, text_pipeline, pipeline_id); } } } fn buildPipeline( core: *mach.Core.Mod, text_pipeline: *Mod, pipeline_id: mach.EntityID, ) !void { 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, .render_attachment = 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.buffer(0, .{ .vertex = true }, .uniform, false, 0), gpu.BindGroupLayout.Entry.buffer(1, .{ .vertex = true }, .read_only_storage, false, 0), gpu.BindGroupLayout.Entry.buffer(2, .{ .vertex = true }, .read_only_storage, false, 0), gpu.BindGroupLayout.Entry.buffer(3, .{ .vertex = true }, .read_only_storage, false, 0), gpu.BindGroupLayout.Entry.sampler(4, .{ .fragment = true }, .filtering), gpu.BindGroupLayout.Entry.texture(5, .{ .fragment = true }, .float, .dimension_2d, false), }, }), ); 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.buffer(0, uniforms, 0, @sizeOf(Uniforms)), gpu.BindGroup.Entry.buffer(1, transforms, 0, @sizeOf(math.Mat4x4) * texts_buffer_cap), gpu.BindGroup.Entry.buffer(2, colors, 0, @sizeOf(math.Vec4) * texts_buffer_cap), gpu.BindGroup.Entry.buffer(3, glyphs, 0, @sizeOf(Glyph) * texts_buffer_cap), gpu.BindGroup.Entry.sampler(4, texture_sampler), gpu.BindGroup.Entry.textureView(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(text_pipeline: *Mod, core: *mach.Core.Mod) void { const label = @tagName(name) ++ ".preRender"; const encoder = core.state().device.createCommandEncoder(&.{ .label = label }); defer encoder.release(); var archetypes_iter = text_pipeline.entities.query(.{ .all = &.{ .{ .mach_gfx_text_pipeline = &.{ .built, } }, } }); while (archetypes_iter.next()) |archetype| { const built_pipelines = archetype.slice(.mach_gfx_text_pipeline, .built); for (built_pipelines) |built| { // Create the projection matrix // TODO(text): move this out of the hot codepath const proj = math.Mat4x4.projection2D(.{ // TODO(Core) .left = -@as(f32, @floatFromInt(mach.core.size().width)) / 2, .right = @as(f32, @floatFromInt(mach.core.size().width)) / 2, .bottom = -@as(f32, @floatFromInt(mach.core.size().height)) / 2, .top = @as(f32, @floatFromInt(mach.core.size().height)) / 2, .near = -0.1, .far = 100000, }); // Update uniform buffer const uniforms = Uniforms{ .view_projection = proj, // TODO(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(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 archetypes_iter = text_pipeline.entities.query(.{ .all = &.{ .{ .mach_gfx_text_pipeline = &.{ .built, } }, } }); while (archetypes_iter.next()) |archetype| { const ids = archetype.slice(.entity, .id); const built_pipelines = archetype.slice(.mach_gfx_text_pipeline, .built); for (ids, 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); } } }