diff --git a/.gitattributes b/.gitattributes index ef2b2b2..319a87d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ *.hdr filter=lfs diff=lfs merge=lfs -text *.png filter=lfs diff=lfs merge=lfs -text +*.ttf filter=lfs diff=lfs merge=lfs -text diff --git a/assets/fonts/FiraMono-Regular.ttf b/assets/fonts/FiraMono-Regular.ttf new file mode 100644 index 0000000..2a44d07 --- /dev/null +++ b/assets/fonts/FiraMono-Regular.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9c1c51abec8b9f7fa24bfb88ebe80b354cc6da9d4036d6bedeaf495d24933e42 +size 170204 diff --git a/assets/shaders/gui_text.slang b/assets/shaders/gui_text.slang index 918f148..21e1aeb 100644 --- a/assets/shaders/gui_text.slang +++ b/assets/shaders/gui_text.slang @@ -3,15 +3,17 @@ import "includes/structs"; struct Varyings { float4 positionCS : SV_Position; nointerpolation uint instance; - float2 texCoordEM : U_TEXCOORD; + float2 texCoordEM; } struct GuiText { float4 color; - // Index into _GlyphData - uint glyph; float2 bottomLeftSSPX; float fontSizePX; + // Index into _GlyphData[font] + uint16_t glyph; + // Index into _GlyphData, _ControlPoints and _BandData + uint16_t font; } struct GlyphData { @@ -28,10 +30,9 @@ struct GlyphData { [[vk::binding(0, 0)]] ConstantBuffer _Global; [[vk::binding(1, 0)]] StructuredBuffer _Instances; - -[[vk::binding(0, 1)]] StructuredBuffer _GlyphData; -[[vk::binding(1, 1)]] StructuredBuffer _ControlPoints; -[[vk::binding(2, 1)]] StructuredBuffer _BandData; +[[vk::binding(2, 0)]] StructuredBuffer _GlyphData[]; +[[vk::binding(3, 0)]] StructuredBuffer _ControlPoints[]; +[[vk::binding(4, 0)]] StructuredBuffer _BandData[]; static const float2 VERTICES[4] = { float2(0, 1), @@ -70,7 +71,7 @@ uint classifyQuadCurve(float y0, float y1, float y2) let class = (sign0 << 0) | (sign1 << 1) | (sign2 << 2); let shift = 2 * class; - return (0x2E74U >> shift) & 0b11U; + return (0x1DB8 >> shift) & 0b11U; } // Find intersections between a quadratic Bézier curve and `y = 0` line by @@ -156,12 +157,12 @@ Varyings vertexMain(uint vertexID : SV_VulkanVertexID, uint instanceID : SV_Vulk let vertex = VERTICES[vertexID]; let instance = _Instances[instanceID]; - let glyph = _GlyphData[instance.glyph]; + let glyph = _GlyphData[instance.font][instance.glyph]; let normal = 2.0 * vertex - 1.0; let sizePX = instance.fontSizePX * glyph.sizeEM; - let positionSSPX = vertex * sizePX + instance.bottomLeftSSPX; + let positionSSPX = vertex * float2(sizePX.x, -sizePX.y) + instance.bottomLeftSSPX; let dilatedPositionSSPX = positionSSPX + 0.5 * normal; let dilatedPositionCS = float4(mul(_Global.matrixSSPXtoCS, float3(positionSSPX, 1.0)), 0.0, 1.0); @@ -177,7 +178,9 @@ Varyings vertexMain(uint vertexID : SV_VulkanVertexID, uint instanceID : SV_Vulk [shader("fragment")] float4 fragmentMain(Varyings frag) : SV_Target { let instance = _Instances[frag.instance]; - let glyph = _GlyphData[instance.glyph]; + let glyph = _GlyphData[instance.font][instance.glyph]; + let bandData = _BandData[instance.font]; + let controlPoints = _ControlPoints[instance.font]; let emPerPX = fwidth(frag.texCoordEM); let pxPerEM = 1.0 / emPerPX; @@ -191,17 +194,17 @@ float4 fragmentMain(Varyings frag) : SV_Target { var xWeight = 0.0; let horizontalBandPtr = glyph.bandPtr.y + band.y; - let horizontalBandData = _BandData[horizontalBandPtr]; + let horizontalBandData = bandData[horizontalBandPtr]; let horizontalBandCurvePtr = horizontalBandPtr + (horizontalBandData >> 16); let horizontalBandCurveCount = horizontalBandData & 0xFFFF; for (uint horizontalBandCurveID = 0; horizontalBandCurveID < horizontalBandCurveCount; horizontalBandCurveID++) { - let cpPtr = _BandData[horizontalBandCurvePtr + horizontalBandCurveID]; - let cp0 = float2(_ControlPoints[cpPtr]) - frag.texCoordEM; - let cp1 = float2(_ControlPoints[cpPtr + 1]) - frag.texCoordEM; - let cp2 = float2(_ControlPoints[cpPtr + 2]) - frag.texCoordEM; + let cpPtr = bandData[horizontalBandCurvePtr + horizontalBandCurveID]; + let cp0 = float2(controlPoints[cpPtr + 0]) - frag.texCoordEM; + let cp1 = float2(controlPoints[cpPtr + 1]) - frag.texCoordEM; + let cp2 = float2(controlPoints[cpPtr + 2]) - frag.texCoordEM; - if (max(max(cp0.x, cp1.x), cp2.x) * pxPerEM.x < -0.5) break; + if (max(max(cp0.x, cp1.x), cp2.x) * pxPerEM.x < -0.5) break; // XXX let rootFlags = classifyQuadCurve(cp0.y, cp1.y, cp2.y); if (rootFlags != 0U) { @@ -228,17 +231,17 @@ float4 fragmentMain(Varyings frag) : SV_Target { var yWeight = 0.0; let verticalBandPtr = glyph.bandPtr.x + band.x; - let verticalBandData = _BandData[verticalBandPtr]; + let verticalBandData = bandData[verticalBandPtr]; let verticalBandCurvePtr = verticalBandPtr + (verticalBandData >> 16); let verticalBandCurveCount = verticalBandData & 0xFFFF; for (uint verticalBandCurveID = 0; verticalBandCurveID < verticalBandCurveCount; verticalBandCurveID++) { - let cpPtr = _BandData[verticalBandCurvePtr + verticalBandCurveID]; - let cp0 = float2(_ControlPoints[cpPtr]) - frag.texCoordEM; - let cp1 = float2(_ControlPoints[cpPtr + 1]) - frag.texCoordEM; - let cp2 = float2(_ControlPoints[cpPtr + 2]) - frag.texCoordEM; + let cpPtr = bandData[verticalBandCurvePtr + verticalBandCurveID]; + let cp0 = float2(controlPoints[cpPtr + 0]) - frag.texCoordEM; + let cp1 = float2(controlPoints[cpPtr + 1]) - frag.texCoordEM; + let cp2 = float2(controlPoints[cpPtr + 2]) - frag.texCoordEM; - if (max(max(cp0.y, cp1.y), cp2.y) * pxPerEM.y < -0.5) break; + if (max(max(cp0.y, cp1.y), cp2.y) * pxPerEM.y < -0.5) break; // XXX let rootFlags = classifyQuadCurve(cp0.x, cp1.x, cp2.x); if (rootFlags != 0U) { diff --git a/castle b/castle index dc839e0..af8d725 160000 --- a/castle +++ b/castle @@ -1 +1 @@ -Subproject commit dc839e098c54483ddc957a1a7c46adba4036b8b1 +Subproject commit af8d725e0c0b16f89f569fcb0d42b238f94736d5 diff --git a/src/AppContext.zig b/src/AppContext.zig index 0070177..f8af7a7 100644 --- a/src/AppContext.zig +++ b/src/AppContext.zig @@ -10,6 +10,7 @@ const media = @import("media"); const glfw = @import("zglfw"); const Atoms = @import("engine/Atoms.zig"); +const Fonts = @import("engine/Fonts.zig"); const Engine = @import("engine/Engine.zig"); const Gui = @import("engine/Gui.zig"); const Materials = @import("engine/Materials.zig"); @@ -23,6 +24,7 @@ pub var allocator_persistent: std.mem.Allocator = undefined; pub var io: std.Io = undefined; pub var vk_allocator: *VkAllocator = undefined; pub var stbi: *media.stbi = undefined; +pub var stbtt: *media.stbtt = undefined; pub var window: *glfw.Window = undefined; pub var atoms: *Atoms = undefined; @@ -31,4 +33,5 @@ pub var swapchain: *Swapchain = undefined; pub var textures: *Textures = undefined; pub var materials: *Materials = undefined; +pub var fonts: *Fonts = undefined; pub var gui: *Gui = undefined; diff --git a/src/Game.zig b/src/Game.zig index 93d894e..91f3061 100644 --- a/src/Game.zig +++ b/src/Game.zig @@ -16,6 +16,7 @@ const Chunk = @import("Chunk.zig"); const Chunks = @import("Chunks.zig"); const CommandBuffer = @import("engine/CommandBuffer.zig"); const Engine = @import("engine/Engine.zig"); +const Fonts = @import("engine/Fonts.zig"); const Iterator2 = math.Iterator2; const Materials = @import("engine/Materials.zig"); const Player = @import("Player.zig"); @@ -47,6 +48,7 @@ blocks: Blocks, chunks: Chunks, skybox: Skybox, player: Player, +font: Fonts.Id, const max_textures = 1024; const max_point_lights = 1024; @@ -70,6 +72,7 @@ pub fn init() !Game { const swapchain = ctx.swapchain; const textures = ctx.textures; const materials = ctx.materials; + const fonts = ctx.fonts; const gui = ctx.gui; var blocks = try Blocks.init(); @@ -137,7 +140,7 @@ pub fn init() !Game { .descriptor_type = .sampled_image, .descriptor_count = max_textures, .stage_flags = .{ .vertex_bit = true, .fragment_bit = true }, - .flags = .{ .variable_descriptor_count_bit = true }, + .flags = .{ .variable_descriptor_count_bit = true }, // TODO Get rid of this AND somehow integrate with Textures module? }, }, }); @@ -442,6 +445,13 @@ pub fn init() !Game { const block_stone = try blocks.getOrLoad("Stone.json"); const block_bedrock = try blocks.getOrLoad("Bedrock.json"); + const font_id = try fonts.load("FiraMono-Regular.ttf", .{ + .codepoint_ranges = &.{ + .{ .start = 32, .end = 127 }, + }, + .name = "EB Garamond Regular", + }); + // VOLATILE Load all assets before this point const descriptor_images = try allocator_frame.alloc(vk.DescriptorImageInfo, textures.array.items.len); @@ -619,6 +629,8 @@ pub fn init() !Game { var skybox = try Skybox.load("skybox.hdr", 512, global_uniforms.buffer); errdefer skybox.deinit(); + const font = &fonts.array.items[@intFromEnum(font_id)]; + // TODO This coupling is not elegant - the game must provide the GUI with // uniform buffer for globals. Maybe GUI should just use their own global // uniform buffer not for any other reason than decoupling. After all, GUI @@ -641,6 +653,66 @@ pub fn init() !Game { }, }, }, + .{ + .dst_set = gui.text_descriptor_set, + .dst_binding = 0, + .dst_array_element = 0, + .descriptor_type = .uniform_buffer, + .descriptor_infos = .{ + .buffer = &.{ + .{ + .buffer = global_uniforms.buffer, + .offset = 0, + .range = vk.WHOLE_SIZE, + }, + }, + }, + }, + .{ + .dst_set = gui.text_descriptor_set, + .dst_binding = 2, + .dst_array_element = @intFromEnum(font_id), + .descriptor_type = .storage_buffer, + .descriptor_infos = .{ + .buffer = &.{ + .{ + .buffer = font.glyph_data.buffer, + .offset = 0, + .range = vk.WHOLE_SIZE, + }, + }, + }, + }, + .{ + .dst_set = gui.text_descriptor_set, + .dst_binding = 3, + .dst_array_element = @intFromEnum(font_id), + .descriptor_type = .storage_buffer, + .descriptor_infos = .{ + .buffer = &.{ + .{ + .buffer = font.control_points.buffer, + .offset = 0, + .range = vk.WHOLE_SIZE, + }, + }, + }, + }, + .{ + .dst_set = gui.text_descriptor_set, + .dst_binding = 4, + .dst_array_element = @intFromEnum(font_id), + .descriptor_type = .storage_buffer, + .descriptor_infos = .{ + .buffer = &.{ + .{ + .buffer = font.band_data.buffer, + .offset = 0, + .range = vk.WHOLE_SIZE, + }, + }, + }, + }, }, }); @@ -667,6 +739,7 @@ pub fn init() !Game { .chunks = .{ .chunks = chunks }, .skybox = skybox, .player = .init(player_position_sv, 0, 0), + .font = font_id, }; } @@ -741,6 +814,43 @@ pub fn update(self: *Game, dt: f32) void { .border_radius_px = 0, }); + { + const margin: f16 = 8; + const font_size: f32 = 16; + + const player_position_sv = self.player.position_sv; + + gui.pushBox(.{ + .background_color = .init(0, 0, 0, 0.75), + .border_color = .init(0, 0, 0, 1), + .position_sspx = .init(0, 0), + .size_px = .init(157, 4 * margin + 3 * font_size), + .border_width_px = 0, + .border_radius_px = 0, + }); + + gui.printLine("X {X:0>8}", .{@as(u32, @bitCast(player_position_sv.x))}, .{ + .position_sspx = .init(margin, margin + font_size), + .font_id = self.font, + .cap_height_px = font_size, + .color = .init(1, 1, 1, 1), + }); + + gui.printLine("Y {X:0>8}", .{@as(u32, @bitCast(player_position_sv.y))}, .{ + .position_sspx = .init(margin, 2 * (margin + font_size)), + .font_id = self.font, + .cap_height_px = font_size, + .color = .init(1, 1, 1, 1), + }); + + gui.printLine("Z {X:0>8}", .{@as(u32, @bitCast(player_position_sv.z))}, .{ + .position_sspx = .init(margin, 3 * (margin + font_size)), + .font_id = self.font, + .cap_height_px = font_size, + .color = .init(1, 1, 1, 1), + }); + } + self.render() catch |err| { std.log.err("Failed to render: {s}", .{@errorName(err)}); @panic("Frame update failed"); diff --git a/src/engine/Engine.zig b/src/engine/Engine.zig index e48803b..84bcafa 100644 --- a/src/engine/Engine.zig +++ b/src/engine/Engine.zig @@ -272,6 +272,7 @@ pub fn init() !*Engine { var enabled_features_vulkan12: vk.PhysicalDeviceVulkan12Features = .{ .p_next = &enabled_features_vulkan11, + .shader_float_16 = .true, .descriptor_indexing = .true, .descriptor_binding_partially_bound = .true, .descriptor_binding_variable_descriptor_count = .true, diff --git a/src/engine/Font.zig b/src/engine/Font.zig new file mode 100644 index 0000000..3533da5 --- /dev/null +++ b/src/engine/Font.zig @@ -0,0 +1,432 @@ +const Font = @This(); +const std = @import("std"); + +const ctx = @import("../AppContext.zig"); +const shaders = @import("../shaders.zig"); +const vm = @import("vecmath"); + +const GenericBuffer = @import("GenericBuffer.zig").GenericBuffer; + +pub const CodePointRange = struct { + /// inclusive + start: u21, + /// exclusive + end: u21, +}; + +pub const InitInfo = struct { + codepoint_ranges: []const CodePointRange = &.{ + .{ .start = 32, .end = 127 }, + }, + /// [vertical, horizontal] + bands_per_em: vm.Vector2 = .init(8, 8), + name: ?[]const u8 = null, +}; + +const GlyphCpuData = struct { + /// `undefined` when `gpu_glyph_id == null` + bottom_left_em: vm.Vector2, + advance_em: f32, + left_side_bearing_em: f32, + /// If `null`, the glyph is empty (like a space). In such case, we only need + /// to advance the cursor by given `advance_em` and we must not emit any gpu + /// glyph instances. + gpu_glyph_id: ?u16, +}; + +cpu_glyph_data: []GlyphCpuData, +codepoint_glyph_id: std.hash_map.AutoHashMapUnmanaged(u21, u16), +kerning_em: std.hash_map.AutoHashMapUnmanaged([2]u16, f32), +cap_height_em: f32, + +glyph_data: GenericBuffer(void, shaders.GlyphData), +control_points: GenericBuffer(void, [2]f16), +band_data: GenericBuffer(void, u32), + +const BandHelper = struct { + allocator: std.mem.Allocator, + bands: []std.ArrayList(u32), + + fn init(allocator: std.mem.Allocator) BandHelper { + return .{ + .allocator = allocator, + .bands = &.{}, + }; + } + + fn ensureBandCount(self: *BandHelper, desired_band_count: usize) !void { + std.debug.assert(desired_band_count > 0); + + const old_band_count = self.bands.len; + if (old_band_count >= desired_band_count) return; + + const new_band_count = std.math.ceilPowerOfTwo(usize, desired_band_count) catch |err| switch (err) { + error.Overflow => return error.OutOfMemory, + }; + + self.bands = try self.allocator.realloc(self.bands, new_band_count); + for (old_band_count..new_band_count) |i| { + self.bands[i] = .empty; + } + } + + fn clearBands(self: *BandHelper, band_count: usize) void { + for (self.bands[0..band_count]) |*array| { + array.clearRetainingCapacity(); + } + } + + fn insertControlPointPtr(self: *BandHelper, band: usize, ptr: u32) !void { + const array = &self.bands[band]; + try array.append(self.allocator, ptr); + } + + fn sortBands(self: *BandHelper, coordinate: u1, band_count: usize, control_points: []const [2]f16) void { + for (self.bands[0..band_count]) |*array| { + const sort_ctx = SortContext{ + .coordinate = coordinate, + .control_points = control_points, + }; + + std.mem.sortUnstable(u32, array.items, &sort_ctx, SortContext.lessThan); + + // std.debug.print(" BAND {d} sorted by coordinate {d}\n", .{ array - self.bands.ptr, coordinate }); + // for (array.items) |cp_ptr| { + // const cp0 = control_points[cp_ptr + 0]; + // const cp1 = control_points[cp_ptr + 1]; + // const cp2 = control_points[cp_ptr + 2]; + // const max = @max(cp0[coordinate], cp1[coordinate], cp2[coordinate]); + // std.debug.print(" max {d} | ({d}, {d}) → ({d}, {d}) → ({d}, {d})\n", .{ max, cp0[0], cp0[1], cp1[0], cp1[1], cp2[0], cp2[1] }); + // } + } + } + + fn write(self: *const BandHelper, band_count: usize, buffer: *std.ArrayList(u32), buffer_allocator: std.mem.Allocator) !void { + var band_data_ptr = buffer.items.len; + + // --- HEADERS --- + + band_data_ptr += band_count; + + for (self.bands[0..band_count]) |*array| { + const band = array.items; + + // Header is a self-relative forward-only pointer in high 16 bits + // and length in low 16 bits. + + const ptr = std.math.cast(u16, band_data_ptr - buffer.items.len) orelse return error.Overflow; + const len = std.math.cast(u16, band.len) orelse return error.Overflow; + + try buffer.append(buffer_allocator, (@as(u32, ptr) << 16) | len); + + band_data_ptr += len; + } + + // --- BAND DATA --- + + for (self.bands[0..band_count]) |*array| { + const band = array.items; + try buffer.appendSlice(buffer_allocator, band); + } + + std.debug.assert(band_data_ptr == buffer.items.len); + } + + fn deinit(self: *BandHelper) void { + for (self.bands) |*array| { + array.deinit(self.allocator); + } + + self.allocator.free(self.bands); + + self.* = undefined; + } +}; + +const SortContext = struct { + coordinate: u1, + control_points: []const [2]f16, + + fn lessThan(self: *const SortContext, lhs: u32, rhs: u32) bool { + const lhs_cp0 = self.control_points[lhs + 0]; + const lhs_cp1 = self.control_points[lhs + 1]; + const lhs_cp2 = self.control_points[lhs + 2]; + + const lhs_max = @max( + lhs_cp0[self.coordinate], + lhs_cp1[self.coordinate], + lhs_cp2[self.coordinate], + ); + + const rhs_cp0 = self.control_points[rhs + 0]; + const rhs_cp1 = self.control_points[rhs + 1]; + const rhs_cp2 = self.control_points[rhs + 2]; + + const rhs_max = @max( + rhs_cp0[self.coordinate], + rhs_cp1[self.coordinate], + rhs_cp2[self.coordinate], + ); + + return lhs_max >= rhs_max; + } +}; + +pub fn init(ttf_buffer: []const u8, init_info: InitInfo) !Font { + const allocator_general = ctx.allocator_general; + const allocator_frame = ctx.allocator_frame; + const stbtt = ctx.stbtt; + + const ttf_font = try stbtt.initFont(ttf_buffer); + defer ttf_font.deinit(); + + const units_per_em = ttf_font.unitsPerEm(); + const em_per_unit = 1 / @as(f32, @floatFromInt(units_per_em)); + + // std.debug.print("Units per EM: {d}\nEM per unit: {d}\n", .{ units_per_em, em_per_unit }); + + var glyph_count: u16 = 0; + for (init_info.codepoint_ranges) |range| { + const range_size = std.math.cast(u16, range.end - range.start) orelse return error.TooManyGlyphs; + glyph_count = std.math.add(u16, glyph_count, range_size) catch |err| switch (err) { + error.Overflow => return error.TooManyGlyphs, + }; + } + + var codepoint_glyph_id: std.hash_map.AutoHashMapUnmanaged(u21, u16) = .empty; + errdefer codepoint_glyph_id.deinit(allocator_general); + try codepoint_glyph_id.ensureTotalCapacity(allocator_general, glyph_count); + + var kerning_em: std.hash_map.AutoHashMapUnmanaged([2]u16, f32) = .empty; + errdefer kerning_em.deinit(allocator_general); + + const cap_height = ttf_font.getCapHeight() orelse return error.FontError; + const cap_height_em = @as(f32, @floatFromInt(cap_height)) * em_per_unit; + + // std.debug.print("Cap height: {d} U | {d} EM\n", .{ cap_height, cap_height_em }); + + var cpu_glyph_data: std.ArrayList(GlyphCpuData) = try .initCapacity(allocator_general, glyph_count); + errdefer cpu_glyph_data.deinit(allocator_general); + + var gpu_glyph_data: std.ArrayList(shaders.GlyphData) = try .initCapacity(allocator_frame, glyph_count); + + var control_points_array: std.ArrayList([2]f16) = .empty; + defer control_points_array.deinit(allocator_general); + + var band_data_array: std.ArrayList(u32) = .empty; + defer band_data_array.deinit(allocator_general); + + var vertical_band_helper: BandHelper = .init(allocator_general); + defer vertical_band_helper.deinit(); + + var horizontal_band_helper: BandHelper = .init(allocator_general); + defer horizontal_band_helper.deinit(); + + for (init_info.codepoint_ranges) |range| { + var codepoint: u21 = range.start; + while (codepoint < range.end) : (codepoint += 1) { + // std.debug.print("Processing codepoint 0x{X} '{u}'\n", .{ codepoint, codepoint }); + if (ttf_font.findGlyphIndex(codepoint)) |external_glyph_id| { + const cpu_glyph_id = @as(u16, @intCast(cpu_glyph_data.items.len)); + codepoint_glyph_id.putAssumeCapacityNoClobber(codepoint, cpu_glyph_id); + + const glyph_metrics = ttf_font.getGlyphMetrics(external_glyph_id); + // std.debug.print(" Advance: {d} U\n", .{glyph_metrics.advance_width}); + // std.debug.print(" LSB: {d} U\n", .{glyph_metrics.left_side_bearing}); + + if (ttf_font.isGlyphEmpty(external_glyph_id)) { + // std.debug.print(" Glyph is empty\n", .{}); + cpu_glyph_data.appendAssumeCapacity(.{ + .bottom_left_em = undefined, + .left_side_bearing_em = @as(f32, @floatFromInt(glyph_metrics.left_side_bearing)) * em_per_unit, + .advance_em = @as(f32, @floatFromInt(glyph_metrics.advance_width)) * em_per_unit, + .gpu_glyph_id = null, + }); + } else if (ttf_font.getGlyphBox(external_glyph_id)) |box| { + const bottom_left_em = vm.Vector2.init( + @floatFromInt(box.x_min), + @floatFromInt(box.y_min), + ).mulScalar(em_per_unit); + const size_em = vm.Vector2.init( + @as(f32, @floatFromInt(box.x_max)) - @as(f32, @floatFromInt(box.x_min)), + @as(f32, @floatFromInt(box.y_max)) - @as(f32, @floatFromInt(box.y_min)), + ).mulScalar(em_per_unit); + + const gpu_glyph_id = @as(u16, @intCast(gpu_glyph_data.items.len)); + cpu_glyph_data.appendAssumeCapacity(.{ + .bottom_left_em = bottom_left_em, + .left_side_bearing_em = @as(f32, @floatFromInt(glyph_metrics.left_side_bearing)) * em_per_unit, + .advance_em = @as(f32, @floatFromInt(glyph_metrics.advance_width)) * em_per_unit, + .gpu_glyph_id = gpu_glyph_id, + }); + + const band_count = vm.Vector2.max(.round(.mul(size_em, init_info.bands_per_em)), .initScalar(1)); + + const vertical_band_count: u32 = @trunc(band_count.x); + const horizontal_band_count: u32 = @trunc(band_count.y); + + try vertical_band_helper.ensureBandCount(vertical_band_count); + try horizontal_band_helper.ensureBandCount(horizontal_band_count); + + vertical_band_helper.clearBands(vertical_band_count); + horizontal_band_helper.clearBands(horizontal_band_count); + + const vertices = ttf_font.getGlyphShape(external_glyph_id); + defer ttf_font.freeShape(vertices); + + // std.debug.print(" bottom left: {f} [EM]\n", .{bottom_left_em}); + // std.debug.print(" size: {f} [EM]\n", .{size_em}); + // std.debug.print(" band count: {f}\n", .{band_count}); + // std.debug.print(" vertices: {d}\n", .{vertices.len}); + + var cp0: vm.Vector2 = undefined; + for (vertices) |vertex| { + var cp1: vm.Vector2 = undefined; + var cp2: vm.Vector2 = undefined; + switch (vertex.type) { + .STBTT_vmove => { + cp0.x = @as(f32, @floatFromInt(vertex.x)) * em_per_unit; + cp0.y = @as(f32, @floatFromInt(vertex.y)) * em_per_unit; + try control_points_array.append( + allocator_general, + .{ @floatCast(cp0.x), @floatCast(cp0.y) }, + ); + continue; + }, + .STBTT_vline => { + cp1.x = @as(f32, @floatFromInt(vertex.x)) * em_per_unit; + cp1.y = @as(f32, @floatFromInt(vertex.y)) * em_per_unit; + cp2 = cp1; + try control_points_array.appendNTimes( + allocator_general, + .{ @floatCast(cp1.x), @floatCast(cp1.y) }, + 2, + ); + }, + .STBTT_vcurve => { + cp1.x = @as(f32, @floatFromInt(vertex.cx)) * em_per_unit; + cp1.y = @as(f32, @floatFromInt(vertex.cy)) * em_per_unit; + cp2.x = @as(f32, @floatFromInt(vertex.x)) * em_per_unit; + cp2.y = @as(f32, @floatFromInt(vertex.y)) * em_per_unit; + try control_points_array.append(allocator_general, .{ @floatCast(cp1.x), @floatCast(cp1.y) }); + try control_points_array.append(allocator_general, .{ @floatCast(cp2.x), @floatCast(cp2.y) }); + }, + .STBTT_vcubic => unreachable, + } + defer cp0 = cp2; + + const cp_min = cp0.min(cp1).min(cp2).sub(.initScalar(em_per_unit)); + const cp_max = cp0.max(cp1).max(cp2).add(.initScalar(em_per_unit)); + + const min_band_f = band_count.mul(.sub(cp_min, bottom_left_em)).div(size_em) + .max(.initScalar(0)) + .min(.sub(band_count, .initScalar(1))) + .floor(); + const max_band_f = band_count.mul(.sub(cp_max, bottom_left_em)).div(size_em) + .max(.initScalar(0)) + .min(.sub(band_count, .initScalar(1))) + .floor(); + + // std.debug.print(" {f} → {f} → {f} | bands: {f} → {f}\n", .{ cp0, cp1, cp2, min_band_f, max_band_f }); + + const vertical_band_start = @as(u32, @trunc(min_band_f.x)); + const vertical_band_end = @as(u32, @trunc(max_band_f.x)) + 1; + const horizontal_band_start = @as(u32, @trunc(min_band_f.y)); + const horizontal_band_end = @as(u32, @trunc(max_band_f.y)) + 1; + + const cp_ptr = @as(u32, @intCast(control_points_array.items.len - 3)); + + for (vertical_band_start..vertical_band_end) |vband| { + try vertical_band_helper.insertControlPointPtr(vband, cp_ptr); + } + + for (horizontal_band_start..horizontal_band_end) |hband| { + try horizontal_band_helper.insertControlPointPtr(hband, cp_ptr); + } + + cp0 = cp2; + } + + const vertical_band_ptr = @as(u32, @intCast(band_data_array.items.len)); + vertical_band_helper.sortBands(1, vertical_band_count, control_points_array.items); + try vertical_band_helper.write(vertical_band_count, &band_data_array, allocator_general); + + const horizontal_band_ptr = @as(u32, @intCast(band_data_array.items.len)); + horizontal_band_helper.sortBands(0, horizontal_band_count, control_points_array.items); + try horizontal_band_helper.write(horizontal_band_count, &band_data_array, allocator_general); + + // std.debug.print(" vband: [{d}] @ {d}\n hband: [{d}] @ {d}\n", .{ vertical_band_count, vertical_band_ptr, horizontal_band_count, horizontal_band_ptr }); + + gpu_glyph_data.appendAssumeCapacity(.{ + .bottomLeftEM = bottom_left_em, + .sizeEM = size_em, + .verticalBandPtr = vertical_band_ptr, + .horizontalBandPtr = horizontal_band_ptr, + .verticalBandCount = vertical_band_count, + .horizontalBandCount = horizontal_band_count, + }); + } else { + return error.FontError; + } + } else { + return error.MissingGlyph; + } + } + } + + var glyph_data: GenericBuffer(void, shaders.GlyphData) = try .init(.{ + .usage = .storage, + .target_queue = .graphics, + .array_capacity = @intCast(gpu_glyph_data.items.len), + .name = "Glyph data", + }); + errdefer glyph_data.deinit(); + + // std.debug.print("Glyph data size: {d} [B]\n", .{gpu_glyph_data.items.len * @sizeOf(shaders.GlyphData)}); + + var control_points: GenericBuffer(void, [2]f16) = try .init(.{ + .usage = .storage, + .target_queue = .graphics, + .array_capacity = @intCast(control_points_array.items.len), + .name = "Control points", + }); + errdefer control_points.deinit(); + + // std.debug.print("Control points size: {d} [B]\n", .{control_points_array.items.len * @sizeOf([2]f16)}); + + var band_data: GenericBuffer(void, u32) = try .init(.{ + .usage = .storage, + .target_queue = .graphics, + .array_capacity = @intCast(band_data_array.items.len), + .name = "Band data", + }); + errdefer band_data.deinit(); + + // std.debug.print("Band data size: {d} [B]\n", .{band_data_array.items.len * @sizeOf(u32)}); + + try glyph_data.write(.{ .elements = gpu_glyph_data.items }); + try control_points.write(.{ .elements = control_points_array.items }); + try band_data.write(.{ .elements = band_data_array.items }); + + return .{ + .cpu_glyph_data = cpu_glyph_data.toOwnedSliceAssert(), + .codepoint_glyph_id = codepoint_glyph_id, + .kerning_em = kerning_em, + .cap_height_em = cap_height_em, + + .glyph_data = glyph_data, + .control_points = control_points, + .band_data = band_data, + }; +} + +pub fn deinit(self: *Font) void { + const allocator_general = ctx.allocator_general; + + self.band_data.deinit(); + self.control_points.deinit(); + self.glyph_data.deinit(); + + self.codepoint_glyph_id.deinit(allocator_general); + allocator_general.free(self.cpu_glyph_data); +} diff --git a/src/engine/Fonts.zig b/src/engine/Fonts.zig new file mode 100644 index 0000000..5a4ae9e --- /dev/null +++ b/src/engine/Fonts.zig @@ -0,0 +1,111 @@ +const Fonts = @This(); +const std = @import("std"); + +const ctx = @import("../AppContext.zig"); + +const Atom = @import("../engine/Atom.zig").Atom; +const Font = @import("Font.zig"); + +pub const Id = enum(u16) { + // VOLATILE Synchronize explicit values with `init` implementation. + + _, + + /// Cast an integer into an ID. This can produce an invalid ID. + pub fn fromInt(value: u16) Id { + return @enumFromInt(value); + } + + /// Cast an index into an ID. This can produce an invalid ID. The caller + /// asserts that the index is not greater than the max ID value. + pub fn fromIndex(index: usize) Id { + std.debug.assert(index < max_fonts); + return @enumFromInt(@as(u16, @intCast(index))); + } + + /// Cast an index into an ID. This can produce an invalid ID. Returns an + /// error if the index is greater than the max ID value. + pub fn fromIndexSafe(index: usize) error{Overflow}!Id { + if (index >= max_fonts) return error.Overflow; + return @enumFromInt(@as(u16, @intCast(index))); + } + + /// Cast an ID into an integer. + pub fn toInt(self: Id) u16 { + return @intFromEnum(self); + } +}; + +/// Stores all `Font` structs and maps a font ID to a `Font` struct. +/// Preallocated with `allocator_persistent`. +array: std.ArrayList(Font), + +pub const max_fonts = 256; + +pub fn init() !*Fonts { + const allocator_persistent = ctx.allocator_persistent; + + const fonts = try allocator_persistent.create(Fonts); + + fonts.* = .{ + .array = .empty, + }; + + errdefer { + for (fonts.array.items) |*font| { + font.deinit(); + } + } + + try fonts.array.ensureTotalCapacityPrecise(allocator_persistent, max_fonts); + + // VOLATILE Synchronize with explicit values on top of `Id` type. + + return fonts; +} + +pub fn deinit(self: *Fonts) void { + std.log.scoped(.deinit).debug("Deinitializing {*}", .{self}); + + for (self.array.items) |*font| { + font.deinit(); + } + self.* = undefined; +} + +pub fn load( + self: *Fonts, + filename: []const u8, + init_info: Font.InitInfo, +) !Id { + const id = Id.fromIndexSafe(self.array.items.len) catch |err| switch (err) { + error.Overflow => return error.OutOfFonts, + }; + const font = try loadFont(filename, init_info); + + self.array.appendAssumeCapacity(font); + + return id; +} + +fn loadFont( + filename: []const u8, + init_info: Font.InitInfo, +) !Font { + const allocator_frame = ctx.allocator_frame; + const io = ctx.io; + + std.log.debug("Loading font \"{s}\"...", .{filename}); + + const cwd = std.Io.Dir.cwd(); + + var dir = try cwd.openDir(io, "assets/fonts", .{}); + defer dir.close(io); + + const ttf_buffer = try dir.readFileAlloc(io, filename, allocator_frame, .unlimited); + + var font = try Font.init(ttf_buffer, init_info); + errdefer font.deinit(); + + return font; +} diff --git a/src/engine/Gui.zig b/src/engine/Gui.zig index aaf0ca2..3837243 100644 --- a/src/engine/Gui.zig +++ b/src/engine/Gui.zig @@ -8,6 +8,7 @@ const vm = @import("vecmath"); const CommandBuffer = @import("CommandBuffer.zig"); const Engine = @import("Engine.zig"); +const Fonts = @import("Fonts.zig"); const GenericBuffer = @import("GenericBuffer.zig").GenericBuffer; const Swapchain = @import("Swapchain.zig"); const Texture = @import("Texture.zig"); @@ -25,11 +26,10 @@ pub const Draw = struct { pub const Text = extern struct { color: vm.Vector4, - pos_min: vm.Vector2, - pos_max: vm.Vector2, - uv_min: vm.Vector2, - uv_max: vm.Vector2, - atlas_id: Textures.Id, + bottom_left_sspx: vm.Vector2, + font_size_px: f32, + glyph_id: u16, + font_id: Fonts.Id, }; pub const Image = extern struct { @@ -65,8 +65,13 @@ box_descriptor_set_layout: vk.DescriptorSetLayout, box_pipeline_layout: vk.PipelineLayout, box_pipeline: vk.Pipeline, +text_descriptor_set_layout: vk.DescriptorSetLayout, +text_pipeline_layout: vk.PipelineLayout, +text_pipeline: vk.Pipeline, + descriptor_pool: vk.DescriptorPool, box_descriptor_set: vk.DescriptorSet, +text_descriptor_set: vk.DescriptorSet, pub const max_box_draws = 1024; pub const max_image_draws = 1024; @@ -273,16 +278,169 @@ pub fn init() !*Gui { errdefer engine.destroyPipeline(box_pipeline); engine.setObjectName(box_pipeline, "P GUI Box", .{}); + const text_descriptor_set_layout = try engine.createDescriptorSetLayout(.{ + .bindings = &.{ + .{ + .binding = 0, + .descriptor_type = .uniform_buffer, + .descriptor_count = 1, + .stage_flags = .{ .vertex_bit = true, .fragment_bit = true }, + }, + .{ + .binding = 1, + .descriptor_type = .storage_buffer, + .descriptor_count = 1, + .stage_flags = .{ .vertex_bit = true, .fragment_bit = true }, + }, + .{ + .binding = 2, + .descriptor_type = .storage_buffer, + .descriptor_count = Fonts.max_fonts, + .stage_flags = .{ .vertex_bit = true, .fragment_bit = true }, + }, + .{ + .binding = 3, + .descriptor_type = .storage_buffer, + .descriptor_count = Fonts.max_fonts, + .stage_flags = .{ .vertex_bit = true, .fragment_bit = true }, + }, + .{ + .binding = 4, + .descriptor_type = .storage_buffer, + .descriptor_count = Fonts.max_fonts, + .stage_flags = .{ .vertex_bit = true, .fragment_bit = true }, + }, + }, + }); + errdefer engine.destroyDescriptorSetLayout(text_descriptor_set_layout); + engine.setObjectName(text_descriptor_set_layout, "DSL GUI Text", .{}); + + const text_pipeline_layout = try engine.createPipelineLayout(.{ + .set_layouts = &.{ + text_descriptor_set_layout, + }, + }); + errdefer engine.destroyPipelineLayout(text_pipeline_layout); + engine.setObjectName(text_pipeline_layout, "PL GUI Text", .{}); + + const text_shader = try engine.createShaderModule(.{ .code = &shaders.gui_text_spv }); + defer engine.destroyShaderModule(text_shader); + engine.setObjectName(text_shader, "SM gui_text", .{}); + + const text_pipeline = try engine.createGraphicsPipeline(.{ + .stages = &.{ + .{ + .stage = .{ .vertex_bit = true }, + .module = text_shader, + .name = "vertexMain", + }, + .{ + .stage = .{ .fragment_bit = true }, + .module = text_shader, + .name = "fragmentMain", + }, + }, + .vertex_input_state = .{}, + .input_assembly_state = .{ + .topology = .triangle_list, + .primitive_restart_enable = .false, + }, + .viewport_state = .{ + .viewports = &.{undefined}, // dynamic + .scissors = &.{undefined}, // dynamic + }, + .rasterization_state = .{ + .depth_clamp_enable = .false, + .rasterizer_discard_enable = .false, + .polygon_mode = .fill, + .cull_mode = .{}, + .front_face = .counter_clockwise, + .depth_bias_enable = .false, + .depth_bias_constant_factor = 0, + .depth_bias_clamp = 0, + .depth_bias_slope_factor = 0, + .line_width = 1, + }, + .multisample_state = .{ + .rasterization_samples = .{ .@"1_bit" = true }, + .sample_shading_enable = .false, + .min_sample_shading = 1, + .alpha_to_coverage_enable = .false, + .alpha_to_one_enable = .false, + }, + .depth_stencil_state = .{ + .depth_test_enable = .false, + .depth_write_enable = .false, + .depth_compare_op = .always, + .depth_bounds_test_enable = .false, + .stencil_test_enable = .false, + .front = .{ + .fail_op = .keep, + .pass_op = .keep, + .depth_fail_op = .keep, + .compare_op = .never, + .compare_mask = 0, + .write_mask = 0, + .reference = 0, + }, + .back = .{ + .fail_op = .keep, + .pass_op = .keep, + .depth_fail_op = .keep, + .compare_op = .never, + .compare_mask = 0, + .write_mask = 0, + .reference = 0, + }, + .min_depth_bounds = 0, + .max_depth_bounds = 1, + }, + .color_blend_state = .{ + .logic_op_enable = .false, + .logic_op = .copy, + .attachments = &.{ + .{ + .blend_enable = .true, + .src_color_blend_factor = .one, + .dst_color_blend_factor = .one_minus_src_alpha, + .color_blend_op = .add, + .src_alpha_blend_factor = .one, + .dst_alpha_blend_factor = .one_minus_src_alpha, + .alpha_blend_op = .add, + .color_write_mask = .{ + .r_bit = true, + .g_bit = true, + .b_bit = true, + .a_bit = true, + }, + }, + }, + .blend_constants = .{ 0, 0, 0, 0 }, + }, + .dynamic_state = .{ + .dynamic_states = &.{ .viewport, .scissor }, + }, + .layout = text_pipeline_layout, + .view_mask = 0, + .color_attachment_formats = &.{swapchain.params.surface_format.format}, + .depth_attachment_format = Texture.Usage.depth.vkFormat(), + .stencil_attachment_format = .undefined, + }); + errdefer engine.destroyPipeline(text_pipeline); + engine.setObjectName(text_pipeline, "P GUI Box", .{}); + const descriptor_pool = try engine.createDescriptorPool(.{ - .max_sets = 1, + .max_sets = 2, .pool_sizes = &.{ .{ .type = .uniform_buffer, - .descriptor_count = 1, + .descriptor_count = 1 + // box + 1, // text }, .{ .type = .storage_buffer, - .descriptor_count = 1, + .descriptor_count = 1 + // box + (1 + 3 * Fonts.max_fonts), // text }, }, }); @@ -295,6 +453,12 @@ pub fn init() !*Gui { }); engine.setObjectName(box_descriptor_set, "DS GUI Box", .{}); + const text_descriptor_set = try engine.allocateDescriptorSet(.{ + .descriptor_pool = descriptor_pool, + .set_layout = text_descriptor_set_layout, + }); + engine.setObjectName(text_descriptor_set, "DS GUI Text", .{}); + try engine.updateDescriptorSets(.{ .writes = &.{ .{ @@ -312,6 +476,21 @@ pub fn init() !*Gui { }, }, }, + .{ + .dst_set = text_descriptor_set, + .dst_binding = 1, + .dst_array_element = 0, + .descriptor_type = .storage_buffer, + .descriptor_infos = .{ + .buffer = &.{ + .{ + .buffer = text_gpu_buffer.buffer, + .offset = 0, + .range = vk.WHOLE_SIZE, + }, + }, + }, + }, }, }); @@ -333,8 +512,13 @@ pub fn init() !*Gui { .box_pipeline_layout = box_pipeline_layout, .box_pipeline = box_pipeline, + .text_descriptor_set_layout = text_descriptor_set_layout, + .text_pipeline_layout = text_pipeline_layout, + .text_pipeline = text_pipeline, + .descriptor_pool = descriptor_pool, .box_descriptor_set = box_descriptor_set, + .text_descriptor_set = text_descriptor_set, }; return gui; @@ -346,9 +530,12 @@ pub fn deinit(self: *Gui) void { std.log.scoped(.deinit).debug("Deinitializing {*}", .{self}); engine.destroyDescriptorPool(self.descriptor_pool); + engine.destroyPipeline(self.text_pipeline); engine.destroyPipeline(self.box_pipeline); self.index_buffer.deinit(); + engine.destroyPipelineLayout(self.text_pipeline_layout); engine.destroyPipelineLayout(self.box_pipeline_layout); + engine.destroyDescriptorSetLayout(self.text_descriptor_set_layout); engine.destroyDescriptorSetLayout(self.box_descriptor_set_layout); engine.destroySampler(self.sampler); @@ -379,6 +566,67 @@ pub fn pushText(self: *Gui, text: Draw.Text) void { self.extendOrAddBatch(.Text, instance); } +pub const PrintLineArgs = struct { + position_sspx: vm.Vector2, + font_id: Fonts.Id, + cap_height_px: f32, + color: vm.Vector4, +}; + +pub fn printLine(self: *Gui, comptime fmt: []const u8, fmt_args: anytype, print_args: PrintLineArgs) void { + const allocator_frame = ctx.allocator_frame; + const fonts = ctx.fonts; + + const text = std.fmt.allocPrint(allocator_frame, fmt, fmt_args) catch |err| { + std.log.err("Failed formatting of string \"{s}\": {}", .{ fmt, err }); + return; + }; + + const font = fonts.array.items[@intFromEnum(print_args.font_id)]; + const font_size_px = print_args.cap_height_px / font.cap_height_em; + + const utf8_view = std.unicode.Utf8View.init(text) catch { + std.log.err("Invalid UTF-8 sequence in GUI print line", .{}); + return; + }; + + var cursor_sspx = print_args.position_sspx; + var maybe_prev_glyph: ?u16 = null; + var it = utf8_view.iterator(); + while (it.nextCodepoint()) |codepoint| { + const glyph_id = font.codepoint_glyph_id.get(codepoint) orelse { + std.log.warn("Missing codepoint 0x{X} '{u}' in font #{d}", .{ codepoint, codepoint, print_args.font_id }); + maybe_prev_glyph = null; + continue; + }; + defer maybe_prev_glyph = glyph_id; + + const glyph_data = &font.cpu_glyph_data[glyph_id]; + + var cursor_offset_em = glyph_data.bottom_left_em; + cursor_offset_em.y = -cursor_offset_em.y; + cursor_offset_em.x += glyph_data.left_side_bearing_em; + + if (maybe_prev_glyph) |prev_glyph| { + const kerning_em = font.kerning_em.get(.{ prev_glyph, glyph_id }) orelse 0; + cursor_offset_em.x += kerning_em; + } + + if (glyph_data.gpu_glyph_id) |gpu_glyph_id| { + self.pushText(.{ + .color = print_args.color, + .bottom_left_sspx = cursor_sspx.add(cursor_offset_em.mulScalar(font_size_px)), + .font_size_px = font_size_px, + .glyph_id = gpu_glyph_id, + .font_id = print_args.font_id, + }); + } + + const advance_px = font_size_px * glyph_data.advance_em; + cursor_sspx.x += advance_px; + } +} + pub fn pushImage(self: *Gui, image: Draw.Image) void { const instance: u32 = @intCast(self.image_cpu_buffer.items.len); self.image_cpu_buffer.appendBounded(image) catch return; @@ -413,8 +661,8 @@ pub fn draw(self: *const Gui, command_buffer: CommandBuffer) !void { command_buffer.bindDescriptorSet(.graphics, self.box_pipeline_layout, 0, self.box_descriptor_set, null); }, .Text => { - std.log.warn("GUI Text not implemented; skipping batch", .{}); - continue; + command_buffer.bindPipeline(.graphics, self.text_pipeline); + command_buffer.bindDescriptorSet(.graphics, self.text_pipeline_layout, 0, self.text_descriptor_set, null); }, .Image => { std.log.warn("GUI Image not implemented; skipping batch", .{}); diff --git a/src/main.zig b/src/main.zig index a168216..326c0ad 100644 --- a/src/main.zig +++ b/src/main.zig @@ -41,6 +41,9 @@ pub fn main(init: std.process.Init) !void { ctx.stbi = try ctx.allocator_persistent.create(media.stbi); ctx.stbi.* = .init(ctx.allocator_general, ctx.io); defer ctx.stbi.deinit(); + ctx.stbtt = try ctx.allocator_persistent.create(media.stbtt); + ctx.stbtt.* = .init(ctx.allocator_general, ctx.io); + defer ctx.stbtt.deinit(); // --- INITIALIZE APP CONTEXT --- @@ -105,6 +108,12 @@ pub fn main(init: std.process.Init) !void { ctx.materials = undefined; } + ctx.fonts = try .init(); + defer { + ctx.fonts.deinit(); + ctx.fonts = undefined; + } + ctx.gui = try .init(); defer { ctx.gui.deinit(); diff --git a/src/shaders.zig b/src/shaders.zig index f84468a..026d98b 100644 --- a/src/shaders.zig +++ b/src/shaders.zig @@ -99,7 +99,17 @@ pub const ObjectUniforms = extern struct { material: Materials.Id, }; +pub const GlyphData = extern struct { + bottomLeftEM: vm.Vector2, + sizeEM: vm.Vector2, + verticalBandPtr: u32, + horizontalBandPtr: u32, + verticalBandCount: u32, + horizontalBandCount: u32, +}; + pub const equirect_to_cube_spv align(4) = @embedFile("shaders/equirect_to_cube").*; pub const gui_box_spv align(4) = @embedFile("shaders/gui_box").*; +pub const gui_text_spv align(4) = @embedFile("shaders/gui_text").*; pub const main_spv align(4) = @embedFile("shaders/main").*; pub const skybox_spv align(4) = @embedFile("shaders/skybox").*;