From 5546469c37dfb82e950ff205322caf6607f691e1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 30 Mar 2024 09:48:44 -0700 Subject: [PATCH 01/53] font: remove unused struct --- src/font/DeferredFace.zig | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/font/DeferredFace.zig b/src/font/DeferredFace.zig index 6ff7f3922..e68988e94 100644 --- a/src/font/DeferredFace.zig +++ b/src/font/DeferredFace.zig @@ -19,17 +19,6 @@ const Presentation = @import("main.zig").Presentation; const log = std.log.scoped(.deferred_face); -/// The struct used for deferred face state. -/// -/// TODO: Change the "fc", "ct", "wc" fields in @This to just use one field -/// with the state since there should be no world in which multiple are used. -const FaceState = switch (options.backend) { - .freetype => void, - .fontconfig_freetype => Fontconfig, - .coretext_freetype, .coretext => CoreText, - .web_canvas => WebCanvas, -}; - /// Fontconfig fc: if (options.backend == .fontconfig_freetype) ?Fontconfig else void = if (options.backend == .fontconfig_freetype) null else {}, From 7fef1aa29459fb681b115dbd02ac0b7398090a95 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 1 Apr 2024 11:54:41 -0700 Subject: [PATCH 02/53] font: descritor can hash using a hasher --- src/font/Group.zig | 2 +- src/font/discovery.zig | 36 ++++++++++++++++++++---------------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/font/Group.zig b/src/font/Group.zig index c08f7bd34..76c69d0f8 100644 --- a/src/font/Group.zig +++ b/src/font/Group.zig @@ -53,7 +53,7 @@ const DescriptorCache = std.HashMapUnmanaged( pub fn hash(ctx: @This(), k: KeyType) u64 { _ = ctx; - return k.hash(); + return k.hashcode(); } pub fn eql(ctx: @This(), a: KeyType, b: KeyType) bool { diff --git a/src/font/discovery.zig b/src/font/discovery.zig index 8874c3436..c926445fd 100644 --- a/src/font/discovery.zig +++ b/src/font/discovery.zig @@ -57,27 +57,31 @@ pub const Descriptor = struct { /// will be preferred, but not guaranteed. variations: []const Variation = &.{}, - /// Returns a hash code that can be used to uniquely identify this - /// action. - pub fn hash(self: Descriptor) u64 { + /// Hash the descriptor with the given hasher. + pub fn hash(self: Descriptor, hasher: anytype) void { const autoHash = std.hash.autoHash; - var hasher = std.hash.Wyhash.init(0); - autoHash(&hasher, self.family); - autoHash(&hasher, self.style); - autoHash(&hasher, self.codepoint); - autoHash(&hasher, self.size); - autoHash(&hasher, self.bold); - autoHash(&hasher, self.italic); - autoHash(&hasher, self.monospace); - autoHash(&hasher, self.variations.len); + autoHash(hasher, self.family); + autoHash(hasher, self.style); + autoHash(hasher, self.codepoint); + autoHash(hasher, self.size); + autoHash(hasher, self.bold); + autoHash(hasher, self.italic); + autoHash(hasher, self.monospace); + autoHash(hasher, self.variations.len); for (self.variations) |variation| { - autoHash(&hasher, variation.id); + autoHash(hasher, variation.id); // This is not correct, but we don't currently depend on the // hash value being different based on decimal values of variations. - autoHash(&hasher, @as(u64, @intFromFloat(variation.value))); + autoHash(hasher, @as(u64, @intFromFloat(variation.value))); } + } + /// Returns a hash code that can be used to uniquely identify this + /// action. + pub fn hashcode(self: Descriptor) u64 { + var hasher = std.hash.Wyhash.init(0); + self.hash(&hasher); return hasher.final(); } @@ -552,7 +556,7 @@ test "descriptor hash" { const testing = std.testing; var d: Descriptor = .{}; - try testing.expect(d.hash() != 0); + try testing.expect(d.hashcode() != 0); } test "descriptor hash familiy names" { @@ -560,7 +564,7 @@ test "descriptor hash familiy names" { var d1: Descriptor = .{ .family = "A" }; var d2: Descriptor = .{ .family = "B" }; - try testing.expect(d1.hash() != d2.hash()); + try testing.expect(d1.hashcode() != d2.hashcode()); } test "fontconfig" { From bfcd5f380a91a87f2c2cd982747d6fb5946f78db Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 1 Apr 2024 12:25:21 -0700 Subject: [PATCH 03/53] font: introduce GroupCacheSet, use it for descriptors to start --- src/Surface.zig | 101 ++++++++--------------- src/font/GroupCacheSet.zig | 164 +++++++++++++++++++++++++++++++++++++ src/font/main.zig | 1 + 3 files changed, 197 insertions(+), 69 deletions(-) create mode 100644 src/font/GroupCacheSet.zig diff --git a/src/Surface.zig b/src/Surface.zig index 39f899d1b..dac8ba705 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -320,6 +320,12 @@ pub fn init( .ydpi = @intFromFloat(y_dpi), }; + // Create our font group key. This is used to determine if we have + // a cached font group we can use already. Otherwise, this can be + // used to build the group. + var font_group_key = try font.GroupCacheSet.Key.init(alloc, config); + defer font_group_key.deinit(); + // Find all the fonts for this surface // // Future: we can share the font group amongst all surfaces to save @@ -368,84 +374,41 @@ pub fn init( // A buffer we use to store the font names for logging. var name_buf: [256]u8 = undefined; - for (config.@"font-family".list.items) |family| { - var disco_it = try disco.discover(alloc, .{ - .family = family, - .style = config.@"font-style".nameValue(), - .size = font_size.points, - .variations = config.@"font-variation".list.items, - }); - defer disco_it.deinit(); - if (try disco_it.next()) |face| { - log.info("font regular: {s}", .{try face.name(&name_buf)}); - _ = try group.addFace(.regular, .{ .deferred = face }); - } else log.warn("font-family not found: {s}", .{family}); - } - - // In all the styled cases below, we prefer to specify an exact - // style via the `font-style` configuration. If a style is not - // specified, we use the discovery mechanism to search for a - // style category such as bold, italic, etc. We can't specify both - // because the latter will restrict the search to only that. If - // a user says `font-style = italic` for the bold face for example, - // no results would be found if we restrict to ALSO searching for - // italic. - for (config.@"font-family-bold".list.items) |family| { - const style = config.@"font-style-bold".nameValue(); - var disco_it = try disco.discover(alloc, .{ - .family = family, - .style = style, - .size = font_size.points, - .bold = style == null, - .variations = config.@"font-variation-bold".list.items, - }); - defer disco_it.deinit(); - if (try disco_it.next()) |face| { - log.info("font bold: {s}", .{try face.name(&name_buf)}); - _ = try group.addFace(.bold, .{ .deferred = face }); - } else log.warn("font-family-bold not found: {s}", .{family}); - } - for (config.@"font-family-italic".list.items) |family| { - const style = config.@"font-style-italic".nameValue(); - var disco_it = try disco.discover(alloc, .{ - .family = family, - .style = style, - .size = font_size.points, - .italic = style == null, - .variations = config.@"font-variation-italic".list.items, - }); - defer disco_it.deinit(); - if (try disco_it.next()) |face| { - log.info("font italic: {s}", .{try face.name(&name_buf)}); - _ = try group.addFace(.italic, .{ .deferred = face }); - } else log.warn("font-family-italic not found: {s}", .{family}); - } - for (config.@"font-family-bold-italic".list.items) |family| { - const style = config.@"font-style-bold-italic".nameValue(); - var disco_it = try disco.discover(alloc, .{ - .family = family, - .style = style, - .size = font_size.points, - .bold = style == null, - .italic = style == null, - .variations = config.@"font-variation-bold-italic".list.items, - }); - defer disco_it.deinit(); - if (try disco_it.next()) |face| { - log.info("font bold+italic: {s}", .{try face.name(&name_buf)}); - _ = try group.addFace(.bold_italic, .{ .deferred = face }); - } else log.warn("font-family-bold-italic not found: {s}", .{family}); + inline for (@typeInfo(font.Style).Enum.fields) |field| { + const style = @field(font.Style, field.name); + for (font_group_key.descriptorsForStyle(style)) |desc| { + var disco_it = try disco.discover(alloc, desc); + defer disco_it.deinit(); + if (try disco_it.next()) |face| { + log.info("font {s}: {s}", .{ + field.name, + try face.name(&name_buf), + }); + _ = try group.addFace(style, .{ .deferred = face }); + } else log.warn("font-family {s} not found: {s}", .{ + field.name, + desc.family.?, + }); + } } } // Our built-in font will be used as a backup _ = try group.addFace( .regular, - .{ .fallback_loaded = try font.Face.init(font_lib, face_ttf, group.faceOptions()) }, + .{ .fallback_loaded = try font.Face.init( + font_lib, + face_ttf, + group.faceOptions(), + ) }, ); _ = try group.addFace( .bold, - .{ .fallback_loaded = try font.Face.init(font_lib, face_bold_ttf, group.faceOptions()) }, + .{ .fallback_loaded = try font.Face.init( + font_lib, + face_bold_ttf, + group.faceOptions(), + ) }, ); // Auto-italicize if we have to. diff --git a/src/font/GroupCacheSet.zig b/src/font/GroupCacheSet.zig new file mode 100644 index 000000000..c4786b216 --- /dev/null +++ b/src/font/GroupCacheSet.zig @@ -0,0 +1,164 @@ +//! This structure contains a set of GroupCache instances keyed by +//! unique font configuration. +//! +//! Most terminals (surfaces) will share the same font configuration. +//! This structure allows expensive font information such as +//! the font atlas, glyph cache, font faces, etc. to be shared. +//! +//! The Ghostty renderers which use this information run on their +//! own threads so this structure is thread-safe. It expects that +//! the case where all glyphs are cached is the common case and +//! optimizes for that. When a glyph is not cached, all renderers +//! that share the same font configuration will be blocked until +//! the glyph is cached. +const GroupCacheSet = @This(); + +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; +const Style = @import("main.zig").Style; +const discovery = @import("discovery.zig"); +const configpkg = @import("../config.zig"); +const Config = configpkg.Config; + +/// The key used to uniquely identify a font configuration. +pub const Key = struct { + arena: ArenaAllocator, + + /// The descriptors used for all the fonts added to the + /// initial group, including all styles. This is hashed + /// in order so the order matters. All users of the struct + /// should ensure that the order is consistent. + descriptors: []const discovery.Descriptor, + + /// These are the offsets into the descriptors array for + /// each style. For example, bold is from + /// offsets[@intFromEnum(.bold) - 1] to + /// offsets[@intFromEnum(.bold)]. + style_offsets: StyleOffsets = .{0} ** style_offsets_len, + + const style_offsets_len = std.enums.directEnumArrayLen(Style, 0); + const StyleOffsets = [style_offsets_len]usize; + + comptime { + // We assume this throughout this structure. If this changes + // we may need to change this structure. + assert(@intFromEnum(Style.regular) == 0); + assert(@intFromEnum(Style.bold) == 1); + assert(@intFromEnum(Style.italic) == 2); + assert(@intFromEnum(Style.bold_italic) == 3); + } + + pub fn init( + alloc_gpa: Allocator, + config: *const Config, + ) !Key { + var arena = ArenaAllocator.init(alloc_gpa); + errdefer arena.deinit(); + const alloc = arena.allocator(); + + var descriptors = std.ArrayList(discovery.Descriptor).init(alloc); + defer descriptors.deinit(); + for (config.@"font-family".list.items) |family| { + try descriptors.append(.{ + .family = family, + .style = config.@"font-style".nameValue(), + .size = config.@"font-size", + .variations = config.@"font-variation".list.items, + }); + } + + // In all the styled cases below, we prefer to specify an exact + // style via the `font-style` configuration. If a style is not + // specified, we use the discovery mechanism to search for a + // style category such as bold, italic, etc. We can't specify both + // because the latter will restrict the search to only that. If + // a user says `font-style = italic` for the bold face for example, + // no results would be found if we restrict to ALSO searching for + // italic. + for (config.@"font-family-bold".list.items) |family| { + const style = config.@"font-style-bold".nameValue(); + try descriptors.append(.{ + .family = family, + .style = style, + .size = config.@"font-size", + .bold = style == null, + .variations = config.@"font-variation".list.items, + }); + } + for (config.@"font-family-italic".list.items) |family| { + const style = config.@"font-style-italic".nameValue(); + try descriptors.append(.{ + .family = family, + .style = style, + .size = config.@"font-size", + .italic = style == null, + .variations = config.@"font-variation".list.items, + }); + } + for (config.@"font-family-bold-italic".list.items) |family| { + const style = config.@"font-style-bold-italic".nameValue(); + try descriptors.append(.{ + .family = family, + .style = style, + .size = config.@"font-size", + .bold = style == null, + .italic = style == null, + .variations = config.@"font-variation".list.items, + }); + } + + return .{ + .arena = arena, + .descriptors = try descriptors.toOwnedSlice(), + .style_offsets = .{ + config.@"font-family".list.items.len, + config.@"font-family-bold".list.items.len, + config.@"font-family-italic".list.items.len, + config.@"font-family-bold-italic".list.items.len, + }, + }; + } + + pub fn deinit(self: *Key) void { + self.arena.deinit(); + } + + /// Get the descriptors for the given font style that can be + /// used with discovery. + pub fn descriptorsForStyle( + self: Key, + style: Style, + ) []const discovery.Descriptor { + const idx = @intFromEnum(style); + const start: usize = if (idx == 0) 0 else self.style_offsets[idx - 1]; + const end = self.style_offsets[idx]; + return self.descriptors[start..end]; + } + + /// Hash the key with the given hasher. + pub fn hash(self: Key, hasher: anytype) void { + const autoHash = std.hash.autoHash; + autoHash(hasher, self.descriptors.len); + for (self.descriptors) |d| d.hash(hasher); + } + + /// Returns a hash code that can be used to uniquely identify this + /// action. + pub fn hashcode(self: Key) u64 { + var hasher = std.hash.Wyhash.init(0); + self.hash(&hasher); + return hasher.final(); + } +}; + +test "Key" { + const testing = std.testing; + const alloc = testing.allocator; + var cfg = try Config.default(alloc); + defer cfg.deinit(); + + var k = try Key.init(alloc, &cfg); + defer k.deinit(); +} diff --git a/src/font/main.zig b/src/font/main.zig index 383f2da74..feff26bfe 100644 --- a/src/font/main.zig +++ b/src/font/main.zig @@ -10,6 +10,7 @@ pub const DeferredFace = @import("DeferredFace.zig"); pub const Face = face.Face; pub const Group = @import("Group.zig"); pub const GroupCache = @import("GroupCache.zig"); +pub const GroupCacheSet = @import("GroupCacheSet.zig"); pub const Glyph = @import("Glyph.zig"); pub const shape = @import("shape.zig"); pub const Shaper = shape.Shaper; From 9d8da8fcc72f3894dc0fd5949d795e1246e941ef Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 1 Apr 2024 12:38:07 -0700 Subject: [PATCH 04/53] font: CodepointMap hashable, use for groupcacheset --- src/Surface.zig | 4 ++-- src/font/CodepointMap.zig | 20 ++++++++++++++++++++ src/font/GroupCacheSet.zig | 17 ++++++++++++++++- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index dac8ba705..36d4c00a4 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -354,8 +354,8 @@ pub fn init( }; // If we have codepoint mappings, set those. - if (config.@"font-codepoint-map".map.list.len > 0) { - group.codepoint_map = config.@"font-codepoint-map".map; + if (font_group_key.codepoint_map.list.len > 0) { + group.codepoint_map = font_group_key.codepoint_map; } // Set our styles diff --git a/src/font/CodepointMap.zig b/src/font/CodepointMap.zig index 58a8a7c43..c5b5b1ffb 100644 --- a/src/font/CodepointMap.zig +++ b/src/font/CodepointMap.zig @@ -53,6 +53,26 @@ pub fn get(self: *const CodepointMap, cp: u21) ?discovery.Descriptor { return null; } +/// Hash with the given hasher. +pub fn hash(self: *const CodepointMap, hasher: anytype) void { + const autoHash = std.hash.autoHash; + autoHash(hasher, self.list.len); + const slice = self.list.slice(); + for (0..slice.len) |i| { + const entry = slice.get(i); + autoHash(hasher, entry.range); + entry.descriptor.hash(hasher); + } +} + +/// Returns a hash code that can be used to uniquely identify this +/// action. +pub fn hashcode(self: *const CodepointMap) u64 { + var hasher = std.hash.Wyhash.init(0); + self.hash(&hasher); + return hasher.final(); +} + test "codepointmap" { const testing = std.testing; const alloc = testing.allocator; diff --git a/src/font/GroupCacheSet.zig b/src/font/GroupCacheSet.zig index c4786b216..c1d344947 100644 --- a/src/font/GroupCacheSet.zig +++ b/src/font/GroupCacheSet.zig @@ -17,7 +17,9 @@ const std = @import("std"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; -const Style = @import("main.zig").Style; +const fontpkg = @import("main.zig"); +const Style = fontpkg.Style; +const CodepointMap = fontpkg.CodepointMap; const discovery = @import("discovery.zig"); const configpkg = @import("../config.zig"); const Config = configpkg.Config; @@ -38,6 +40,9 @@ pub const Key = struct { /// offsets[@intFromEnum(.bold)]. style_offsets: StyleOffsets = .{0} ** style_offsets_len, + /// The codepoint map configuration. + codepoint_map: CodepointMap, + const style_offsets_len = std.enums.directEnumArrayLen(Style, 0); const StyleOffsets = [style_offsets_len]usize; @@ -109,6 +114,14 @@ pub const Key = struct { }); } + // Setup the codepoint map + const codepoint_map: CodepointMap = map: { + const map = config.@"font-codepoint-map"; + if (map.map.list.len == 0) break :map .{}; + const clone = try config.@"font-codepoint-map".clone(alloc); + break :map clone.map; + }; + return .{ .arena = arena, .descriptors = try descriptors.toOwnedSlice(), @@ -118,6 +131,7 @@ pub const Key = struct { config.@"font-family-italic".list.items.len, config.@"font-family-bold-italic".list.items.len, }, + .codepoint_map = codepoint_map, }; } @@ -142,6 +156,7 @@ pub const Key = struct { const autoHash = std.hash.autoHash; autoHash(hasher, self.descriptors.len); for (self.descriptors) |d| d.hash(hasher); + autoHash(hasher, self.codepoint_map); } /// Returns a hash code that can be used to uniquely identify this From 2a386daa19e6647914690059e44a7ebbdb378d39 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 1 Apr 2024 12:49:32 -0700 Subject: [PATCH 05/53] font: GroupCacheSet.Key has metric modifiers --- src/Surface.zig | 21 ++------------------- src/font/GroupCacheSet.zig | 30 ++++++++++++++++++++++++++++++ src/font/face/Metrics.zig | 15 +++++++++++++++ 3 files changed, 47 insertions(+), 19 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 36d4c00a4..2efc23fdf 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -338,25 +338,8 @@ pub fn init( font_group.* = try font.GroupCache.init(alloc, group: { var group = try font.Group.init(alloc, font_lib, font_size); errdefer group.deinit(); - - // Setup our font metric modifiers if we have any. - group.metric_modifiers = set: { - var set: font.face.Metrics.ModifierSet = .{}; - errdefer set.deinit(alloc); - if (config.@"adjust-cell-width") |m| try set.put(alloc, .cell_width, m); - if (config.@"adjust-cell-height") |m| try set.put(alloc, .cell_height, m); - if (config.@"adjust-font-baseline") |m| try set.put(alloc, .cell_baseline, m); - if (config.@"adjust-underline-position") |m| try set.put(alloc, .underline_position, m); - if (config.@"adjust-underline-thickness") |m| try set.put(alloc, .underline_thickness, m); - if (config.@"adjust-strikethrough-position") |m| try set.put(alloc, .strikethrough_position, m); - if (config.@"adjust-strikethrough-thickness") |m| try set.put(alloc, .strikethrough_thickness, m); - break :set set; - }; - - // If we have codepoint mappings, set those. - if (font_group_key.codepoint_map.list.len > 0) { - group.codepoint_map = font_group_key.codepoint_map; - } + group.metric_modifiers = font_group_key.metric_modifiers; + group.codepoint_map = font_group_key.codepoint_map; // Set our styles group.styles.set(.bold, config.@"font-style-bold" != .false); diff --git a/src/font/GroupCacheSet.zig b/src/font/GroupCacheSet.zig index c1d344947..867658627 100644 --- a/src/font/GroupCacheSet.zig +++ b/src/font/GroupCacheSet.zig @@ -19,6 +19,7 @@ const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; const fontpkg = @import("main.zig"); const Style = fontpkg.Style; +const Metrics = fontpkg.face.Metrics; const CodepointMap = fontpkg.CodepointMap; const discovery = @import("discovery.zig"); const configpkg = @import("../config.zig"); @@ -43,6 +44,9 @@ pub const Key = struct { /// The codepoint map configuration. codepoint_map: CodepointMap, + /// The metric modifier set configuration. + metric_modifiers: Metrics.ModifierSet, + const style_offsets_len = std.enums.directEnumArrayLen(Style, 0); const StyleOffsets = [style_offsets_len]usize; @@ -122,6 +126,19 @@ pub const Key = struct { break :map clone.map; }; + // Metric modifiers + const metric_modifiers: Metrics.ModifierSet = set: { + var set: Metrics.ModifierSet = .{}; + if (config.@"adjust-cell-width") |m| try set.put(alloc, .cell_width, m); + if (config.@"adjust-cell-height") |m| try set.put(alloc, .cell_height, m); + if (config.@"adjust-font-baseline") |m| try set.put(alloc, .cell_baseline, m); + if (config.@"adjust-underline-position") |m| try set.put(alloc, .underline_position, m); + if (config.@"adjust-underline-thickness") |m| try set.put(alloc, .underline_thickness, m); + if (config.@"adjust-strikethrough-position") |m| try set.put(alloc, .strikethrough_position, m); + if (config.@"adjust-strikethrough-thickness") |m| try set.put(alloc, .strikethrough_thickness, m); + break :set set; + }; + return .{ .arena = arena, .descriptors = try descriptors.toOwnedSlice(), @@ -132,6 +149,7 @@ pub const Key = struct { config.@"font-family-bold-italic".list.items.len, }, .codepoint_map = codepoint_map, + .metric_modifiers = metric_modifiers, }; } @@ -157,6 +175,16 @@ pub const Key = struct { autoHash(hasher, self.descriptors.len); for (self.descriptors) |d| d.hash(hasher); autoHash(hasher, self.codepoint_map); + autoHash(hasher, self.metric_modifiers.count()); + if (self.metric_modifiers.count() > 0) { + inline for (@typeInfo(Metrics.Key).Enum.fields) |field| { + const key = @field(Metrics.Key, field.name); + if (self.metric_modifiers.get(key)) |value| { + autoHash(hasher, key); + value.hash(hasher); + } + } + } } /// Returns a hash code that can be used to uniquely identify this @@ -176,4 +204,6 @@ test "Key" { var k = try Key.init(alloc, &cfg); defer k.deinit(); + + try testing.expect(k.hashcode() > 0); } diff --git a/src/font/face/Metrics.zig b/src/font/face/Metrics.zig index e8f318d48..621f0ddbf 100644 --- a/src/font/face/Metrics.zig +++ b/src/font/face/Metrics.zig @@ -169,6 +169,21 @@ pub const Modifier = union(enum) { }; } + /// Hash using the hasher. + pub fn hash(self: Modifier, hasher: anytype) void { + const autoHash = std.hash.autoHash; + autoHash(hasher, std.meta.activeTag(self)); + switch (self) { + // floats can't be hashed directly so we round it to the + // nearest int and then hash that. This is not perfect but + // hash collisions due to the modifier being wrong are really + // rare so we should fix this up later. + // TODO(fontmem): make better + .percent => |v| autoHash(hasher, @as(i64, @intFromFloat(v))), + .absolute => |v| autoHash(hasher, v), + } + } + test "formatConfig percent" { const configpkg = @import("../../config.zig"); const testing = std.testing; From 9f34edfa83ef0772321bb3376203577fc394f884 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 1 Apr 2024 15:26:16 -0700 Subject: [PATCH 06/53] font: GroupCacheSet can initialize a group --- src/font/GroupCacheSet.zig | 241 +++++++++++++++++++++++++++++++++++++ 1 file changed, 241 insertions(+) diff --git a/src/font/GroupCacheSet.zig b/src/font/GroupCacheSet.zig index 867658627..98a3f6e06 100644 --- a/src/font/GroupCacheSet.zig +++ b/src/font/GroupCacheSet.zig @@ -14,17 +14,245 @@ const GroupCacheSet = @This(); const std = @import("std"); +const builtin = @import("builtin"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; const fontpkg = @import("main.zig"); +const Discover = fontpkg.Discover; const Style = fontpkg.Style; +const Library = fontpkg.Library; const Metrics = fontpkg.face.Metrics; const CodepointMap = fontpkg.CodepointMap; +const DesiredSize = fontpkg.face.DesiredSize; +const Face = fontpkg.Face; +const Group = fontpkg.Group; +const GroupCache = fontpkg.GroupCache; const discovery = @import("discovery.zig"); const configpkg = @import("../config.zig"); const Config = configpkg.Config; +const log = std.log.scoped(.font_group_cache_set); + +/// The allocator to use for all heap allocations. +alloc: Allocator, + +/// The map of font configurations to GroupCache instances. +map: Map = .{}, + +/// The font library that is used for all font groups. +font_lib: Library, + +/// Font discovery mechanism. +font_discover: ?Discover = null, + +/// Initialize a new GroupCacheSet. +pub fn init(alloc: Allocator) !GroupCacheSet { + var font_lib = try Library.init(); + errdefer font_lib.deinit(); + + return .{ + .alloc = alloc, + .map = .{}, + .font_lib = font_lib, + }; +} + +pub fn deinit(self: *GroupCacheSet) void { + var it = self.map.iterator(); + while (it.next()) |entry| { + const ref = entry.value_ptr.*; + ref.cache.deinit(self.alloc); + self.alloc.destroy(ref.cache); + } + self.map.deinit(self.alloc); + + if (comptime Discover != void) { + if (self.font_discover) |*v| v.deinit(); + } + + self.font_lib.deinit(); +} + +/// Initialize a GroupCache for the given font configuration. If the +/// GroupCache is not present it will be initialized with a ref count of +/// 1. If it is present, the ref count will be incremented. +/// +/// This is NOT thread-safe. +pub fn groupInit( + self: *GroupCacheSet, + config: *const Config, + font_size: DesiredSize, +) !*GroupCache { + var key = try Key.init(self.alloc, config); + errdefer key.deinit(); + + const gop = try self.map.getOrPut(key); + if (gop.found_existing) { + // We can deinit the key because we found a cached value. + key.deinit(); + + // Increment our ref count and return the cache + gop.value_ptr.ref += 1; + return gop.value_ptr.cache; + } + errdefer self.map.removeByPtr(gop.key_ptr); + + // A new font config, initialize the cache. + const cache = try self.alloc.create(GroupCache); + errdefer self.alloc.destroy(cache); + gop.value_ptr.* = .{ + .cache = cache, + .ref = 1, + }; + + cache.* = try GroupCache.init(self.alloc, group: { + var group = try Group.init(self.alloc, self.font_lib, font_size); + errdefer group.deinit(); + group.metric_modifiers = key.metric_modifiers; + group.codepoint_map = key.codepoint_map; + + // Set our styles + group.styles.set(.bold, config.@"font-style-bold" != .false); + group.styles.set(.italic, config.@"font-style-italic" != .false); + group.styles.set(.bold_italic, config.@"font-style-bold-italic" != .false); + + // Search for fonts + if (Discover != void) discover: { + const disco = try self.discover() orelse { + log.warn("font discovery not available, cannot search for fonts", .{}); + break :discover; + }; + group.discover = disco; + + // A buffer we use to store the font names for logging. + var name_buf: [256]u8 = undefined; + + inline for (@typeInfo(Style).Enum.fields) |field| { + const style = @field(Style, field.name); + for (key.descriptorsForStyle(style)) |desc| { + var disco_it = try disco.discover(self.alloc, desc); + defer disco_it.deinit(); + if (try disco_it.next()) |face| { + log.info("font {s}: {s}", .{ + field.name, + try face.name(&name_buf), + }); + _ = try group.addFace(style, .{ .deferred = face }); + } else log.warn("font-family {s} not found: {s}", .{ + field.name, + desc.family.?, + }); + } + } + } + + // Our built-in font will be used as a backup + _ = try group.addFace( + .regular, + .{ .fallback_loaded = try Face.init( + self.font_lib, + face_ttf, + group.faceOptions(), + ) }, + ); + _ = try group.addFace( + .bold, + .{ .fallback_loaded = try Face.init( + self.font_lib, + face_bold_ttf, + group.faceOptions(), + ) }, + ); + + // Auto-italicize if we have to. + try group.italicize(); + + // On macOS, always search for and add the Apple Emoji font + // as our preferred emoji font for fallback. We do this in case + // people add other emoji fonts to their system, we always want to + // prefer the official one. Users can override this by explicitly + // specifying a font-family for emoji. + if (comptime builtin.target.isDarwin()) apple_emoji: { + const disco = group.discover orelse break :apple_emoji; + var disco_it = try disco.discover(self.alloc, .{ + .family = "Apple Color Emoji", + }); + defer disco_it.deinit(); + if (try disco_it.next()) |face| { + _ = try group.addFace(.regular, .{ .fallback_deferred = face }); + } + } + + // Emoji fallback. We don't include this on Mac since Mac is expected + // to always have the Apple Emoji available on the system. + if (comptime !builtin.target.isDarwin() or Discover == void) { + _ = try group.addFace( + .regular, + .{ .fallback_loaded = try Face.init( + self.font_lib, + face_emoji_ttf, + group.faceOptions(), + ) }, + ); + _ = try group.addFace( + .regular, + .{ .fallback_loaded = try Face.init( + self.font_lib, + face_emoji_text_ttf, + group.faceOptions(), + ) }, + ); + } + + break :group group; + }); + errdefer cache.deinit(self.alloc); + + return gop.value_ptr.cache; +} + +/// Map of font configurations to GroupCache instances. The GroupCache +/// instances are pointers that are heap allocated so that they're +/// stable pointers across hash map resizes. +pub const Map = std.HashMapUnmanaged( + Key, + RefGroupCache, + struct { + const KeyType = Key; + + pub fn hash(ctx: @This(), k: KeyType) u64 { + _ = ctx; + return k.hashcode(); + } + + pub fn eql(ctx: @This(), a: KeyType, b: KeyType) bool { + return ctx.hash(a) == ctx.hash(b); + } + }, + std.hash_map.default_max_load_percentage, +); + +/// Initialize once and return the font discovery mechanism. This remains +/// initialized throughout the lifetime of the application because some +/// font discovery mechanisms (i.e. fontconfig) are unsafe to reinit. +fn discover(self: *GroupCacheSet) !?*Discover { + // If we're built without a font discovery mechanism, return null + if (comptime Discover == void) return null; + + // If we initialized, use it + if (self.font_discover) |*v| return v; + + self.font_discover = Discover.init(); + return &self.font_discover.?; +} + +/// Ref-counted GroupCache. +const RefGroupCache = struct { + cache: *GroupCache, + ref: u32 = 0, +}; + /// The key used to uniquely identify a font configuration. pub const Key = struct { arena: ArenaAllocator, @@ -196,6 +424,11 @@ pub const Key = struct { } }; +const face_ttf = @embedFile("res/JetBrainsMono-Regular.ttf"); +const face_bold_ttf = @embedFile("res/JetBrainsMono-Bold.ttf"); +const face_emoji_ttf = @embedFile("res/NotoColorEmoji.ttf"); +const face_emoji_text_ttf = @embedFile("res/NotoEmoji-Regular.ttf"); + test "Key" { const testing = std.testing; const alloc = testing.allocator; @@ -207,3 +440,11 @@ test "Key" { try testing.expect(k.hashcode() > 0); } + +test "basics" { + const testing = std.testing; + const alloc = testing.allocator; + + var set = try GroupCacheSet.init(alloc); + defer set.deinit(); +} From 6d7053a1ad32745a94d3589bfc5f49e8c157e35c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 1 Apr 2024 15:37:09 -0700 Subject: [PATCH 07/53] core: convert surface/app to use GroupCacheSet --- src/App.zig | 31 +++------- src/Surface.zig | 122 +------------------------------------ src/font/GroupCacheSet.zig | 3 +- 3 files changed, 15 insertions(+), 141 deletions(-) diff --git a/src/App.zig b/src/App.zig index 99949d9e1..53ca77c6d 100644 --- a/src/App.zig +++ b/src/App.zig @@ -41,10 +41,9 @@ mailbox: Mailbox.Queue, /// Set to true once we're quitting. This never goes false again. quit: bool, -/// Font discovery mechanism. This is only safe to use from the main thread. -/// This is lazily initialized on the first call to fontDiscover so do -/// not access this directly. -font_discover: ?font.Discover = null, +/// The set of font GroupCache instances shared by surfaces with the +/// same font configuration. +font_group_set: font.GroupCacheSet, /// Initialize the main app instance. This creates the main window, sets /// up the renderer state, compiles the shaders, etc. This is the primary @@ -55,11 +54,15 @@ pub fn create( var app = try alloc.create(App); errdefer alloc.destroy(app); + var font_group_set = try font.GroupCacheSet.init(alloc); + errdefer font_group_set.deinit(); + app.* = .{ .alloc = alloc, .surfaces = .{}, .mailbox = .{}, .quit = false, + .font_group_set = font_group_set, }; errdefer app.surfaces.deinit(alloc); @@ -71,9 +74,9 @@ pub fn destroy(self: *App) void { for (self.surfaces.items) |surface| surface.deinit(); self.surfaces.deinit(self.alloc); - if (comptime font.Discover != void) { - if (self.font_discover) |*v| v.deinit(); - } + // Clean up our font group cache + // TODO(fontmem): assert all ref counts are zero + self.font_group_set.deinit(); self.alloc.destroy(self); } @@ -166,20 +169,6 @@ pub fn needsConfirmQuit(self: *const App) bool { return false; } -/// Initialize once and return the font discovery mechanism. This remains -/// initialized throughout the lifetime of the application because some -/// font discovery mechanisms (i.e. fontconfig) are unsafe to reinit. -pub fn fontDiscover(self: *App) !?*font.Discover { - // If we're built without a font discovery mechanism, return null - if (comptime font.Discover == void) return null; - - // If we initialized, use it - if (self.font_discover) |*v| return v; - - self.font_discover = font.Discover.init(); - return &self.font_discover.?; -} - /// Drain the mailbox. fn drainMailbox(self: *App, rt_app: *apprt.App) !void { while (self.mailbox.pop()) |message| { diff --git a/src/Surface.zig b/src/Surface.zig index 2efc23fdf..c8cf55ebc 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -54,8 +54,6 @@ rt_app: *apprt.runtime.App, rt_surface: *apprt.runtime.Surface, /// The font structures -font_lib: font.Library, -font_group: *font.GroupCache, font_size: font.face.DesiredSize, /// The renderer for this surface. @@ -320,117 +318,9 @@ pub fn init( .ydpi = @intFromFloat(y_dpi), }; - // Create our font group key. This is used to determine if we have - // a cached font group we can use already. Otherwise, this can be - // used to build the group. - var font_group_key = try font.GroupCacheSet.Key.init(alloc, config); - defer font_group_key.deinit(); - - // Find all the fonts for this surface - // - // Future: we can share the font group amongst all surfaces to save - // some new surface init time and some memory. This will require making - // thread-safe changes to font structs. - var font_lib = try font.Library.init(); - errdefer font_lib.deinit(); - var font_group = try alloc.create(font.GroupCache); - errdefer alloc.destroy(font_group); - font_group.* = try font.GroupCache.init(alloc, group: { - var group = try font.Group.init(alloc, font_lib, font_size); - errdefer group.deinit(); - group.metric_modifiers = font_group_key.metric_modifiers; - group.codepoint_map = font_group_key.codepoint_map; - - // Set our styles - group.styles.set(.bold, config.@"font-style-bold" != .false); - group.styles.set(.italic, config.@"font-style-italic" != .false); - group.styles.set(.bold_italic, config.@"font-style-bold-italic" != .false); - - // Search for fonts - if (font.Discover != void) discover: { - const disco = try app.fontDiscover() orelse { - log.warn("font discovery not available, cannot search for fonts", .{}); - break :discover; - }; - group.discover = disco; - - // A buffer we use to store the font names for logging. - var name_buf: [256]u8 = undefined; - - inline for (@typeInfo(font.Style).Enum.fields) |field| { - const style = @field(font.Style, field.name); - for (font_group_key.descriptorsForStyle(style)) |desc| { - var disco_it = try disco.discover(alloc, desc); - defer disco_it.deinit(); - if (try disco_it.next()) |face| { - log.info("font {s}: {s}", .{ - field.name, - try face.name(&name_buf), - }); - _ = try group.addFace(style, .{ .deferred = face }); - } else log.warn("font-family {s} not found: {s}", .{ - field.name, - desc.family.?, - }); - } - } - } - - // Our built-in font will be used as a backup - _ = try group.addFace( - .regular, - .{ .fallback_loaded = try font.Face.init( - font_lib, - face_ttf, - group.faceOptions(), - ) }, - ); - _ = try group.addFace( - .bold, - .{ .fallback_loaded = try font.Face.init( - font_lib, - face_bold_ttf, - group.faceOptions(), - ) }, - ); - - // Auto-italicize if we have to. - try group.italicize(); - - // On macOS, always search for and add the Apple Emoji font - // as our preferred emoji font for fallback. We do this in case - // people add other emoji fonts to their system, we always want to - // prefer the official one. Users can override this by explicitly - // specifying a font-family for emoji. - if (comptime builtin.target.isDarwin()) apple_emoji: { - const disco = group.discover orelse break :apple_emoji; - var disco_it = try disco.discover(alloc, .{ - .family = "Apple Color Emoji", - }); - defer disco_it.deinit(); - if (try disco_it.next()) |face| { - _ = try group.addFace(.regular, .{ .fallback_deferred = face }); - } - } - - // Emoji fallback. We don't include this on Mac since Mac is expected - // to always have the Apple Emoji available on the system. - if (comptime !builtin.target.isDarwin() or font.Discover == void) { - _ = try group.addFace( - .regular, - .{ .fallback_loaded = try font.Face.init(font_lib, face_emoji_ttf, group.faceOptions()) }, - ); - _ = try group.addFace( - .regular, - .{ .fallback_loaded = try font.Face.init(font_lib, face_emoji_text_ttf, group.faceOptions()) }, - ); - } - - break :group group; - }); - errdefer font_group.deinit(alloc); - - log.info("font loading complete, any non-logged faces are using the built-in font", .{}); + // Setup our font group. This will reuse an existing font group if + // it was already loaded. + const font_group = try app.font_group_set.groupInit(config, font_size); // Pre-calculate our initial cell size ourselves. const cell_size = try renderer.CellSize.init(alloc, font_group); @@ -516,8 +406,6 @@ pub fn init( .app = app, .rt_app = rt_app, .rt_surface = rt_surface, - .font_lib = font_lib, - .font_group = font_group, .font_size = font_size, .renderer = renderer_impl, .renderer_thread = render_thread, @@ -632,10 +520,6 @@ pub fn deinit(self: *Surface) void { self.io_thread.deinit(); self.io.deinit(); - self.font_group.deinit(self.alloc); - self.font_lib.deinit(); - self.alloc.destroy(self.font_group); - if (self.inspector) |v| { v.deinit(); self.alloc.destroy(v); diff --git a/src/font/GroupCacheSet.zig b/src/font/GroupCacheSet.zig index 98a3f6e06..bab0eae3f 100644 --- a/src/font/GroupCacheSet.zig +++ b/src/font/GroupCacheSet.zig @@ -87,7 +87,7 @@ pub fn groupInit( var key = try Key.init(self.alloc, config); errdefer key.deinit(); - const gop = try self.map.getOrPut(key); + const gop = try self.map.getOrPut(self.alloc, key); if (gop.found_existing) { // We can deinit the key because we found a cached value. key.deinit(); @@ -205,6 +205,7 @@ pub fn groupInit( ); } + log.info("font loading complete, any non-logged faces are using the built-in font", .{}); break :group group; }); errdefer cache.deinit(self.alloc); From 5de88fe3f8faba3542e70ace993ce28263f984a7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 1 Apr 2024 15:43:43 -0700 Subject: [PATCH 08/53] core: deref the font group when not used --- src/Surface.zig | 10 +++++++++- src/font/GroupCacheSet.zig | 28 ++++++++++++++++++++++++---- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index c8cf55ebc..00258e112 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -54,6 +54,7 @@ rt_app: *apprt.runtime.App, rt_surface: *apprt.runtime.Surface, /// The font structures +font_group_key: font.GroupCacheSet.Key, font_size: font.face.DesiredSize, /// The renderer for this surface. @@ -320,7 +321,10 @@ pub fn init( // Setup our font group. This will reuse an existing font group if // it was already loaded. - const font_group = try app.font_group_set.groupInit(config, font_size); + const font_group_key, const font_group = try app.font_group_set.groupRef( + config, + font_size, + ); // Pre-calculate our initial cell size ourselves. const cell_size = try renderer.CellSize.init(alloc, font_group); @@ -406,6 +410,7 @@ pub fn init( .app = app, .rt_app = rt_app, .rt_surface = rt_surface, + .font_group_key = font_group_key, .font_size = font_size, .renderer = renderer_impl, .renderer_thread = render_thread, @@ -525,6 +530,9 @@ pub fn deinit(self: *Surface) void { self.alloc.destroy(v); } + // Clean up our font group + self.app.font_group_set.groupDeref(self.font_group_key); + // Clean up our render state if (self.renderer_state.preedit) |p| self.alloc.free(p.codepoints); self.alloc.destroy(self.renderer_state.mutex); diff --git a/src/font/GroupCacheSet.zig b/src/font/GroupCacheSet.zig index bab0eae3f..4ab97ce02 100644 --- a/src/font/GroupCacheSet.zig +++ b/src/font/GroupCacheSet.zig @@ -61,6 +61,7 @@ pub fn init(alloc: Allocator) !GroupCacheSet { pub fn deinit(self: *GroupCacheSet) void { var it = self.map.iterator(); while (it.next()) |entry| { + entry.key_ptr.deinit(); const ref = entry.value_ptr.*; ref.cache.deinit(self.alloc); self.alloc.destroy(ref.cache); @@ -79,11 +80,11 @@ pub fn deinit(self: *GroupCacheSet) void { /// 1. If it is present, the ref count will be incremented. /// /// This is NOT thread-safe. -pub fn groupInit( +pub fn groupRef( self: *GroupCacheSet, config: *const Config, font_size: DesiredSize, -) !*GroupCache { +) !struct { Key, *GroupCache } { var key = try Key.init(self.alloc, config); errdefer key.deinit(); @@ -94,7 +95,7 @@ pub fn groupInit( // Increment our ref count and return the cache gop.value_ptr.ref += 1; - return gop.value_ptr.cache; + return .{ gop.key_ptr.*, gop.value_ptr.cache }; } errdefer self.map.removeByPtr(gop.key_ptr); @@ -210,7 +211,26 @@ pub fn groupInit( }); errdefer cache.deinit(self.alloc); - return gop.value_ptr.cache; + return .{ gop.key_ptr.*, gop.value_ptr.cache }; +} + +/// Decrement the ref count for the given key. If the ref count is zero, +/// the GroupCache will be deinitialized and removed from the map.j:w +pub fn groupDeref(self: *GroupCacheSet, key: Key) void { + const entry = self.map.getEntry(key) orelse return; + assert(entry.value_ptr.ref >= 1); + + // If we have more than one reference, decrement and return. + if (entry.value_ptr.ref > 1) { + entry.value_ptr.ref -= 1; + return; + } + + // We are at a zero ref count so deinit the group and remove. + entry.key_ptr.deinit(); + entry.value_ptr.cache.deinit(self.alloc); + self.alloc.destroy(entry.value_ptr.cache); + self.map.removeByPtr(entry.key_ptr); } /// Map of font configurations to GroupCache instances. The GroupCache From 7b428367dfde41cf9b72ad02bc5dcb96a96bdd2d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 1 Apr 2024 15:44:42 -0700 Subject: [PATCH 09/53] font: improve log statements a bit --- src/font/GroupCacheSet.zig | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/font/GroupCacheSet.zig b/src/font/GroupCacheSet.zig index 4ab97ce02..51719306c 100644 --- a/src/font/GroupCacheSet.zig +++ b/src/font/GroupCacheSet.zig @@ -90,6 +90,8 @@ pub fn groupRef( const gop = try self.map.getOrPut(self.alloc, key); if (gop.found_existing) { + log.debug("found cached GroupCache for font config", .{}); + // We can deinit the key because we found a cached value. key.deinit(); @@ -99,6 +101,8 @@ pub fn groupRef( } errdefer self.map.removeByPtr(gop.key_ptr); + log.debug("initializing new GroupCache for font config", .{}); + // A new font config, initialize the cache. const cache = try self.alloc.create(GroupCache); errdefer self.alloc.destroy(cache); From 72d59956d5a0526d56394acb38203a6d74deffc8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 2 Apr 2024 10:24:56 -0700 Subject: [PATCH 10/53] font: [broken] working on extracting Collection from Group --- src/font/Collection.zig | 264 +++++++++++++++++++++++++++++++++++++ src/font/Group.zig | 134 +++---------------- src/font/GroupCacheSet.zig | 215 ++++++++++++++++++------------ src/font/main.zig | 1 + 4 files changed, 409 insertions(+), 205 deletions(-) create mode 100644 src/font/Collection.zig diff --git a/src/font/Collection.zig b/src/font/Collection.zig new file mode 100644 index 000000000..09e6c6c09 --- /dev/null +++ b/src/font/Collection.zig @@ -0,0 +1,264 @@ +//! A font collection is a list of faces of different styles. The list is +//! ordered by priority (per style). All fonts in a collection share the same +//! size so they can be used interchangeably in cases a glyph is missing in one +//! and present in another. +const Collection = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const font = @import("main.zig"); +const DeferredFace = font.DeferredFace; +const Face = font.Face; +const Library = font.Library; +const Presentation = font.Presentation; +const Style = font.Style; + +/// The available faces we have. This shouldn't be modified manually. +/// Instead, use the functions available on Collection. +faces: StyleArray, + +/// Initialize an empty collection. +pub fn init(alloc: Allocator) !Collection { + // Initialize our styles array, preallocating some space that is + // likely to be used. + var faces = StyleArray.initFill(.{}); + for (&faces.values) |*list| try list.ensureTotalCapacityPrecise(alloc, 2); + return .{ .faces = faces }; +} + +pub fn deinit(self: *Collection, alloc: Allocator) void { + var it = self.faces.iterator(); + while (it.next()) |entry| { + for (entry.value.items) |*item| item.deinit(); + entry.value.deinit(alloc); + } +} + +pub const AddError = Allocator.Error || error{ + CollectionFull, +}; + +/// Add a face to the collection for the given style. This face will be added +/// next in priority if others exist already, i.e. it'll be the _last_ to be +/// searched for a glyph in that list. +/// +/// The collection takes ownership of the face. The face will be deallocated +/// when the collection is deallocated. +/// +/// If a loaded face is added to the collection, it should be the same +/// size as all the other faces in the collection. This function will not +/// verify or modify the size until the size of the entire collection is +/// changed. +pub fn add( + self: *Collection, + alloc: Allocator, + style: Style, + face: Entry, +) AddError!Index { + const list = self.faces.getPtr(style); + + // We have some special indexes so we must never pass those. + if (list.items.len >= Index.Special.start - 1) + return error.CollectionFull; + + const idx = list.items.len; + try list.append(alloc, face); + return .{ .style = style, .idx = @intCast(idx) }; +} + +/// Packed array of all Style enum cases mapped to a growable list of faces. +/// +/// We use this data structure because there aren't many styles and all +/// styles are typically loaded for a terminal session. The overhead per +/// style even if it is not used or barely used is minimal given the +/// small style count. +const StyleArray = std.EnumArray(Style, std.ArrayListUnmanaged(Entry)); + +/// A entry in a collection can be deferred or loaded. A deferred face +/// is not yet fully loaded and only represents the font descriptor +/// and usually uses less resources. A loaded face is fully parsed, +/// ready to rasterize, and usually uses more resources than a +/// deferred version. +/// +/// A face can also be a "fallback" variant that is still either +/// deferred or loaded. Today, there is only one difference between +/// fallback and non-fallback (or "explicit") faces: the handling +/// of emoji presentation. +/// +/// For explicit faces, when an explicit emoji presentation is +/// not requested, we will use any glyph for that codepoint found +/// even if the font presentation does not match the UCD +/// (Unicode Character Database) value. When an explicit presentation +/// is requested (via either VS15/V16), that is always honored. +/// The reason we do this is because we assume that if a user +/// explicitly chosen a font face (hence it is "explicit" and +/// not "fallback"), they want to use any glyphs possible within that +/// font face. Fallback fonts on the other hand are picked as a +/// last resort, so we should prefer exactness if possible. +pub const Entry = union(enum) { + deferred: DeferredFace, // Not loaded + loaded: Face, // Loaded, explicit use + + // The same as deferred/loaded but fallback font semantics (see large + // comment above Entry). + fallback_deferred: DeferredFace, + fallback_loaded: Face, + + pub fn deinit(self: *Entry) void { + switch (self.*) { + inline .deferred, + .loaded, + .fallback_deferred, + .fallback_loaded, + => |*v| v.deinit(), + } + } + + /// True if this face satisfies the given codepoint and presentation. + fn hasCodepoint(self: Entry, cp: u32, p_mode: PresentationMode) bool { + return switch (self) { + // Non-fallback fonts require explicit presentation matching but + // otherwise don't care about presentation + .deferred => |v| switch (p_mode) { + .explicit => |p| v.hasCodepoint(cp, p), + .default, .any => v.hasCodepoint(cp, null), + }, + + .loaded => |face| switch (p_mode) { + .explicit => |p| face.presentation == p and face.glyphIndex(cp) != null, + .default, .any => face.glyphIndex(cp) != null, + }, + + // Fallback fonts require exact presentation matching. + .fallback_deferred => |v| switch (p_mode) { + .explicit, .default => |p| v.hasCodepoint(cp, p), + .any => v.hasCodepoint(cp, null), + }, + + .fallback_loaded => |face| switch (p_mode) { + .explicit, + .default, + => |p| face.presentation == p and face.glyphIndex(cp) != null, + .any => face.glyphIndex(cp) != null, + }, + }; + } +}; + +/// The requested presentation for a codepoint. +pub const PresentationMode = union(enum) { + /// The codepoint has an explicit presentation that is required, + /// i.e. VS15/V16. + explicit: Presentation, + + /// The codepoint has no explicit presentation and we should use + /// the presentation from the UCd. + default: Presentation, + + /// The codepoint can be any presentation. + any: void, +}; + +/// This represents a specific font in the collection. +/// +/// The backing size of this packed struct represents the total number +/// of possible usable fonts in a collection. And the number of bits +/// used for the index and not the style represents the total number +/// of possible usable fonts for a given style. +/// +/// The goal is to keep the size of this struct as small as practical. We +/// accept the limitations that this imposes so long as they're reasonable. +/// At the time of writing this comment, this is a 16-bit struct with 13 +/// bits used for the index, supporting up to 8192 fonts per style. This +/// seems more than reasonable. There are synthetic scenarios where this +/// could be a limitation but I can't think of any that are practical. +/// +/// If you somehow need more fonts per style, you can increase the size of +/// the Backing type and everything should just work fine. +pub const Index = packed struct(Index.Backing) { + const Backing = u16; + const backing_bits = @typeInfo(Backing).Int.bits; + + /// The number of bits we use for the index. + const idx_bits = backing_bits - @typeInfo(@typeInfo(Style).Enum.tag_type).Int.bits; + pub const IndexInt = @Type(.{ .Int = .{ .signedness = .unsigned, .bits = idx_bits } }); + + /// The special-case fonts that we support. + pub const Special = enum(IndexInt) { + // We start all special fonts at this index so they can be detected. + pub const start = std.math.maxInt(IndexInt); + + /// Sprite drawing, this is rendered JIT using 2D graphics APIs. + sprite = start, + }; + + style: Style = .regular, + idx: IndexInt = 0, + + /// Initialize a special font index. + pub fn initSpecial(v: Special) Index { + return .{ .style = .regular, .idx = @intFromEnum(v) }; + } + + /// Convert to int + pub fn int(self: Index) Backing { + return @bitCast(self); + } + + /// Returns true if this is a "special" index which doesn't map to + /// a real font face. We can still render it but there is no face for + /// this font. + pub fn special(self: Index) ?Special { + if (self.idx < Special.start) return null; + return @enumFromInt(self.idx); + } + + test { + // We never want to take up more than a byte since font indexes are + // everywhere so if we increase the size of this we'll dramatically + // increase our memory usage. + try std.testing.expectEqual(@sizeOf(Backing), @sizeOf(Index)); + + // Just so we're aware when this changes. The current maximum number + // of fonts for a style is 13 bits or 8192 fonts. + try std.testing.expectEqual(13, idx_bits); + } +}; + +test init { + const testing = std.testing; + const alloc = testing.allocator; + + var c = try init(alloc); + defer c.deinit(alloc); +} + +test "add full" { + const testing = std.testing; + const alloc = testing.allocator; + const testFont = @import("test.zig").fontRegular; + + var lib = try Library.init(); + defer lib.deinit(); + + var c = try init(alloc); + defer c.deinit(alloc); + + for (0..Index.Special.start - 1) |_| { + _ = try c.add(alloc, .regular, .{ .loaded = try Face.init( + lib, + testFont, + .{ .size = .{ .points = 12 } }, + ) }); + } + + try testing.expectError(error.CollectionFull, c.add( + alloc, + .regular, + .{ .loaded = try Face.init( + lib, + testFont, + .{ .size = .{ .points = 12 } }, + ) }, + )); +} diff --git a/src/font/Group.zig b/src/font/Group.zig index 76c69d0f8..c6ccd01ed 100644 --- a/src/font/Group.zig +++ b/src/font/Group.zig @@ -16,6 +16,7 @@ const Allocator = std.mem.Allocator; const ziglyph = @import("ziglyph"); const font = @import("main.zig"); +const Collection = @import("main.zig").Collection; const DeferredFace = @import("main.zig").DeferredFace; const Face = @import("main.zig").Face; const Library = @import("main.zig").Library; @@ -27,13 +28,6 @@ const quirks = @import("../quirks.zig"); const log = std.log.scoped(.font_group); -/// Packed array to map our styles to a set of faces. -// Note: this is not the most efficient way to store these, but there is -// usually only one font group for the entire process so this isn't the -// most important memory efficiency we can look for. This is totally opaque -// to the user so we can change this later. -const StyleArray = std.EnumArray(Style, std.ArrayListUnmanaged(GroupFace)); - /// Packed array of booleans to indicate if a style is enabled or not. pub const StyleStatus = std.EnumArray(Style, bool); @@ -92,7 +86,7 @@ metric_modifiers: ?font.face.Metrics.ModifierSet = null, /// The available faces we have. This shouldn't be modified manually. /// Instead, use the functions available on Group. -faces: StyleArray, +faces: Collection, /// The set of statuses and whether they're enabled or not. This defaults /// to true. This can be changed at runtime with no ill effect. If you @@ -119,105 +113,25 @@ descriptor_cache: DescriptorCache = .{}, /// terminal rendering will look wrong. sprite: ?font.sprite.Face = null, -/// A face in a group can be deferred or loaded. A deferred face -/// is not yet fully loaded and only represents the font descriptor -/// and usually uses less resources. A loaded face is fully parsed, -/// ready to rasterize, and usually uses more resources than a -/// deferred version. -/// -/// A face can also be a "fallback" variant that is still either -/// deferred or loaded. Today, there is only one different between -/// fallback and non-fallback (or "explicit") faces: the handling -/// of emoji presentation. -/// -/// For explicit faces, when an explicit emoji presentation is -/// not requested, we will use any glyph for that codepoint found -/// even if the font presentation does not match the UCD -/// (Unicode Character Database) value. When an explicit presentation -/// is requested (via either VS15/V16), that is always honored. -/// The reason we do this is because we assume that if a user -/// explicitly chosen a font face (hence it is "explicit" and -/// not "fallback"), they want to use any glyphs possible within that -/// font face. Fallback fonts on the other hand are picked as a -/// last resort, so we should prefer exactness if possible. -pub const GroupFace = union(enum) { - deferred: DeferredFace, // Not loaded - loaded: Face, // Loaded, explicit use - - // The same as deferred/loaded but fallback font semantics (see large - // comment above GroupFace). - fallback_deferred: DeferredFace, - fallback_loaded: Face, - - pub fn deinit(self: *GroupFace) void { - switch (self.*) { - inline .deferred, - .loaded, - .fallback_deferred, - .fallback_loaded, - => |*v| v.deinit(), - } - } - - /// True if this face satisfies the given codepoint and presentation. - fn hasCodepoint(self: GroupFace, cp: u32, p_mode: PresentationMode) bool { - return switch (self) { - // Non-fallback fonts require explicit presentation matching but - // otherwise don't care about presentation - .deferred => |v| switch (p_mode) { - .explicit => |p| v.hasCodepoint(cp, p), - .default, .any => v.hasCodepoint(cp, null), - }, - - .loaded => |face| switch (p_mode) { - .explicit => |p| face.presentation == p and face.glyphIndex(cp) != null, - .default, .any => face.glyphIndex(cp) != null, - }, - - // Fallback fonts require exact presentation matching. - .fallback_deferred => |v| switch (p_mode) { - .explicit, .default => |p| v.hasCodepoint(cp, p), - .any => v.hasCodepoint(cp, null), - }, - - .fallback_loaded => |face| switch (p_mode) { - .explicit, - .default, - => |p| face.presentation == p and face.glyphIndex(cp) != null, - .any => face.glyphIndex(cp) != null, - }, - }; - } -}; - +/// Initializes an empty group. This is not useful until faces are added +/// and finalizeInit is called. pub fn init( alloc: Allocator, lib: Library, size: font.face.DesiredSize, + collection: Collection, ) !Group { - var result = Group{ .alloc = alloc, .lib = lib, .size = size, .faces = undefined }; - - // Initialize all our styles to initially sized lists. - var i: usize = 0; - while (i < StyleArray.len) : (i += 1) { - result.faces.values[i] = .{}; - try result.faces.values[i].ensureTotalCapacityPrecise(alloc, 2); - } - - return result; + return .{ + .alloc = alloc, + .lib = lib, + .size = size, + .faces = collection, + }; } pub fn deinit(self: *Group) void { - { - var it = self.faces.iterator(); - while (it.next()) |entry| { - for (entry.value.items) |*item| item.deinit(); - entry.value.deinit(self.alloc); - } - } - + self.faces.deinit(self.alloc); if (self.metric_modifiers) |*v| v.deinit(self.alloc); - self.descriptor_cache.deinit(self.alloc); } @@ -230,23 +144,6 @@ pub fn faceOptions(self: *const Group) font.face.Options { }; } -/// Add a face to the list for the given style. This face will be added as -/// next in priority if others exist already, i.e. it'll be the _last_ to be -/// searched for a glyph in that list. -/// -/// The group takes ownership of the face. The face will be deallocated when -/// the group is deallocated. -pub fn addFace(self: *Group, style: Style, face: GroupFace) !FontIndex { - const list = self.faces.getPtr(style); - - // We have some special indexes so we must never pass those. - if (list.items.len >= FontIndex.Special.start - 1) return error.GroupFull; - - const idx = list.items.len; - try list.append(self.alloc, face); - return .{ .style = style, .idx = @intCast(idx) }; -} - /// This will automatically create an italicized font from the regular /// font face if we don't have any italicized fonts. pub fn italicize(self: *Group) !void { @@ -472,7 +369,9 @@ pub fn indexForCodepoint( // Discovery is supposed to only return faces that have our // codepoint but we can't search presentation in discovery so // we have to check it here. - const face: GroupFace = .{ .fallback_deferred = deferred_face }; + const face: Collection.Entry = .{ + .fallback_deferred = deferred_face, + }; if (!face.hasCodepoint(cp, p_mode)) { deferred_face.deinit(); continue; @@ -895,9 +794,6 @@ test "face count limit" { const alloc = testing.allocator; const testFont = @import("test.zig").fontRegular; - var atlas_greyscale = try font.Atlas.init(alloc, 512, .greyscale); - defer atlas_greyscale.deinit(alloc); - var lib = try Library.init(); defer lib.deinit(); diff --git a/src/font/GroupCacheSet.zig b/src/font/GroupCacheSet.zig index 51719306c..818e13e91 100644 --- a/src/font/GroupCacheSet.zig +++ b/src/font/GroupCacheSet.zig @@ -19,6 +19,7 @@ const assert = std.debug.assert; const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; const fontpkg = @import("main.zig"); +const Collection = fontpkg.Collection; const Discover = fontpkg.Discover; const Style = fontpkg.Style; const Library = fontpkg.Library; @@ -111,105 +112,29 @@ pub fn groupRef( .ref = 1, }; + // Build our font face collection that we'll use to initialize + // the Group. + cache.* = try GroupCache.init(self.alloc, group: { - var group = try Group.init(self.alloc, self.font_lib, font_size); + var group = try Group.init( + self.alloc, + self.font_lib, + font_size, + try self.collection(&key, font_size), + ); errdefer group.deinit(); group.metric_modifiers = key.metric_modifiers; group.codepoint_map = key.codepoint_map; + group.discover = try self.discover(); // Set our styles group.styles.set(.bold, config.@"font-style-bold" != .false); group.styles.set(.italic, config.@"font-style-italic" != .false); group.styles.set(.bold_italic, config.@"font-style-bold-italic" != .false); - // Search for fonts - if (Discover != void) discover: { - const disco = try self.discover() orelse { - log.warn("font discovery not available, cannot search for fonts", .{}); - break :discover; - }; - group.discover = disco; - - // A buffer we use to store the font names for logging. - var name_buf: [256]u8 = undefined; - - inline for (@typeInfo(Style).Enum.fields) |field| { - const style = @field(Style, field.name); - for (key.descriptorsForStyle(style)) |desc| { - var disco_it = try disco.discover(self.alloc, desc); - defer disco_it.deinit(); - if (try disco_it.next()) |face| { - log.info("font {s}: {s}", .{ - field.name, - try face.name(&name_buf), - }); - _ = try group.addFace(style, .{ .deferred = face }); - } else log.warn("font-family {s} not found: {s}", .{ - field.name, - desc.family.?, - }); - } - } - } - - // Our built-in font will be used as a backup - _ = try group.addFace( - .regular, - .{ .fallback_loaded = try Face.init( - self.font_lib, - face_ttf, - group.faceOptions(), - ) }, - ); - _ = try group.addFace( - .bold, - .{ .fallback_loaded = try Face.init( - self.font_lib, - face_bold_ttf, - group.faceOptions(), - ) }, - ); - // Auto-italicize if we have to. try group.italicize(); - // On macOS, always search for and add the Apple Emoji font - // as our preferred emoji font for fallback. We do this in case - // people add other emoji fonts to their system, we always want to - // prefer the official one. Users can override this by explicitly - // specifying a font-family for emoji. - if (comptime builtin.target.isDarwin()) apple_emoji: { - const disco = group.discover orelse break :apple_emoji; - var disco_it = try disco.discover(self.alloc, .{ - .family = "Apple Color Emoji", - }); - defer disco_it.deinit(); - if (try disco_it.next()) |face| { - _ = try group.addFace(.regular, .{ .fallback_deferred = face }); - } - } - - // Emoji fallback. We don't include this on Mac since Mac is expected - // to always have the Apple Emoji available on the system. - if (comptime !builtin.target.isDarwin() or Discover == void) { - _ = try group.addFace( - .regular, - .{ .fallback_loaded = try Face.init( - self.font_lib, - face_emoji_ttf, - group.faceOptions(), - ) }, - ); - _ = try group.addFace( - .regular, - .{ .fallback_loaded = try Face.init( - self.font_lib, - face_emoji_text_ttf, - group.faceOptions(), - ) }, - ); - } - log.info("font loading complete, any non-logged faces are using the built-in font", .{}); break :group group; }); @@ -218,6 +143,124 @@ pub fn groupRef( return .{ gop.key_ptr.*, gop.value_ptr.cache }; } +/// Builds the Collection for the given configuration key and +/// initial font size. +fn collection( + self: *GroupCacheSet, + key: *const Key, + size: DesiredSize, +) !Collection { + var c = try Collection.init(self.alloc); + errdefer c.deinit(self.alloc); + + const opts: fontpkg.face.Options = .{ + .size = size, + .metric_modifiers = &key.metric_modifiers, + }; + + // Search for fonts + if (Discover != void) discover: { + const disco = try self.discover() orelse { + log.warn( + "font discovery not available, cannot search for fonts", + .{}, + ); + break :discover; + }; + + // A buffer we use to store the font names for logging. + var name_buf: [256]u8 = undefined; + + inline for (@typeInfo(Style).Enum.fields) |field| { + const style = @field(Style, field.name); + for (key.descriptorsForStyle(style)) |desc| { + var disco_it = try disco.discover(self.alloc, desc); + defer disco_it.deinit(); + if (try disco_it.next()) |face| { + log.info("font {s}: {s}", .{ + field.name, + try face.name(&name_buf), + }); + + _ = try c.add( + self.alloc, + style, + .{ .deferred = face }, + ); + } else log.warn("font-family {s} not found: {s}", .{ + field.name, + desc.family.?, + }); + } + } + } + + // Our built-in font will be used as a backup + _ = try c.add( + self.alloc, + .regular, + .{ .fallback_loaded = try Face.init( + self.font_lib, + face_ttf, + opts, + ) }, + ); + _ = try c.add( + self.alloc, + .bold, + .{ .fallback_loaded = try Face.init( + self.font_lib, + face_bold_ttf, + opts, + ) }, + ); + + // On macOS, always search for and add the Apple Emoji font + // as our preferred emoji font for fallback. We do this in case + // people add other emoji fonts to their system, we always want to + // prefer the official one. Users can override this by explicitly + // specifying a font-family for emoji. + if (comptime builtin.target.isDarwin()) apple_emoji: { + const disco = try self.discover() orelse break :apple_emoji; + var disco_it = try disco.discover(self.alloc, .{ + .family = "Apple Color Emoji", + }); + defer disco_it.deinit(); + if (try disco_it.next()) |face| { + _ = try c.add( + self.alloc, + .regular, + .{ .fallback_deferred = face }, + ); + } + } + + // Emoji fallback. We don't include this on Mac since Mac is expected + // to always have the Apple Emoji available on the system. + if (comptime !builtin.target.isDarwin() or Discover == void) { + _ = try c.add( + self.alloc, + .regular, + .{ .fallback_loaded = try Face.init( + self.font_lib, + face_emoji_ttf, + opts, + ) }, + ); + _ = try c.add( + self.alloc, + .regular, + .{ .fallback_loaded = try Face.init( + self.font_lib, + face_emoji_text_ttf, + opts, + ) }, + ); + } + + return c; +} + /// Decrement the ref count for the given key. If the ref count is zero, /// the GroupCache will be deinitialized and removed from the map.j:w pub fn groupDeref(self: *GroupCacheSet, key: Key) void { diff --git a/src/font/main.zig b/src/font/main.zig index feff26bfe..91a620053 100644 --- a/src/font/main.zig +++ b/src/font/main.zig @@ -6,6 +6,7 @@ pub const Atlas = @import("Atlas.zig"); pub const discovery = @import("discovery.zig"); pub const face = @import("face.zig"); pub const CodepointMap = @import("CodepointMap.zig"); +pub const Collection = @import("Collection.zig"); pub const DeferredFace = @import("DeferredFace.zig"); pub const Face = face.Face; pub const Group = @import("Group.zig"); From 89b3e3ae4efd7858f436d300ca140feea7403183 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 2 Apr 2024 10:53:55 -0700 Subject: [PATCH 11/53] font: a mess --- src/font/Group.zig | 132 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 119 insertions(+), 13 deletions(-) diff --git a/src/font/Group.zig b/src/font/Group.zig index c6ccd01ed..b9cfa2935 100644 --- a/src/font/Group.zig +++ b/src/font/Group.zig @@ -16,7 +16,6 @@ const Allocator = std.mem.Allocator; const ziglyph = @import("ziglyph"); const font = @import("main.zig"); -const Collection = @import("main.zig").Collection; const DeferredFace = @import("main.zig").DeferredFace; const Face = @import("main.zig").Face; const Library = @import("main.zig").Library; @@ -28,6 +27,13 @@ const quirks = @import("../quirks.zig"); const log = std.log.scoped(.font_group); +/// Packed array to map our styles to a set of faces. +// Note: this is not the most efficient way to store these, but there is +// usually only one font group for the entire process so this isn't the +// most important memory efficiency we can look for. This is totally opaque +// to the user so we can change this later. +const StyleArray = std.EnumArray(Style, std.ArrayListUnmanaged(GroupFace)); + /// Packed array of booleans to indicate if a style is enabled or not. pub const StyleStatus = std.EnumArray(Style, bool); @@ -86,7 +92,7 @@ metric_modifiers: ?font.face.Metrics.ModifierSet = null, /// The available faces we have. This shouldn't be modified manually. /// Instead, use the functions available on Group. -faces: Collection, +faces: StyleArray, /// The set of statuses and whether they're enabled or not. This defaults /// to true. This can be changed at runtime with no ill effect. If you @@ -113,25 +119,107 @@ descriptor_cache: DescriptorCache = .{}, /// terminal rendering will look wrong. sprite: ?font.sprite.Face = null, +/// A face in a group can be deferred or loaded. A deferred face +/// is not yet fully loaded and only represents the font descriptor +/// and usually uses less resources. A loaded face is fully parsed, +/// ready to rasterize, and usually uses more resources than a +/// deferred version. +/// +/// A face can also be a "fallback" variant that is still either +/// deferred or loaded. Today, there is only one different between +/// fallback and non-fallback (or "explicit") faces: the handling +/// of emoji presentation. +/// +/// For explicit faces, when an explicit emoji presentation is +/// not requested, we will use any glyph for that codepoint found +/// even if the font presentation does not match the UCD +/// (Unicode Character Database) value. When an explicit presentation +/// is requested (via either VS15/V16), that is always honored. +/// The reason we do this is because we assume that if a user +/// explicitly chosen a font face (hence it is "explicit" and +/// not "fallback"), they want to use any glyphs possible within that +/// font face. Fallback fonts on the other hand are picked as a +/// last resort, so we should prefer exactness if possible. +pub const GroupFace = union(enum) { + deferred: DeferredFace, // Not loaded + loaded: Face, // Loaded, explicit use + + // The same as deferred/loaded but fallback font semantics (see large + // comment above GroupFace). + fallback_deferred: DeferredFace, + fallback_loaded: Face, + + pub fn deinit(self: *GroupFace) void { + switch (self.*) { + inline .deferred, + .loaded, + .fallback_deferred, + .fallback_loaded, + => |*v| v.deinit(), + } + } + + /// True if this face satisfies the given codepoint and presentation. + fn hasCodepoint(self: GroupFace, cp: u32, p_mode: PresentationMode) bool { + return switch (self) { + // Non-fallback fonts require explicit presentation matching but + // otherwise don't care about presentation + .deferred => |v| switch (p_mode) { + .explicit => |p| v.hasCodepoint(cp, p), + .default, .any => v.hasCodepoint(cp, null), + }, + + .loaded => |face| switch (p_mode) { + .explicit => |p| face.presentation == p and face.glyphIndex(cp) != null, + .default, .any => face.glyphIndex(cp) != null, + }, + + // Fallback fonts require exact presentation matching. + .fallback_deferred => |v| switch (p_mode) { + .explicit, .default => |p| v.hasCodepoint(cp, p), + .any => v.hasCodepoint(cp, null), + }, + + .fallback_loaded => |face| switch (p_mode) { + .explicit, + .default, + => |p| face.presentation == p and face.glyphIndex(cp) != null, + .any => face.glyphIndex(cp) != null, + }, + }; + } +}; + /// Initializes an empty group. This is not useful until faces are added /// and finalizeInit is called. pub fn init( alloc: Allocator, lib: Library, size: font.face.DesiredSize, - collection: Collection, ) !Group { - return .{ - .alloc = alloc, - .lib = lib, - .size = size, - .faces = collection, - }; + var result = Group{ .alloc = alloc, .lib = lib, .size = size, .faces = undefined }; + + // Initialize all our styles to initially sized lists. + var i: usize = 0; + while (i < StyleArray.len) : (i += 1) { + result.faces.values[i] = .{}; + try result.faces.values[i].ensureTotalCapacityPrecise(alloc, 2); + } + + return result; } pub fn deinit(self: *Group) void { - self.faces.deinit(self.alloc); + { + var it = self.faces.iterator(); + while (it.next()) |entry| { + for (entry.value.items) |*item| item.deinit(); + entry.value.deinit(self.alloc); + } + } + if (self.metric_modifiers) |*v| v.deinit(self.alloc); + self.descriptor_cache.deinit(self.alloc); } @@ -144,6 +232,23 @@ pub fn faceOptions(self: *const Group) font.face.Options { }; } +/// Add a face to the list for the given style. This face will be added as +/// next in priority if others exist already, i.e. it'll be the _last_ to be +/// searched for a glyph in that list. +/// +/// The group takes ownership of the face. The face will be deallocated when +/// the group is deallocated. +pub fn addFace(self: *Group, style: Style, face: GroupFace) !FontIndex { + const list = self.faces.getPtr(style); + + // We have some special indexes so we must never pass those. + if (list.items.len >= FontIndex.Special.start - 1) return error.GroupFull; + + const idx = list.items.len; + try list.append(self.alloc, face); + return .{ .style = style, .idx = @intCast(idx) }; +} + /// This will automatically create an italicized font from the regular /// font face if we don't have any italicized fonts. pub fn italicize(self: *Group) !void { @@ -369,9 +474,7 @@ pub fn indexForCodepoint( // Discovery is supposed to only return faces that have our // codepoint but we can't search presentation in discovery so // we have to check it here. - const face: Collection.Entry = .{ - .fallback_deferred = deferred_face, - }; + const face: GroupFace = .{ .fallback_deferred = deferred_face }; if (!face.hasCodepoint(cp, p_mode)) { deferred_face.deinit(); continue; @@ -794,6 +897,9 @@ test "face count limit" { const alloc = testing.allocator; const testFont = @import("test.zig").fontRegular; + var atlas_greyscale = try font.Atlas.init(alloc, 512, .greyscale); + defer atlas_greyscale.deinit(alloc); + var lib = try Library.init(); defer lib.deinit(); From 833d54e441f3423f7133609ed39d56d3b15a47e5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 2 Apr 2024 11:07:37 -0700 Subject: [PATCH 12/53] font: Collection has load options --- src/font/Collection.zig | 126 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 124 insertions(+), 2 deletions(-) diff --git a/src/font/Collection.zig b/src/font/Collection.zig index 09e6c6c09..162ec849a 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -8,8 +8,10 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const font = @import("main.zig"); const DeferredFace = font.DeferredFace; +const DesiredSize = font.face.DesiredSize; const Face = font.Face; const Library = font.Library; +const Metrics = font.face.Metrics; const Presentation = font.Presentation; const Style = font.Style; @@ -17,12 +19,19 @@ const Style = font.Style; /// Instead, use the functions available on Collection. faces: StyleArray, +/// The load options for deferred faces in the face list. If this +/// is not set, then deferred faces will not be loaded. Attempting to +/// add a deferred face will result in an error. +load_options: ?LoadOptions = null, + /// Initialize an empty collection. -pub fn init(alloc: Allocator) !Collection { +pub fn init( + alloc: Allocator, +) !Collection { // Initialize our styles array, preallocating some space that is // likely to be used. var faces = StyleArray.initFill(.{}); - for (&faces.values) |*list| try list.ensureTotalCapacityPrecise(alloc, 2); + for (&faces.values) |*v| try v.ensureTotalCapacityPrecise(alloc, 2); return .{ .faces = faces }; } @@ -32,10 +41,13 @@ pub fn deinit(self: *Collection, alloc: Allocator) void { for (entry.value.items) |*item| item.deinit(); entry.value.deinit(alloc); } + + //self.load_options.deinit(alloc); } pub const AddError = Allocator.Error || error{ CollectionFull, + DeferredLoadingUnavailable, }; /// Add a face to the collection for the given style. This face will be added @@ -61,11 +73,47 @@ pub fn add( if (list.items.len >= Index.Special.start - 1) return error.CollectionFull; + // If this is deferred and we don't have load options, we can't. + if (face.isDeferred() and self.load_options == null) + return error.DeferredLoadingUnavailable; + const idx = list.items.len; try list.append(alloc, face); return .{ .style = style, .idx = @intCast(idx) }; } +/// Return the Face represented by a given Index. The returned pointer +/// is only valid as long as this collection is not modified. +/// +/// This will initialize the face if it is deferred and not yet loaded, +/// which can fail. +pub fn getFace(self: *Collection, index: Index) !*Face { + if (index.special() != null) return error.SpecialHasNoFace; + const list = self.faces.getPtr(index.style); + const item = &list.items[index.idx]; + return switch (item.*) { + inline .deferred, .fallback_deferred => |*d, tag| deferred: { + const opts = self.load_options orelse + return error.DeferredLoadingUnavailable; + const face = try d.load(opts.library, opts.faceOptions()); + d.deinit(); + item.* = switch (tag) { + .deferred => .{ .loaded = face }, + .fallback_deferred => .{ .fallback_loaded = face }, + else => unreachable, + }; + + break :deferred switch (tag) { + .deferred => &item.loaded, + .fallback_deferred => &item.fallback_loaded, + else => unreachable, + }; + }, + + .loaded, .fallback_loaded => |*f| f, + }; +} + /// Packed array of all Style enum cases mapped to a growable list of faces. /// /// We use this data structure because there aren't many styles and all @@ -74,6 +122,32 @@ pub fn add( /// small style count. const StyleArray = std.EnumArray(Style, std.ArrayListUnmanaged(Entry)); +/// Load options are used to configure all the details a Collection +/// needs to load deferred faces. +pub const LoadOptions = struct { + /// The library to use for loading faces. This is not owned by + /// the collection and can be used by multiple collections. When + /// deinitializing the collection, the library is not deinitialized. + library: Library, + + /// The desired font size for all loaded faces. + size: DesiredSize = .{ .points = 12 }, + + /// The metric modifiers to use for all loaded faces. If this is + /// set then the memory is owned by the collection and will be + /// freed when the collection is deinitialized. The modifier set + /// must use the same allocator as the collection. + metric_modifiers: Metrics.ModifierSet = .{}, + + /// The options to use for loading faces. + fn faceOptions(self: *const LoadOptions) font.face.Options { + return .{ + .size = self.size, + .metric_modifiers = &self.metric_modifiers, + }; + } +}; + /// A entry in a collection can be deferred or loaded. A deferred face /// is not yet fully loaded and only represents the font descriptor /// and usually uses less resources. A loaded face is fully parsed, @@ -114,6 +188,14 @@ pub const Entry = union(enum) { } } + /// True if the entry is deferred. + fn isDeferred(self: Entry) bool { + return switch (self) { + .deferred, .fallback_deferred => true, + .loaded, .fallback_loaded => false, + }; + } + /// True if this face satisfies the given codepoint and presentation. fn hasCodepoint(self: Entry, cp: u32, p_mode: PresentationMode) bool { return switch (self) { @@ -262,3 +344,43 @@ test "add full" { ) }, )); } + +test "add deferred without loading options" { + const testing = std.testing; + const alloc = testing.allocator; + + var c = try init(alloc); + defer c.deinit(alloc); + + try testing.expectError(error.DeferredLoadingUnavailable, c.add( + alloc, + .regular, + + // This can be undefined because it should never be accessed. + .{ .deferred = undefined }, + )); +} + +test getFace { + const testing = std.testing; + const alloc = testing.allocator; + const testFont = @import("test.zig").fontRegular; + + var lib = try Library.init(); + defer lib.deinit(); + + var c = try init(alloc); + defer c.deinit(alloc); + + const idx = try c.add(alloc, .regular, .{ .loaded = try Face.init( + lib, + testFont, + .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, + ) }); + + { + const face1 = try c.getFace(idx); + const face2 = try c.getFace(idx); + try testing.expectEqual(@intFromPtr(face1), @intFromPtr(face2)); + } +} From 0d0688404e39a96d4bfde378c32def5b88d9b229 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 2 Apr 2024 11:13:36 -0700 Subject: [PATCH 13/53] font: Collection.getIndex --- src/font/Collection.zig | 56 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/font/Collection.zig b/src/font/Collection.zig index 162ec849a..a7e87d898 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -114,6 +114,31 @@ pub fn getFace(self: *Collection, index: Index) !*Face { }; } +/// Return the index of the font in this collection that contains +/// the given codepoint, style, and presentation. If no font is found, +/// null is returned. +/// +/// This does not trigger font loading; deferred fonts can be +/// searched for codepoints. +pub fn getIndex( + self: *const Collection, + cp: u32, + style: Style, + p_mode: PresentationMode, +) ?Index { + for (self.faces.get(style).items, 0..) |elem, i| { + if (elem.hasCodepoint(cp, p_mode)) { + return .{ + .style = style, + .idx = @intCast(i), + }; + } + } + + // Not found + return null; +} + /// Packed array of all Style enum cases mapped to a growable list of faces. /// /// We use this data structure because there aren't many styles and all @@ -384,3 +409,34 @@ test getFace { try testing.expectEqual(@intFromPtr(face1), @intFromPtr(face2)); } } + +test getIndex { + const testing = std.testing; + const alloc = testing.allocator; + const testFont = @import("test.zig").fontRegular; + + var lib = try Library.init(); + defer lib.deinit(); + + var c = try init(alloc); + defer c.deinit(alloc); + + _ = try c.add(alloc, .regular, .{ .loaded = try Face.init( + lib, + testFont, + .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, + ) }); + + // Should find all visible ASCII + var i: u32 = 32; + while (i < 127) : (i += 1) { + const idx = c.getIndex(i, .regular, .{ .any = {} }); + try testing.expect(idx != null); + } + + // Should not find emoji + { + const idx = c.getIndex('🥸', .regular, .{ .any = {} }); + try testing.expect(idx == null); + } +} From 40b4183b1ffbf4d88b1060099c8549f2b40ca6d1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 2 Apr 2024 11:16:50 -0700 Subject: [PATCH 14/53] font: Collection deinit --- src/font/Collection.zig | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/font/Collection.zig b/src/font/Collection.zig index a7e87d898..64453fc39 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -42,7 +42,7 @@ pub fn deinit(self: *Collection, alloc: Allocator) void { entry.value.deinit(alloc); } - //self.load_options.deinit(alloc); + if (self.load_options) |*v| v.deinit(alloc); } pub const AddError = Allocator.Error || error{ @@ -164,6 +164,10 @@ pub const LoadOptions = struct { /// must use the same allocator as the collection. metric_modifiers: Metrics.ModifierSet = .{}, + pub fn deinit(self: *LoadOptions, alloc: Allocator) void { + self.metric_modifiers.deinit(alloc); + } + /// The options to use for loading faces. fn faceOptions(self: *const LoadOptions) font.face.Options { return .{ From 4d7085986407aac468877740494af8fd0e790a02 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 2 Apr 2024 11:27:54 -0700 Subject: [PATCH 15/53] font: Collection autoItalicize --- src/font/Collection.zig | 87 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/src/font/Collection.zig b/src/font/Collection.zig index 64453fc39..d9b88a1bd 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -2,6 +2,17 @@ //! ordered by priority (per style). All fonts in a collection share the same //! size so they can be used interchangeably in cases a glyph is missing in one //! and present in another. +//! +//! The purpose of a collection is to store a list of fonts by style +//! and priority order. A collection does not handle searching for font +//! callbacks, rasterization, etc. +//! +//! The collection can contain both loaded and deferred faces. Deferred faces +//! typically use less memory while still providing some necessary information +//! such as codepoint support, presentation, etc. This is useful for looking +//! for fallback fonts as efficiently as possible. For example, when the glyph +//! "X" is not found, we can quickly search through deferred fonts rather +//! than loading the font completely. const Collection = @This(); const std = @import("std"); @@ -15,6 +26,8 @@ const Metrics = font.face.Metrics; const Presentation = font.Presentation; const Style = font.Style; +const log = std.log.scoped(.font_collection); + /// The available faces we have. This shouldn't be modified manually. /// Instead, use the functions available on Collection. faces: StyleArray, @@ -139,6 +152,57 @@ pub fn getIndex( return null; } +/// Automatically create an italicized font from the regular +/// font face if we don't have one already. If we already have +/// an italicized font face, this does nothing. +pub fn autoItalicize(self: *Collection, alloc: Allocator) !void { + // If we have an italic font, do nothing. + const italic_list = self.faces.getPtr(.italic); + if (italic_list.items.len > 0) return; + + // Not all font backends support auto-italicization. + if (comptime !@hasDecl(Face, "italicize")) { + log.warn( + "no italic font face available, italics will not render", + .{}, + ); + return; + } + + // Our regular font. If we have no regular font we also do nothing. + const regular = regular: { + const list = self.faces.get(.regular); + if (list.items.len == 0) return; + + // Find our first font that is text. This will force + // loading any deferred faces but we only load them until + // we find a text face. A text face is almost always the + // first face in the list. + for (0..list.items.len) |i| { + const face = try self.getFace(.{ + .style = .regular, + .idx = @intCast(i), + }); + if (face.presentation == .text) break :regular face; + } + + // No regular text face found. + return; + }; + + // We require loading options to auto-italicize. + const opts = self.load_options orelse return error.DeferredLoadingUnavailable; + + // Try to italicize it. + const face = try regular.italicize(opts.faceOptions()); + try italic_list.append(alloc, .{ .loaded = face }); + + var buf: [256]u8 = undefined; + if (face.name(&buf)) |name| { + log.info("font auto-italicized: {s}", .{name}); + } else |_| {} +} + /// Packed array of all Style enum cases mapped to a growable list of faces. /// /// We use this data structure because there aren't many styles and all @@ -444,3 +508,26 @@ test getIndex { try testing.expect(idx == null); } } + +test autoItalicize { + const testing = std.testing; + const alloc = testing.allocator; + const testFont = @import("test.zig").fontRegular; + + var lib = try Library.init(); + defer lib.deinit(); + + var c = try init(alloc); + defer c.deinit(alloc); + c.load_options = .{ .library = lib }; + + _ = try c.add(alloc, .regular, .{ .loaded = try Face.init( + lib, + testFont, + .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, + ) }); + + try testing.expect(c.getIndex('A', .italic, .{ .any = {} }) == null); + try c.autoItalicize(alloc); + try testing.expect(c.getIndex('A', .italic, .{ .any = {} }) != null); +} From bd479db09f2929ea3e0cb02803ecc18b03513b91 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 2 Apr 2024 11:32:42 -0700 Subject: [PATCH 16/53] font: Collection setSize --- src/font/Collection.zig | 48 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/font/Collection.zig b/src/font/Collection.zig index d9b88a1bd..e3ad969bc 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -203,6 +203,31 @@ pub fn autoItalicize(self: *Collection, alloc: Allocator) !void { } else |_| {} } +/// Update the size of all faces in the collection. This will +/// also update the size in the load options for future deferred +/// face loading. +/// +/// This requires load options to be set. +pub fn setSize(self: *Collection, size: DesiredSize) !void { + // Get a pointer to our options so we can modify the size. + const opts = if (self.load_options) |*v| + v + else + return error.DeferredLoadingUnavailable; + opts.size = size; + + // Resize all our faces that are loaded + var it = self.faces.iterator(); + while (it.next()) |entry| { + for (entry.value.items) |*elem| switch (elem.*) { + .deferred, .fallback_deferred => continue, + .loaded, .fallback_loaded => |*f| try f.setSize( + opts.faceOptions(), + ), + }; + } +} + /// Packed array of all Style enum cases mapped to a growable list of faces. /// /// We use this data structure because there aren't many styles and all @@ -531,3 +556,26 @@ test autoItalicize { try c.autoItalicize(alloc); try testing.expect(c.getIndex('A', .italic, .{ .any = {} }) != null); } + +test setSize { + const testing = std.testing; + const alloc = testing.allocator; + const testFont = @import("test.zig").fontRegular; + + var lib = try Library.init(); + defer lib.deinit(); + + var c = try init(alloc); + defer c.deinit(alloc); + c.load_options = .{ .library = lib }; + + _ = try c.add(alloc, .regular, .{ .loaded = try Face.init( + lib, + testFont, + .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, + ) }); + + try testing.expectEqual(@as(u32, 12), c.load_options.?.size.points); + try c.setSize(.{ .points = 24 }); + try testing.expectEqual(@as(u32, 24), c.load_options.?.size.points); +} From 4eccd42f6b281e06038ac3d412586a239d27209e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 2 Apr 2024 14:59:54 -0700 Subject: [PATCH 17/53] font: CodepointResolver beginnings --- src/font/CodepointResolver.zig | 387 +++++++++++++++++++++++++++++++++ src/font/Collection.zig | 26 ++- src/font/main.zig | 2 + 3 files changed, 413 insertions(+), 2 deletions(-) create mode 100644 src/font/CodepointResolver.zig diff --git a/src/font/CodepointResolver.zig b/src/font/CodepointResolver.zig new file mode 100644 index 000000000..4efeafbbc --- /dev/null +++ b/src/font/CodepointResolver.zig @@ -0,0 +1,387 @@ +//! CodepointResolver maps a codepoint to a font. It is more dynamic +//! than "Collection" since it supports mapping codepoint ranges to +//! specific fonts, searching for fallback fonts, and more. +//! +//! To initialize the codepoint resolver, manually initialize using +//! Zig initialization syntax: .{}-style. Set the fields you want set, +//! and begin using the resolver. +//! +//! Deinit must still be called on the resolver to free any memory +//! allocated during use. All functions that take allocators should use +//! the same allocator. +const CodepointResolver = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const ziglyph = @import("ziglyph"); +const font = @import("main.zig"); +const CodepointMap = font.CodepointMap; +const Collection = font.Collection; +const Discover = font.Discover; +const DiscoveryDescriptor = font.discovery.Descriptor; +const Face = font.Face; +const Library = font.Library; +const Presentation = font.Presentation; +const SpriteFace = font.SpriteFace; +const Style = font.Style; + +const log = std.log.scoped(.font_codepoint_resolver); + +/// The underlying collection of fonts. This will be modified as +/// new fonts are found via the resolver. The resolver takes ownership +/// of the collection and will deinit it when it is deinitialized. +collection: Collection, + +/// The set of statuses and whether they're enabled or not. This defaults +/// to true. This can be changed at runtime with no ill effect. +styles: StyleStatus = StyleStatus.initFill(true), + +/// If discovery is available, we'll look up fonts where we can't find +/// the codepoint. This can be set after initialization. +discover: ?*Discover = null, + +/// A map of codepoints to font requests for codepoint-level overrides. +/// The memory associated with the map is owned by the caller and is not +/// modified or freed by Group. +codepoint_map: ?CodepointMap = null, + +/// The descriptor cache is used to cache the descriptor to font face +/// mapping for codepoint maps. +descriptor_cache: DescriptorCache = .{}, + +/// Set this to a non-null value to enable sprite glyph drawing. If this +/// isn't enabled we'll just fall through to trying to use regular fonts +/// to render sprite glyphs. But more than likely, if this isn't set then +/// terminal rendering will look wrong. +sprite: ?SpriteFace = null, + +pub fn deinit(self: *CodepointResolver, alloc: Allocator) void { + self.collection.deinit(alloc); + self.descriptor_cache.deinit(alloc); +} + +/// Looks up the font that should be used for a specific codepoint. +/// The font index is valid as long as font faces aren't removed. This +/// isn't cached; it is expected that downstream users handle caching if +/// that is important. +/// +/// Optionally, a presentation format can be specified. This presentation +/// format will be preferred but if it can't be found in this format, +/// any format will be accepted. If presentation is null, the UCD +/// (Unicode Character Database) will be used to determine the default +/// presentation for the codepoint. +/// a code point. +/// +/// This logic is relatively complex so the exact algorithm is documented +/// here. If this gets out of sync with the code, ask questions. +/// +/// 1. If a font style is requested that is disabled (i.e. bold), +/// we start over with the regular font style. The regular font style +/// cannot be disabled, but it can be replaced with a stylized font +/// face. +/// +/// 2. If there is a codepoint override for the codepoint, we satisfy +/// that requirement if we can, no matter what style or presentation. +/// +/// 3. If this is a sprite codepoint (such as an underline), then the +/// sprite font always is the result. +/// +/// 4. If the exact style and presentation request can be satisfied by +/// one of our loaded fonts, we return that value. We search loaded +/// fonts in the order they're added to the group, so the caller must +/// set the priority order. +/// +/// 5. If the style isn't regular, we restart this process at this point +/// but with the regular style. This lets us fall back to regular with +/// our loaded fonts before trying a fallback. We'd rather show a regular +/// version of a codepoint from a loaded font than find a new font in +/// the correct style because styles in other fonts often change +/// metrics like glyph widths. +/// +/// 6. If the style is regular, and font discovery is enabled, we look +/// for a fallback font to satisfy our request. +/// +/// 7. Finally, as a last resort, we fall back to restarting this whole +/// process with a regular font face satisfying ANY presentation for +/// the codepoint. If this fails, we return null. +/// +pub fn getIndex( + self: *CodepointResolver, + alloc: Allocator, + cp: u32, + style: Style, + p: ?Presentation, +) ?Collection.Index { + // If we've disabled a font style, then fall back to regular. + if (style != .regular and !self.styles.get(style)) { + return self.getIndex(alloc, cp, .regular, p); + } + + // Codepoint overrides. + if (self.indexForCodepointOverride(alloc, cp)) |idx_| { + if (idx_) |idx| return idx; + } else |err| { + log.warn("codepoint override failed codepoint={} err={}", .{ cp, err }); + } + + // If we have sprite drawing enabled, check if our sprite face can + // handle this. + if (self.sprite) |sprite| { + if (sprite.hasCodepoint(cp, p)) { + return Collection.Index.initSpecial(.sprite); + } + } + + // Build our presentation mode. If we don't have an explicit presentation + // given then we use the UCD (Unicode Character Database) to determine + // the default presentation. Note there is some inefficiency here because + // we'll do this muliple times if we recurse, but this is a cached function + // call higher up (GroupCache) so this should be rare. + const p_mode: Collection.PresentationMode = if (p) |v| .{ .explicit = v } else .{ + .default = if (ziglyph.emoji.isEmojiPresentation(@intCast(cp))) + .emoji + else + .text, + }; + + // If we can find the exact value, then return that. + if (self.collection.getIndex(cp, style, p_mode)) |value| return value; + + // If we're not a regular font style, try looking for a regular font + // that will satisfy this request. Blindly looking for unmatched styled + // fonts to satisfy one codepoint results in some ugly rendering. + if (style != .regular) { + if (self.getIndex(alloc, cp, .regular, p)) |value| return value; + } + + // If we are regular, try looking for a fallback using discovery. + if (style == .regular and font.Discover != void) { + log.debug("searching for a fallback font for cp={X}", .{cp}); + if (self.discover) |disco| discover: { + const load_opts = self.collection.load_options orelse + break :discover; + var disco_it = disco.discover(alloc, .{ + .codepoint = cp, + .size = load_opts.size.points, + .bold = style == .bold or style == .bold_italic, + .italic = style == .italic or style == .bold_italic, + .monospace = false, + }) catch break :discover; + defer disco_it.deinit(); + + while (true) { + var deferred_face = (disco_it.next() catch |err| { + log.warn("fallback search failed with error err={}", .{err}); + break; + }) orelse break; + + // Discovery is supposed to only return faces that have our + // codepoint but we can't search presentation in discovery so + // we have to check it here. + const face: Collection.Entry = .{ .fallback_deferred = deferred_face }; + if (!face.hasCodepoint(cp, p_mode)) { + deferred_face.deinit(); + continue; + } + + var buf: [256]u8 = undefined; + log.info("found codepoint 0x{X} in fallback face={s}", .{ + cp, + deferred_face.name(&buf) catch "", + }); + return self.collection.add(alloc, style, face) catch { + deferred_face.deinit(); + break :discover; + }; + } + + log.debug("no fallback face found for cp={X}", .{cp}); + } + } + + // If this is already regular, we're done falling back. + if (style == .regular and p == null) return null; + + // For non-regular fonts, we fall back to regular with any presentation + return self.collection.getIndex(cp, .regular, .{ .any = {} }); +} + +/// Checks if the codepoint is in the map of codepoint overrides, +/// finds the override font, and returns it. +fn indexForCodepointOverride( + self: *CodepointResolver, + alloc: Allocator, + cp: u32, +) !?Collection.Index { + // If discovery is disabled then we can't do codepoint overrides + // since the override is based on discovery to find the font. + if (comptime font.Discover == void) return null; + + // Get our codepoint map. If we have no map set then we have no + // codepoint overrides and we're done. + const map = self.codepoint_map orelse return null; + + // If we have a codepoint too large or isn't in the map, then we + // don't have an override. The map returns a descriptor that can be + // used with font discovery to search for a matching font. + const cp_u21 = std.math.cast(u21, cp) orelse return null; + const desc = map.get(cp_u21) orelse return null; + + // Fast path: the descriptor is already loaded. This means that we + // already did the search before and we have an exact font for this + // codepoint. + const idx_: ?Collection.Index = self.descriptor_cache.get(desc) orelse idx: { + // Slow path: we have to find this descriptor and load the font + const discover = self.discover orelse return null; + var disco_it = try discover.discover(alloc, desc); + defer disco_it.deinit(); + + const face = (try disco_it.next()) orelse { + log.warn( + "font lookup for codepoint map failed codepoint={} err=FontNotFound", + .{cp}, + ); + + // Add null to the cache so we don't do a lookup again later. + try self.descriptor_cache.put(alloc, desc, null); + return null; + }; + + // Add the font to our list of fonts so we can get an index for it, + // and ensure the index is stored in the descriptor cache for next time. + const idx = try self.collection.add( + alloc, + .regular, + .{ .deferred = face }, + ); + try self.descriptor_cache.put(alloc, desc, idx); + + break :idx idx; + }; + + // The descriptor cache will populate null if the descriptor is not found + // to avoid expensive discoveries later, so if it is null then we already + // searched and found nothing. + const idx = idx_ orelse return null; + + // We need to verify that this index has the codepoint we want. + if (self.collection.hasCodepoint(idx, cp, null)) { + log.debug("codepoint override based on config codepoint={} family={s}", .{ + cp, + desc.family orelse "", + }); + + return idx; + } + + return null; +} + +/// Packed array of booleans to indicate if a style is enabled or not. +pub const StyleStatus = std.EnumArray(Style, bool); + +/// Map of descriptors to faces. This is used with manual codepoint maps +/// to ensure that we don't load the same font multiple times. +/// +/// Note that the current implementation will load the same font multiple +/// times if the font used for a codepoint map is identical to a font used +/// for a regular style. That's just an inefficient choice made now because +/// the implementation is simpler and codepoint maps matching a regular +/// font is a rare case. +const DescriptorCache = std.HashMapUnmanaged( + DiscoveryDescriptor, + ?Collection.Index, + struct { + const KeyType = DiscoveryDescriptor; + + pub fn hash(ctx: @This(), k: KeyType) u64 { + _ = ctx; + return k.hashcode(); + } + + pub fn eql(ctx: @This(), a: KeyType, b: KeyType) bool { + // Note that this means its possible to have two different + // descriptors match when there is a hash collision so we + // should button this up later. + return ctx.hash(a) == ctx.hash(b); + } + }, + std.hash_map.default_max_load_percentage, +); + +test getIndex { + const testing = std.testing; + const alloc = testing.allocator; + const testFont = @import("test.zig").fontRegular; + const testEmoji = @import("test.zig").fontEmoji; + const testEmojiText = @import("test.zig").fontEmojiText; + + var lib = try Library.init(); + defer lib.deinit(); + + var c = try Collection.init(alloc); + c.load_options = .{ .library = lib }; + + _ = try c.add(alloc, .regular, .{ .loaded = try Face.init( + lib, + testFont, + .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, + ) }); + if (font.options.backend != .coretext) { + // Coretext doesn't support Noto's format + _ = try c.add( + alloc, + .regular, + .{ .loaded = try Face.init( + lib, + testEmoji, + .{ .size = .{ .points = 12 } }, + ) }, + ); + } + _ = try c.add( + alloc, + .regular, + .{ .loaded = try Face.init( + lib, + testEmojiText, + .{ .size = .{ .points = 12 } }, + ) }, + ); + + var r: CodepointResolver = .{ .collection = c }; + defer r.deinit(alloc); + + // Should find all visible ASCII + var i: u32 = 32; + while (i < 127) : (i += 1) { + const idx = r.getIndex(alloc, i, .regular, null).?; + try testing.expectEqual(Style.regular, idx.style); + try testing.expectEqual(@as(Collection.Index.IndexInt, 0), idx.idx); + } + + // Try emoji + { + const idx = r.getIndex(alloc, '🥸', .regular, null).?; + try testing.expectEqual(Style.regular, idx.style); + try testing.expectEqual(@as(Collection.Index.IndexInt, 1), idx.idx); + } + + // Try text emoji + { + const idx = r.getIndex(alloc, 0x270C, .regular, .text).?; + try testing.expectEqual(Style.regular, idx.style); + const text_idx = if (font.options.backend == .coretext) 1 else 2; + try testing.expectEqual(@as(Collection.Index.IndexInt, text_idx), idx.idx); + } + { + const idx = r.getIndex(alloc, 0x270C, .regular, .emoji).?; + try testing.expectEqual(Style.regular, idx.style); + try testing.expectEqual(@as(Collection.Index.IndexInt, 1), idx.idx); + } + + // Box glyph should be null since we didn't set a box font + { + try testing.expect(r.getIndex(alloc, 0x1FB00, .regular, null) == null); + } +} diff --git a/src/font/Collection.zig b/src/font/Collection.zig index e3ad969bc..4d623967a 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -5,7 +5,7 @@ //! //! The purpose of a collection is to store a list of fonts by style //! and priority order. A collection does not handle searching for font -//! callbacks, rasterization, etc. +//! callbacks, rasterization, etc. For this, see CodepointResolver. //! //! The collection can contain both loaded and deferred faces. Deferred faces //! typically use less memory while still providing some necessary information @@ -152,6 +152,24 @@ pub fn getIndex( return null; } +/// Check if a specific font index has a specific codepoint. This does not +/// necessarily force the font to load. The presentation value "p" will +/// verify the Emoji representation matches if it is non-null. If "p" is +/// null then any presentation will be accepted. +pub fn hasCodepoint( + self: *const Collection, + index: Index, + cp: u32, + p: ?Presentation, +) bool { + const list = self.faces.get(index.style); + if (index.idx >= list.items.len) return false; + return list.items[index.idx].hasCodepoint( + cp, + if (p) |v| .{ .explicit = v } else .{ .any = {} }, + ); +} + /// Automatically create an italicized font from the regular /// font face if we don't have one already. If we already have /// an italicized font face, this does nothing. @@ -315,7 +333,11 @@ pub const Entry = union(enum) { } /// True if this face satisfies the given codepoint and presentation. - fn hasCodepoint(self: Entry, cp: u32, p_mode: PresentationMode) bool { + pub fn hasCodepoint( + self: Entry, + cp: u32, + p_mode: PresentationMode, + ) bool { return switch (self) { // Non-fallback fonts require explicit presentation matching but // otherwise don't care about presentation diff --git a/src/font/main.zig b/src/font/main.zig index 91a620053..0798bae05 100644 --- a/src/font/main.zig +++ b/src/font/main.zig @@ -6,6 +6,7 @@ pub const Atlas = @import("Atlas.zig"); pub const discovery = @import("discovery.zig"); pub const face = @import("face.zig"); pub const CodepointMap = @import("CodepointMap.zig"); +pub const CodepointResolver = @import("CodepointResolver.zig"); pub const Collection = @import("Collection.zig"); pub const DeferredFace = @import("DeferredFace.zig"); pub const Face = face.Face; @@ -17,6 +18,7 @@ pub const shape = @import("shape.zig"); pub const Shaper = shape.Shaper; pub const sprite = @import("sprite.zig"); pub const Sprite = sprite.Sprite; +pub const SpriteFace = sprite.Face; pub const Descriptor = discovery.Descriptor; pub const Discover = discovery.Discover; pub usingnamespace @import("library.zig"); From b2541d24f19f3233f8a862f10dfa20d1e16cd5c9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 2 Apr 2024 19:32:03 -0700 Subject: [PATCH 18/53] font: CodepointResolver style disabling test --- src/font/CodepointResolver.zig | 71 ++++++++++++++++++++++++++++++++-- src/font/Collection.zig | 55 +++++++++++++++++++++++--- 2 files changed, 118 insertions(+), 8 deletions(-) diff --git a/src/font/CodepointResolver.zig b/src/font/CodepointResolver.zig index 4efeafbbc..7a03f0e5d 100644 --- a/src/font/CodepointResolver.zig +++ b/src/font/CodepointResolver.zig @@ -72,6 +72,15 @@ pub fn deinit(self: *CodepointResolver, alloc: Allocator) void { /// presentation for the codepoint. /// a code point. /// +/// An allocator is required because certain functionality (codepoint +/// mapping, fallback fonts, etc.) may require memory allocation. Curiously, +/// this function cannot error! If an error occurs for any reason, including +/// memory allocation, the associated functionality is ignored and the +/// resolver attempts to use a different method to satisfy the codepoint. +/// This behavior is intentional to make the resolver apply best-effort +/// logic to satisfy the codepoint since its better to render something +/// than nothing. +/// /// This logic is relatively complex so the exact algorithm is documented /// here. If this gets out of sync with the code, ask questions. /// @@ -118,7 +127,7 @@ pub fn getIndex( } // Codepoint overrides. - if (self.indexForCodepointOverride(alloc, cp)) |idx_| { + if (self.getIndexCodepointOverride(alloc, cp)) |idx_| { if (idx_) |idx| return idx; } else |err| { log.warn("codepoint override failed codepoint={} err={}", .{ cp, err }); @@ -208,7 +217,7 @@ pub fn getIndex( /// Checks if the codepoint is in the map of codepoint overrides, /// finds the override font, and returns it. -fn indexForCodepointOverride( +fn getIndexCodepointOverride( self: *CodepointResolver, alloc: Allocator, cp: u32, @@ -265,7 +274,7 @@ fn indexForCodepointOverride( const idx = idx_ orelse return null; // We need to verify that this index has the codepoint we want. - if (self.collection.hasCodepoint(idx, cp, null)) { + if (self.collection.hasCodepoint(idx, cp, .{ .any = {} })) { log.debug("codepoint override based on config codepoint={} family={s}", .{ cp, desc.family orelse "", @@ -385,3 +394,59 @@ test getIndex { try testing.expect(r.getIndex(alloc, 0x1FB00, .regular, null) == null); } } + +test "getIndex disabled font style" { + const testing = std.testing; + const alloc = testing.allocator; + const testFont = @import("test.zig").fontRegular; + + var atlas_greyscale = try font.Atlas.init(alloc, 512, .greyscale); + defer atlas_greyscale.deinit(alloc); + + var lib = try Library.init(); + defer lib.deinit(); + + var c = try Collection.init(alloc); + c.load_options = .{ .library = lib }; + + _ = try c.add(alloc, .regular, .{ .loaded = try Face.init( + lib, + testFont, + .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, + ) }); + _ = try c.add(alloc, .bold, .{ .loaded = try Face.init( + lib, + testFont, + .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, + ) }); + _ = try c.add(alloc, .italic, .{ .loaded = try Face.init( + lib, + testFont, + .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, + ) }); + + var r: CodepointResolver = .{ .collection = c }; + defer r.deinit(alloc); + r.styles.set(.bold, false); // Disable bold + + // Regular should work fine + { + const idx = r.getIndex(alloc, 'A', .regular, null).?; + try testing.expectEqual(Style.regular, idx.style); + try testing.expectEqual(@as(Collection.Index.IndexInt, 0), idx.idx); + } + + // Bold should go to regular + { + const idx = r.getIndex(alloc, 'A', .bold, null).?; + try testing.expectEqual(Style.regular, idx.style); + try testing.expectEqual(@as(Collection.Index.IndexInt, 0), idx.idx); + } + + // Italic should still work + { + const idx = r.getIndex(alloc, 'A', .italic, null).?; + try testing.expectEqual(Style.italic, idx.style); + try testing.expectEqual(@as(Collection.Index.IndexInt, 0), idx.idx); + } +} diff --git a/src/font/Collection.zig b/src/font/Collection.zig index 4d623967a..f75356bc9 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -18,6 +18,7 @@ const Collection = @This(); const std = @import("std"); const Allocator = std.mem.Allocator; const font = @import("main.zig"); +const options = font.options; const DeferredFace = font.DeferredFace; const DesiredSize = font.face.DesiredSize; const Face = font.Face; @@ -160,14 +161,11 @@ pub fn hasCodepoint( self: *const Collection, index: Index, cp: u32, - p: ?Presentation, + p_mode: PresentationMode, ) bool { const list = self.faces.get(index.style); if (index.idx >= list.items.len) return false; - return list.items[index.idx].hasCodepoint( - cp, - if (p) |v| .{ .explicit = v } else .{ .any = {} }, - ); + return list.items[index.idx].hasCodepoint(cp, p_mode); } /// Automatically create an italicized font from the regular @@ -601,3 +599,50 @@ test setSize { try c.setSize(.{ .points = 24 }); try testing.expectEqual(@as(u32, 24), c.load_options.?.size.points); } + +test hasCodepoint { + const testing = std.testing; + const alloc = testing.allocator; + const testFont = @import("test.zig").fontRegular; + + var lib = try Library.init(); + defer lib.deinit(); + + var c = try init(alloc); + defer c.deinit(alloc); + c.load_options = .{ .library = lib }; + + const idx = try c.add(alloc, .regular, .{ .loaded = try Face.init( + lib, + testFont, + .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, + ) }); + + try testing.expect(c.hasCodepoint(idx, 'A', .{ .any = {} })); + try testing.expect(!c.hasCodepoint(idx, '🥸', .{ .any = {} })); +} + +test "hasCodepoint emoji default graphical" { + if (options.backend != .fontconfig_freetype) return error.SkipZigTest; + + const testing = std.testing; + const alloc = testing.allocator; + const testEmoji = @import("test.zig").fontEmoji; + + var lib = try Library.init(); + defer lib.deinit(); + + var c = try init(alloc); + defer c.deinit(alloc); + c.load_options = .{ .library = lib }; + + const idx = try c.add(alloc, .regular, .{ .loaded = try Face.init( + lib, + testEmoji, + .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, + ) }); + + try testing.expect(!c.hasCodepoint(idx, 'A', .{ .any = {} })); + try testing.expect(c.hasCodepoint(idx, '🥸', .{ .any = {} })); + // TODO(fontmem): test explicit/implicit +} From 88db80b7b0359247185ab2f847532a853e012a50 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 2 Apr 2024 20:39:51 -0700 Subject: [PATCH 19/53] font: CodepointResolver test box glyph --- src/font/CodepointResolver.zig | 21 +++++++++++++++++++++ src/font/Group.zig | 4 ++++ 2 files changed, 25 insertions(+) diff --git a/src/font/CodepointResolver.zig b/src/font/CodepointResolver.zig index 7a03f0e5d..f902c2275 100644 --- a/src/font/CodepointResolver.zig +++ b/src/font/CodepointResolver.zig @@ -450,3 +450,24 @@ test "getIndex disabled font style" { try testing.expectEqual(@as(Collection.Index.IndexInt, 0), idx.idx); } } + +test "getIndex box glyph" { + const testing = std.testing; + const alloc = testing.allocator; + + var lib = try Library.init(); + defer lib.deinit(); + + const c = try Collection.init(alloc); + + var r: CodepointResolver = .{ + .collection = c, + .sprite = .{ .width = 18, .height = 36, .thickness = 2 }, + }; + defer r.deinit(alloc); + + // Should find a box glyph + const idx = r.getIndex(alloc, 0x2500, .regular, null).?; + try testing.expectEqual(Style.regular, idx.style); + try testing.expectEqual(@intFromEnum(Collection.Index.Special.sprite), idx.idx); +} diff --git a/src/font/Group.zig b/src/font/Group.zig index b9cfa2935..4637be3d9 100644 --- a/src/font/Group.zig +++ b/src/font/Group.zig @@ -769,6 +769,7 @@ pub const Wasm = struct { } }; +// X test { const testing = std.testing; const alloc = testing.allocator; @@ -847,6 +848,7 @@ test { } } +// X test "disabled font style" { const testing = std.testing; const alloc = testing.allocator; @@ -892,6 +894,7 @@ test "disabled font style" { } } +// X test "face count limit" { const testing = std.testing; const alloc = testing.allocator; @@ -917,6 +920,7 @@ test "face count limit" { )); } +// X test "box glyph" { const testing = std.testing; const alloc = testing.allocator; From 9fb883666af5e8ffa94cd719525ec510ebda8a8b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 2 Apr 2024 21:20:58 -0700 Subject: [PATCH 20/53] font: start SharedGrid --- src/font/SharedGrid.zig | 120 ++++++++++++++++++++++++++++++++++++++++ src/font/main.zig | 1 + 2 files changed, 121 insertions(+) create mode 100644 src/font/SharedGrid.zig diff --git a/src/font/SharedGrid.zig b/src/font/SharedGrid.zig new file mode 100644 index 000000000..bfb8f411f --- /dev/null +++ b/src/font/SharedGrid.zig @@ -0,0 +1,120 @@ +//! This structure represents the state required to render a terminal +//! grid using the font subsystem. It is "shared" because it is able to +//! be shared across multiple surfaces. +//! +//! It is desirable for the grid state to be shared because the font +//! configuration for a set of surfaces is almost always the same and +//! font data is relatively memory intensive. Further, the font subsystem +//! should be read-heavy compared to write-heavy, so it handles concurrent +//! reads well. Going even further, the font subsystem should be very rarely +//! read at all since it should only be necessary when the grid actively +//! changes. +const SharedGrid = @This(); + +// TODO(fontmem): +// - consider config changes and how they affect the shared grid. + +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const font = @import("main.zig"); +const Atlas = font.Atlas; +const CodepointResolver = font.CodepointResolver; +const Collection = font.Collection; +const Glyph = font.Glyph; +const Metrics = font.face.Metrics; +const Presentation = font.Presentation; +const Style = font.Style; +const RenderOptions = font.face.RenderOptions; + +const log = std.log.scoped(.font_shared_grid); + +/// Cache for codepoints to font indexes in a group. +codepoints: std.AutoHashMapUnmanaged(CodepointKey, ?Collection.Index) = .{}, + +/// Cache for glyph renders into the atlas. +glyphs: std.AutoHashMapUnmanaged(GlyphKey, Glyph) = .{}, + +/// The texture atlas to store renders in. The Glyph data in the glyphs +/// cache is dependent on the atlas matching. +atlas_greyscale: Atlas, +atlas_color: Atlas, + +/// The underlying resolver for font data, fallbacks, etc. The shared +/// grid takes ownership of the resolver and will free it. +resolver: CodepointResolver, + +/// The currently active grid metrics dictating the layout of the grid. +/// This is calculated based on the resolver and current fonts. +metrics: Metrics, + +pub fn init( + alloc: Allocator, + resolver: CodepointResolver, + thicken: bool, +) !SharedGrid { + // We need to support loading options since we use the size data + assert(resolver.collection.load_options != null); + + var atlas_greyscale = try Atlas.init(alloc, 512, .greyscale); + errdefer atlas_greyscale.deinit(alloc); + var atlas_color = try Atlas.init(alloc, 512, .rgba); + errdefer atlas_color.deinit(alloc); + + var result: SharedGrid = .{ + .resolver = resolver, + .atlas_greyscale = atlas_greyscale, + .atlas_color = atlas_color, + }; + + // We set an initial capacity that can fit a good number of characters. + // This number was picked empirically based on my own terminal usage. + try result.codepoints.ensureTotalCapacity(alloc, 128); + try result.glyphs.ensureTotalCapacity(alloc, 128); + + // Initialize our metrics. + try result.reloadMetrics(thicken); + + return result; +} + +pub fn deinit(self: *SharedGrid, alloc: Allocator) void { + self.codepoints.deinit(alloc); + self.glyphs.deinit(alloc); + self.atlas_greyscale.deinit(alloc); + self.atlas_color.deinit(alloc); + self.resolver.deinit(alloc); +} + +fn reloadMetrics(self: *SharedGrid, thicken: bool) !void { + // Get our cell metrics based on a regular font ascii 'M'. Why 'M'? + // Doesn't matter, any normal ASCII will do we're just trying to make + // sure we use the regular font. + // We don't go through our caching layer because we want to minimize + // possible failures. + const collection = &self.resolver.collection; + const index = collection.getIndex('M', .regular, .{ .any = {} }).?; + const face = try collection.getFace(index); + self.metrics = face.metrics; + + // Setup our sprite font. + self.resolver.sprite = .{ + .width = self.metrics.cell_width, + .height = self.metrics.cell_height, + .thickness = self.metrics.underline_thickness * + @as(u32, if (thicken) 2 else 1), + .underline_position = self.metrics.underline_position, + }; +} + +const CodepointKey = struct { + style: Style, + codepoint: u32, + presentation: ?Presentation, +}; + +const GlyphKey = struct { + index: Collection.Index, + glyph: u32, + opts: RenderOptions, +}; diff --git a/src/font/main.zig b/src/font/main.zig index 0798bae05..0932ad4e3 100644 --- a/src/font/main.zig +++ b/src/font/main.zig @@ -16,6 +16,7 @@ pub const GroupCacheSet = @import("GroupCacheSet.zig"); pub const Glyph = @import("Glyph.zig"); pub const shape = @import("shape.zig"); pub const Shaper = shape.Shaper; +pub const SharedGrid = @import("SharedGrid.zig"); pub const sprite = @import("sprite.zig"); pub const Sprite = sprite.Sprite; pub const SpriteFace = sprite.Face; From b9471f37919366d4345c48180a7fbef981a446c9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 5 Apr 2024 14:39:31 -0700 Subject: [PATCH 21/53] font: SharedGrid setup test infra --- src/font/SharedGrid.zig | 78 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/src/font/SharedGrid.zig b/src/font/SharedGrid.zig index bfb8f411f..f69a474fb 100644 --- a/src/font/SharedGrid.zig +++ b/src/font/SharedGrid.zig @@ -9,6 +9,13 @@ //! reads well. Going even further, the font subsystem should be very rarely //! read at all since it should only be necessary when the grid actively //! changes. +//! +//! SharedGrid does NOT support resizing, font-family changes, font removals +//! in collections, etc. Because the Grid is shared this would cause a +//! major disruption in the rendering of multiple surfaces (i.e. increasing +//! the font size in one would increase it in all). In many cases this isn't +//! desirable so to implement configuration changes the grid should be +//! reinitialized and all surfaces should switch over to using that one. const SharedGrid = @This(); // TODO(fontmem): @@ -21,7 +28,9 @@ const font = @import("main.zig"); const Atlas = font.Atlas; const CodepointResolver = font.CodepointResolver; const Collection = font.Collection; +const Face = font.Face; const Glyph = font.Glyph; +const Library = font.Library; const Metrics = font.face.Metrics; const Presentation = font.Presentation; const Style = font.Style; @@ -48,6 +57,18 @@ resolver: CodepointResolver, /// This is calculated based on the resolver and current fonts. metrics: Metrics, +/// The RwLock used to protect the shared grid. +lock: std.Thread.RwLock, + +/// Initialize the grid. +/// +/// The resolver must have a collection that supports deferred loading +/// (collection.load_options != null). This is because we need the load +/// options data to determine grid metrics and setup our sprite font. +/// +/// SharedGrid always configures the sprite font. This struct is expected to be +/// used with a terminal grid and therefore the sprite font is always +/// necessary for correct rendering. pub fn init( alloc: Allocator, resolver: CodepointResolver, @@ -65,6 +86,8 @@ pub fn init( .resolver = resolver, .atlas_greyscale = atlas_greyscale, .atlas_color = atlas_color, + .lock = .{}, + .metrics = undefined, // Loaded below }; // We set an initial capacity that can fit a good number of characters. @@ -78,6 +101,7 @@ pub fn init( return result; } +/// Deinit. Assumes no concurrent access so no lock is taken. pub fn deinit(self: *SharedGrid, alloc: Allocator) void { self.codepoints.deinit(alloc); self.glyphs.deinit(alloc); @@ -118,3 +142,57 @@ const GlyphKey = struct { glyph: u32, opts: RenderOptions, }; + +const TestMode = enum { normal }; + +fn testGrid(mode: TestMode, alloc: Allocator, lib: Library) !SharedGrid { + const testFont = @import("test.zig").fontRegular; + + var c = try Collection.init(alloc); + c.load_options = .{ .library = lib }; + + switch (mode) { + .normal => { + _ = try c.add(alloc, .regular, .{ .loaded = try Face.init( + lib, + testFont, + .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, + ) }); + }, + } + + var r: CodepointResolver = .{ .collection = c }; + errdefer r.deinit(alloc); + + return try init(alloc, r, false); +} + +test "SharedGrid inits metrics" { + const testing = std.testing; + const alloc = testing.allocator; + // const testEmoji = @import("test.zig").fontEmoji; + + var lib = try Library.init(); + defer lib.deinit(); + + var grid = try testGrid(.normal, alloc, lib); + defer grid.deinit(alloc); + + // Visible ASCII. Do it twice to verify cache is used. + // var i: u32 = 32; + // while (i < 127) : (i += 1) { + // const idx = (try cache.indexForCodepoint(alloc, i, .regular, null)).?; + // try testing.expectEqual(Style.regular, idx.style); + // try testing.expectEqual(@as(Group.FontIndex.IndexInt, 0), idx.idx); + // + // // Render + // const face = try cache.group.faceFromIndex(idx); + // const glyph_index = face.glyphIndex(i).?; + // _ = try cache.renderGlyph( + // alloc, + // idx, + // glyph_index, + // .{}, + // ); + // } +} From 719c5d7c2504ce06744d401a6309ff68c19aa095 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 5 Apr 2024 15:03:22 -0700 Subject: [PATCH 22/53] font: SharedGridSet starts --- src/font/SharedGrid.zig | 3 +- src/font/SharedGridSet.zig | 530 +++++++++++++++++++++++++++++++++++++ src/font/main.zig | 1 + 3 files changed, 533 insertions(+), 1 deletion(-) create mode 100644 src/font/SharedGridSet.zig diff --git a/src/font/SharedGrid.zig b/src/font/SharedGrid.zig index f69a474fb..994d84936 100644 --- a/src/font/SharedGrid.zig +++ b/src/font/SharedGrid.zig @@ -57,7 +57,8 @@ resolver: CodepointResolver, /// This is calculated based on the resolver and current fonts. metrics: Metrics, -/// The RwLock used to protect the shared grid. +/// The RwLock used to protect the shared grid. Callers are expected to use +/// this directly if they need to i.e. access the atlas directly. lock: std.Thread.RwLock, /// Initialize the grid. diff --git a/src/font/SharedGridSet.zig b/src/font/SharedGridSet.zig new file mode 100644 index 000000000..ec466dc07 --- /dev/null +++ b/src/font/SharedGridSet.zig @@ -0,0 +1,530 @@ +//! This structure contains a set of SharedGrid structures keyed by +//! unique font configuration. +//! +//! Most terminals (surfaces) will share the same font configuration. +//! This structure allows expensive font information such as +//! the font atlas, glyph cache, font faces, etc. to be shared. +//! +//! This structure itself is not thread-safe. It is expected that a single +//! main app thread handles initializing new values and dispensing them to +//! the appropriate threads. +const SharedGridSet = @This(); + +const std = @import("std"); +const builtin = @import("builtin"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; +const font = @import("main.zig"); +const CodepointResolver = font.CodepointResolver; +const Collection = font.Collection; +const Discover = font.Discover; +const Style = font.Style; +const Library = font.Library; +const Metrics = font.face.Metrics; +const CodepointMap = font.CodepointMap; +const DesiredSize = font.face.DesiredSize; +const Face = font.Face; +const SharedGrid = font.SharedGrid; +const discovery = @import("discovery.zig"); +const configpkg = @import("../config.zig"); +const Config = configpkg.Config; + +const log = std.log.scoped(.font_shared_grid_set); + +/// The allocator to use for all heap allocations. +alloc: Allocator, + +/// The map of font configurations to SharedGrid instances. +map: Map = .{}, + +/// The font library that is used for all font groups. +font_lib: Library, + +/// Font discovery mechanism. +font_discover: ?Discover = null, + +/// Initialize a new SharedGridSet. +pub fn init(alloc: Allocator) !SharedGridSet { + var font_lib = try Library.init(); + errdefer font_lib.deinit(); + + return .{ + .alloc = alloc, + .map = .{}, + .font_lib = font_lib, + }; +} + +pub fn deinit(self: *SharedGridSet) void { + var it = self.map.iterator(); + while (it.next()) |entry| { + entry.key_ptr.deinit(); + const v = entry.value_ptr.*; + v.grid.deinit(self.alloc); + self.alloc.destroy(v.grid); + } + self.map.deinit(self.alloc); + + if (comptime Discover != void) { + if (self.font_discover) |*v| v.deinit(); + } + + self.font_lib.deinit(); +} + +/// Initialize a SharedGrid for the given font configuration. If the +/// SharedGrid is not present it will be initialized with a ref count of +/// 1. If it is present, the ref count will be incremented. +/// +/// This is NOT thread-safe. +pub fn ref( + self: *SharedGridSet, + config: *const Config, + font_size: DesiredSize, +) !struct { Key, *SharedGrid } { + var key = try Key.init(self.alloc, config); + errdefer key.deinit(); + + const gop = try self.map.getOrPut(self.alloc, key); + if (gop.found_existing) { + log.debug("found cached grid for font config", .{}); + + // We can deinit the key because we found a cached value. + key.deinit(); + + // Increment our ref count and return the cache + gop.value_ptr.ref += 1; + return .{ gop.key_ptr.*, gop.value_ptr.grid }; + } + errdefer self.map.removeByPtr(gop.key_ptr); + + log.debug("initializing new grid for font config", .{}); + + // A new font config, initialize the cache. + const grid = try self.alloc.create(SharedGrid); + errdefer self.alloc.destroy(grid); + gop.value_ptr.* = .{ + .grid = grid, + .ref = 1, + }; + + grid.* = try SharedGrid.init(self.alloc, resolver: { + // Build our collection. This is the expensive operation that + // involves finding fonts, loading them (maybe, some are deferred), + // etc. + var c = try self.collection(&key, font_size); + errdefer c.deinit(self.alloc); + + // Setup our enabled/disabled styles + var styles = CodepointResolver.StyleStatus.initFill(true); + styles.set(.bold, config.@"font-style-bold" != .false); + styles.set(.italic, config.@"font-style-italic" != .false); + styles.set(.bold_italic, config.@"font-style-bold-italic" != .false); + + // Init our resolver which just requires setting fields. + break :resolver .{ + .collection = c, + .styles = styles, + .discover = try self.discover(), + .codepoint_map = key.codepoint_map, + }; + }, config.@"font-thicken"); + errdefer grid.deinit(self.alloc); + + return .{ gop.key_ptr.*, gop.value_ptr.grid }; +} + +/// Builds the Collection for the given configuration key and +/// initial font size. +fn collection( + self: *SharedGridSet, + key: *const Key, + size: DesiredSize, +) !Collection { + var c = try Collection.init(self.alloc); + errdefer c.deinit(self.alloc); + c.load_options = .{ + .library = self.font_lib, + .size = size, + .metric_modifiers = key.metric_modifiers, + }; + + const opts: font.face.Options = .{ + .size = size, + .metric_modifiers = &key.metric_modifiers, + }; + + // Search for fonts + if (Discover != void) discover: { + const disco = try self.discover() orelse { + log.warn( + "font discovery not available, cannot search for fonts", + .{}, + ); + break :discover; + }; + + // A buffer we use to store the font names for logging. + var name_buf: [256]u8 = undefined; + + inline for (@typeInfo(Style).Enum.fields) |field| { + const style = @field(Style, field.name); + for (key.descriptorsForStyle(style)) |desc| { + var disco_it = try disco.discover(self.alloc, desc); + defer disco_it.deinit(); + if (try disco_it.next()) |face| { + log.info("font {s}: {s}", .{ + field.name, + try face.name(&name_buf), + }); + + _ = try c.add( + self.alloc, + style, + .{ .deferred = face }, + ); + } else log.warn("font-family {s} not found: {s}", .{ + field.name, + desc.family.?, + }); + } + } + } + + // Our built-in font will be used as a backup + _ = try c.add( + self.alloc, + .regular, + .{ .fallback_loaded = try Face.init( + self.font_lib, + face_ttf, + opts, + ) }, + ); + _ = try c.add( + self.alloc, + .bold, + .{ .fallback_loaded = try Face.init( + self.font_lib, + face_bold_ttf, + opts, + ) }, + ); + + // On macOS, always search for and add the Apple Emoji font + // as our preferred emoji font for fallback. We do this in case + // people add other emoji fonts to their system, we always want to + // prefer the official one. Users can override this by explicitly + // specifying a font-family for emoji. + if (comptime builtin.target.isDarwin()) apple_emoji: { + const disco = try self.discover() orelse break :apple_emoji; + var disco_it = try disco.discover(self.alloc, .{ + .family = "Apple Color Emoji", + }); + defer disco_it.deinit(); + if (try disco_it.next()) |face| { + _ = try c.add( + self.alloc, + .regular, + .{ .fallback_deferred = face }, + ); + } + } + + // Emoji fallback. We don't include this on Mac since Mac is expected + // to always have the Apple Emoji available on the system. + if (comptime !builtin.target.isDarwin() or Discover == void) { + _ = try c.add( + self.alloc, + .regular, + .{ .fallback_loaded = try Face.init( + self.font_lib, + face_emoji_ttf, + opts, + ) }, + ); + _ = try c.add( + self.alloc, + .regular, + .{ .fallback_loaded = try Face.init( + self.font_lib, + face_emoji_text_ttf, + opts, + ) }, + ); + } + + // Auto-italicize + try c.autoItalicize(self.alloc); + + return c; +} + +/// Decrement the ref count for the given key. If the ref count is zero, +/// the grid will be deinitialized and removed from the map.j:w +pub fn deref(self: *SharedGridSet, key: Key) void { + const entry = self.map.getEntry(key) orelse return; + assert(entry.value_ptr.ref >= 1); + + // If we have more than one reference, decrement and return. + if (entry.value_ptr.ref > 1) { + entry.value_ptr.ref -= 1; + return; + } + + // We are at a zero ref count so deinit the group and remove. + entry.key_ptr.deinit(); + entry.value_ptr.grid.deinit(self.alloc); + self.alloc.destroy(entry.value_ptr.grid); + self.map.removeByPtr(entry.key_ptr); +} + +/// Map of font configurations to grid instances. The grid +/// instances are pointers that are heap allocated so that they're +/// stable pointers across hash map resizes. +pub const Map = std.HashMapUnmanaged( + Key, + ReffedGrid, + struct { + const KeyType = Key; + + pub fn hash(ctx: @This(), k: KeyType) u64 { + _ = ctx; + return k.hashcode(); + } + + pub fn eql(ctx: @This(), a: KeyType, b: KeyType) bool { + return ctx.hash(a) == ctx.hash(b); + } + }, + std.hash_map.default_max_load_percentage, +); + +/// Initialize once and return the font discovery mechanism. This remains +/// initialized throughout the lifetime of the application because some +/// font discovery mechanisms (i.e. fontconfig) are unsafe to reinit. +fn discover(self: *SharedGridSet) !?*Discover { + // If we're built without a font discovery mechanism, return null + if (comptime Discover == void) return null; + + // If we initialized, use it + if (self.font_discover) |*v| return v; + + self.font_discover = Discover.init(); + return &self.font_discover.?; +} + +/// Ref-counted SharedGrid. +const ReffedGrid = struct { + grid: *SharedGrid, + ref: u32 = 0, +}; + +/// The key used to uniquely identify a font configuration. +pub const Key = struct { + arena: ArenaAllocator, + + /// The descriptors used for all the fonts added to the + /// initial group, including all styles. This is hashed + /// in order so the order matters. All users of the struct + /// should ensure that the order is consistent. + descriptors: []const discovery.Descriptor = &.{}, + + /// These are the offsets into the descriptors array for + /// each style. For example, bold is from + /// offsets[@intFromEnum(.bold) - 1] to + /// offsets[@intFromEnum(.bold)]. + style_offsets: StyleOffsets = .{0} ** style_offsets_len, + + /// The codepoint map configuration. + codepoint_map: CodepointMap = .{}, + + /// The metric modifier set configuration. + metric_modifiers: Metrics.ModifierSet = .{}, + + const style_offsets_len = std.enums.directEnumArrayLen(Style, 0); + const StyleOffsets = [style_offsets_len]usize; + + comptime { + // We assume this throughout this structure. If this changes + // we may need to change this structure. + assert(@intFromEnum(Style.regular) == 0); + assert(@intFromEnum(Style.bold) == 1); + assert(@intFromEnum(Style.italic) == 2); + assert(@intFromEnum(Style.bold_italic) == 3); + } + + pub fn init( + alloc_gpa: Allocator, + config: *const Config, + ) !Key { + var arena = ArenaAllocator.init(alloc_gpa); + errdefer arena.deinit(); + const alloc = arena.allocator(); + + var descriptors = std.ArrayList(discovery.Descriptor).init(alloc); + defer descriptors.deinit(); + for (config.@"font-family".list.items) |family| { + try descriptors.append(.{ + .family = family, + .style = config.@"font-style".nameValue(), + .size = config.@"font-size", + .variations = config.@"font-variation".list.items, + }); + } + + // In all the styled cases below, we prefer to specify an exact + // style via the `font-style` configuration. If a style is not + // specified, we use the discovery mechanism to search for a + // style category such as bold, italic, etc. We can't specify both + // because the latter will restrict the search to only that. If + // a user says `font-style = italic` for the bold face for example, + // no results would be found if we restrict to ALSO searching for + // italic. + for (config.@"font-family-bold".list.items) |family| { + const style = config.@"font-style-bold".nameValue(); + try descriptors.append(.{ + .family = family, + .style = style, + .size = config.@"font-size", + .bold = style == null, + .variations = config.@"font-variation".list.items, + }); + } + for (config.@"font-family-italic".list.items) |family| { + const style = config.@"font-style-italic".nameValue(); + try descriptors.append(.{ + .family = family, + .style = style, + .size = config.@"font-size", + .italic = style == null, + .variations = config.@"font-variation".list.items, + }); + } + for (config.@"font-family-bold-italic".list.items) |family| { + const style = config.@"font-style-bold-italic".nameValue(); + try descriptors.append(.{ + .family = family, + .style = style, + .size = config.@"font-size", + .bold = style == null, + .italic = style == null, + .variations = config.@"font-variation".list.items, + }); + } + + // Setup the codepoint map + const codepoint_map: CodepointMap = map: { + const map = config.@"font-codepoint-map"; + if (map.map.list.len == 0) break :map .{}; + const clone = try config.@"font-codepoint-map".clone(alloc); + break :map clone.map; + }; + + // Metric modifiers + const metric_modifiers: Metrics.ModifierSet = set: { + var set: Metrics.ModifierSet = .{}; + if (config.@"adjust-cell-width") |m| try set.put(alloc, .cell_width, m); + if (config.@"adjust-cell-height") |m| try set.put(alloc, .cell_height, m); + if (config.@"adjust-font-baseline") |m| try set.put(alloc, .cell_baseline, m); + if (config.@"adjust-underline-position") |m| try set.put(alloc, .underline_position, m); + if (config.@"adjust-underline-thickness") |m| try set.put(alloc, .underline_thickness, m); + if (config.@"adjust-strikethrough-position") |m| try set.put(alloc, .strikethrough_position, m); + if (config.@"adjust-strikethrough-thickness") |m| try set.put(alloc, .strikethrough_thickness, m); + break :set set; + }; + + return .{ + .arena = arena, + .descriptors = try descriptors.toOwnedSlice(), + .style_offsets = .{ + config.@"font-family".list.items.len, + config.@"font-family-bold".list.items.len, + config.@"font-family-italic".list.items.len, + config.@"font-family-bold-italic".list.items.len, + }, + .codepoint_map = codepoint_map, + .metric_modifiers = metric_modifiers, + }; + } + + pub fn deinit(self: *Key) void { + self.arena.deinit(); + } + + /// Get the descriptors for the given font style that can be + /// used with discovery. + pub fn descriptorsForStyle( + self: Key, + style: Style, + ) []const discovery.Descriptor { + const idx = @intFromEnum(style); + const start: usize = if (idx == 0) 0 else self.style_offsets[idx - 1]; + const end = self.style_offsets[idx]; + return self.descriptors[start..end]; + } + + /// Hash the key with the given hasher. + pub fn hash(self: Key, hasher: anytype) void { + const autoHash = std.hash.autoHash; + autoHash(hasher, self.descriptors.len); + for (self.descriptors) |d| d.hash(hasher); + autoHash(hasher, self.codepoint_map); + autoHash(hasher, self.metric_modifiers.count()); + if (self.metric_modifiers.count() > 0) { + inline for (@typeInfo(Metrics.Key).Enum.fields) |field| { + const key = @field(Metrics.Key, field.name); + if (self.metric_modifiers.get(key)) |value| { + autoHash(hasher, key); + value.hash(hasher); + } + } + } + } + + /// Returns a hash code that can be used to uniquely identify this + /// action. + pub fn hashcode(self: Key) u64 { + var hasher = std.hash.Wyhash.init(0); + self.hash(&hasher); + return hasher.final(); + } +}; + +const face_ttf = @embedFile("res/JetBrainsMono-Regular.ttf"); +const face_bold_ttf = @embedFile("res/JetBrainsMono-Bold.ttf"); +const face_emoji_ttf = @embedFile("res/NotoColorEmoji.ttf"); +const face_emoji_text_ttf = @embedFile("res/NotoEmoji-Regular.ttf"); + +test "Key" { + const testing = std.testing; + const alloc = testing.allocator; + var cfg = try Config.default(alloc); + defer cfg.deinit(); + + var k = try Key.init(alloc, &cfg); + defer k.deinit(); + + try testing.expect(k.hashcode() > 0); +} + +test SharedGridSet { + const testing = std.testing; + const alloc = testing.allocator; + + var set = try SharedGridSet.init(alloc); + defer set.deinit(); + + var config = try Config.default(alloc); + defer config.deinit(); + + // Get a grid for the given config + _, const grid1 = try set.ref(&config, .{ .points = 12 }); + + // Get another + _, const grid2 = try set.ref(&config, .{ .points = 12 }); + + // They should be pointer equivalent + try testing.expectEqual(@intFromPtr(grid1), @intFromPtr(grid2)); +} diff --git a/src/font/main.zig b/src/font/main.zig index 0932ad4e3..1bd70bb1c 100644 --- a/src/font/main.zig +++ b/src/font/main.zig @@ -17,6 +17,7 @@ pub const Glyph = @import("Glyph.zig"); pub const shape = @import("shape.zig"); pub const Shaper = shape.Shaper; pub const SharedGrid = @import("SharedGrid.zig"); +pub const SharedGridSet = @import("SharedGridSet.zig"); pub const sprite = @import("sprite.zig"); pub const Sprite = sprite.Sprite; pub const SpriteFace = sprite.Face; From 4a29da35257a4e8d823bc1229dd17a42ea5ef163 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 5 Apr 2024 15:15:30 -0700 Subject: [PATCH 23/53] font: SharedGridSet clarify memory ownership --- src/font/Collection.zig | 11 +++++----- src/font/SharedGridSet.zig | 45 +++++++++++++++++++++++++++----------- 2 files changed, 37 insertions(+), 19 deletions(-) diff --git a/src/font/Collection.zig b/src/font/Collection.zig index f75356bc9..89d4aee4c 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -263,18 +263,17 @@ pub const LoadOptions = struct { /// The desired font size for all loaded faces. size: DesiredSize = .{ .points = 12 }, - /// The metric modifiers to use for all loaded faces. If this is - /// set then the memory is owned by the collection and will be - /// freed when the collection is deinitialized. The modifier set - /// must use the same allocator as the collection. + /// The metric modifiers to use for all loaded faces. The memory + /// for this is owned by the user and is not freed by the collection. metric_modifiers: Metrics.ModifierSet = .{}, pub fn deinit(self: *LoadOptions, alloc: Allocator) void { - self.metric_modifiers.deinit(alloc); + _ = self; + _ = alloc; } /// The options to use for loading faces. - fn faceOptions(self: *const LoadOptions) font.face.Options { + pub fn faceOptions(self: *const LoadOptions) font.face.Options { return .{ .size = self.size, .metric_modifiers = &self.metric_modifiers, diff --git a/src/font/SharedGridSet.zig b/src/font/SharedGridSet.zig index ec466dc07..e629b9595 100644 --- a/src/font/SharedGridSet.zig +++ b/src/font/SharedGridSet.zig @@ -73,11 +73,19 @@ pub fn deinit(self: *SharedGridSet) void { self.font_lib.deinit(); } +/// Returns the number of cached grids. +pub fn count(self: *const SharedGridSet) usize { + return self.map.count(); +} + /// Initialize a SharedGrid for the given font configuration. If the /// SharedGrid is not present it will be initialized with a ref count of /// 1. If it is present, the ref count will be incremented. /// /// This is NOT thread-safe. +/// +/// The returned data (key and grid) should never be freed. The memory is +/// owned by the set and will be freed when the ref count reaches zero. pub fn ref( self: *SharedGridSet, config: *const Config, @@ -142,18 +150,19 @@ fn collection( key: *const Key, size: DesiredSize, ) !Collection { - var c = try Collection.init(self.alloc); - errdefer c.deinit(self.alloc); - c.load_options = .{ + // A quick note on memory management: + // - font_lib is owned by the SharedGridSet + // - metric_modifiers is owned by the key which is freed only when + // the ref count for this grid reaches zero. + const load_options: Collection.LoadOptions = .{ .library = self.font_lib, .size = size, .metric_modifiers = key.metric_modifiers, }; - const opts: font.face.Options = .{ - .size = size, - .metric_modifiers = &key.metric_modifiers, - }; + var c = try Collection.init(self.alloc); + errdefer c.deinit(self.alloc); + c.load_options = load_options; // Search for fonts if (Discover != void) discover: { @@ -199,7 +208,7 @@ fn collection( .{ .fallback_loaded = try Face.init( self.font_lib, face_ttf, - opts, + load_options.faceOptions(), ) }, ); _ = try c.add( @@ -208,7 +217,7 @@ fn collection( .{ .fallback_loaded = try Face.init( self.font_lib, face_bold_ttf, - opts, + load_options.faceOptions(), ) }, ); @@ -241,7 +250,7 @@ fn collection( .{ .fallback_loaded = try Face.init( self.font_lib, face_emoji_ttf, - opts, + load_options.faceOptions(), ) }, ); _ = try c.add( @@ -250,7 +259,7 @@ fn collection( .{ .fallback_loaded = try Face.init( self.font_lib, face_emoji_text_ttf, - opts, + load_options.faceOptions(), ) }, ); } @@ -520,11 +529,21 @@ test SharedGridSet { defer config.deinit(); // Get a grid for the given config - _, const grid1 = try set.ref(&config, .{ .points = 12 }); + const key1, const grid1 = try set.ref(&config, .{ .points = 12 }); + try testing.expectEqual(@as(usize, 1), set.count()); // Get another - _, const grid2 = try set.ref(&config, .{ .points = 12 }); + const key2, const grid2 = try set.ref(&config, .{ .points = 12 }); + try testing.expectEqual(@as(usize, 1), set.count()); // They should be pointer equivalent try testing.expectEqual(@intFromPtr(grid1), @intFromPtr(grid2)); + + // If I deref grid2 then we should still have a count of 1 + set.deref(key2); + try testing.expectEqual(@as(usize, 1), set.count()); + + // If I deref grid1 then we should have a count of 0 + set.deref(key1); + try testing.expectEqual(@as(usize, 0), set.count()); } From 04e0cd29e59ac9b99e0a7f98df398c6c082ba694 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 5 Apr 2024 15:24:24 -0700 Subject: [PATCH 24/53] core: begin converting to SharedGridSet, renderers still broken --- src/App.zig | 10 +++++----- src/Surface.zig | 14 +++++++------- src/font/SharedGrid.zig | 11 +++++++++++ src/renderer/Options.zig | 2 +- src/renderer/size.zig | 20 -------------------- 5 files changed, 24 insertions(+), 33 deletions(-) diff --git a/src/App.zig b/src/App.zig index 53ca77c6d..d9b5e67f2 100644 --- a/src/App.zig +++ b/src/App.zig @@ -43,7 +43,7 @@ quit: bool, /// The set of font GroupCache instances shared by surfaces with the /// same font configuration. -font_group_set: font.GroupCacheSet, +font_grid_set: font.SharedGridSet, /// Initialize the main app instance. This creates the main window, sets /// up the renderer state, compiles the shaders, etc. This is the primary @@ -54,15 +54,15 @@ pub fn create( var app = try alloc.create(App); errdefer alloc.destroy(app); - var font_group_set = try font.GroupCacheSet.init(alloc); - errdefer font_group_set.deinit(); + var font_grid_set = try font.SharedGridSet.init(alloc); + errdefer font_grid_set.deinit(); app.* = .{ .alloc = alloc, .surfaces = .{}, .mailbox = .{}, .quit = false, - .font_group_set = font_group_set, + .font_grid_set = font_grid_set, }; errdefer app.surfaces.deinit(alloc); @@ -76,7 +76,7 @@ pub fn destroy(self: *App) void { // Clean up our font group cache // TODO(fontmem): assert all ref counts are zero - self.font_group_set.deinit(); + self.font_grid_set.deinit(); self.alloc.destroy(self); } diff --git a/src/Surface.zig b/src/Surface.zig index 00258e112..25a8b800d 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -54,7 +54,7 @@ rt_app: *apprt.runtime.App, rt_surface: *apprt.runtime.Surface, /// The font structures -font_group_key: font.GroupCacheSet.Key, +font_grid_key: font.SharedGridSet.Key, font_size: font.face.DesiredSize, /// The renderer for this surface. @@ -321,13 +321,13 @@ pub fn init( // Setup our font group. This will reuse an existing font group if // it was already loaded. - const font_group_key, const font_group = try app.font_group_set.groupRef( + const font_grid_key, const font_grid = try app.font_grid_set.ref( config, font_size, ); // Pre-calculate our initial cell size ourselves. - const cell_size = try renderer.CellSize.init(alloc, font_group); + const cell_size = font_grid.cellSize(); // Convert our padding from points to pixels const padding_x: u32 = padding_x: { @@ -349,7 +349,7 @@ pub fn init( const app_mailbox: App.Mailbox = .{ .rt_app = rt_app, .mailbox = &app.mailbox }; var renderer_impl = try Renderer.init(alloc, .{ .config = try Renderer.DerivedConfig.init(alloc, config), - .font_group = font_group, + .font_grid = font_grid, .padding = .{ .explicit = padding, .balance = config.@"window-padding-balance", @@ -410,7 +410,7 @@ pub fn init( .app = app, .rt_app = rt_app, .rt_surface = rt_surface, - .font_group_key = font_group_key, + .font_grid_key = font_grid_key, .font_size = font_size, .renderer = renderer_impl, .renderer_thread = render_thread, @@ -530,8 +530,8 @@ pub fn deinit(self: *Surface) void { self.alloc.destroy(v); } - // Clean up our font group - self.app.font_group_set.groupDeref(self.font_group_key); + // Clean up our font grid + self.app.font_grid_set.deref(self.font_grid_key); // Clean up our render state if (self.renderer_state.preedit) |p| self.alloc.free(p.codepoints); diff --git a/src/font/SharedGrid.zig b/src/font/SharedGrid.zig index 994d84936..1333f2849 100644 --- a/src/font/SharedGrid.zig +++ b/src/font/SharedGrid.zig @@ -24,6 +24,7 @@ const SharedGrid = @This(); const std = @import("std"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; +const renderer = @import("../renderer.zig"); const font = @import("main.zig"); const Atlas = font.Atlas; const CodepointResolver = font.CodepointResolver; @@ -132,6 +133,16 @@ fn reloadMetrics(self: *SharedGrid, thicken: bool) !void { }; } +/// Returns the grid cell size. +/// +/// This is not thread safe. +pub fn cellSize(self: *SharedGrid) renderer.CellSize { + return .{ + .width = self.metrics.cell_width, + .height = self.metrics.cell_height, + }; +} + const CodepointKey = struct { style: Style, codepoint: u32, diff --git a/src/renderer/Options.zig b/src/renderer/Options.zig index c951eacd1..18e98c73c 100644 --- a/src/renderer/Options.zig +++ b/src/renderer/Options.zig @@ -9,7 +9,7 @@ const Config = @import("../config.zig").Config; config: renderer.Renderer.DerivedConfig, /// The font group that should be used. -font_group: *font.GroupCache, +font_grid: *font.SharedGrid, /// Padding options for the viewport. padding: Padding, diff --git a/src/renderer/size.zig b/src/renderer/size.zig index 4f6b5fc5b..7b458b57e 100644 --- a/src/renderer/size.zig +++ b/src/renderer/size.zig @@ -17,26 +17,6 @@ const log = std.log.scoped(.renderer_size); pub const CellSize = struct { width: u32, height: u32, - - /// Initialize the cell size information from a font group. This ensures - /// that all renderers use the same cell sizing information for the same - /// fonts. - pub fn init(alloc: Allocator, group: *font.GroupCache) !CellSize { - // Get our cell metrics based on a regular font ascii 'M'. Why 'M'? - // Doesn't matter, any normal ASCII will do we're just trying to make - // sure we use the regular font. - const metrics = metrics: { - const index = (try group.indexForCodepoint(alloc, 'M', .regular, .text)).?; - const face = try group.group.faceFromIndex(index); - break :metrics face.metrics; - }; - log.debug("cell dimensions={}", .{metrics}); - - return CellSize{ - .width = metrics.cell_width, - .height = metrics.cell_height, - }; - } }; /// The dimensions of the screen that the grid is rendered to. This is the From d6c048f1e3df46da140bb2a32787c757977054d0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 5 Apr 2024 15:35:16 -0700 Subject: [PATCH 25/53] renderer/metal: picking away at font group => grid changes --- src/font/main.zig | 1 + src/renderer/Metal.zig | 72 ++++++++++++++++++++++------------------ src/renderer/Options.zig | 2 +- 3 files changed, 41 insertions(+), 34 deletions(-) diff --git a/src/font/main.zig b/src/font/main.zig index 1bd70bb1c..be1888822 100644 --- a/src/font/main.zig +++ b/src/font/main.zig @@ -14,6 +14,7 @@ pub const Group = @import("Group.zig"); pub const GroupCache = @import("GroupCache.zig"); pub const GroupCacheSet = @import("GroupCacheSet.zig"); pub const Glyph = @import("Glyph.zig"); +pub const Metrics = face.Metrics; pub const shape = @import("shape.zig"); pub const Shaper = shape.Shaper; pub const SharedGrid = @import("SharedGrid.zig"); diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index e89454f9d..4abb4c0fd 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -97,7 +97,7 @@ cells: std.ArrayListUnmanaged(mtl_shaders.Cell), uniforms: mtl_shaders.Uniforms, /// The font structures. -font_group: *font.GroupCache, +font_grid: *font.SharedGrid, font_shaper: font.Shaper, /// The images that we may render. @@ -343,25 +343,6 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { // to blurry rendering. layer.setProperty("contentsScale", info.scaleFactor); - // Get our cell metrics based on a regular font ascii 'M'. Why 'M'? - // Doesn't matter, any normal ASCII will do we're just trying to make - // sure we use the regular font. - const metrics = metrics: { - const index = (try options.font_group.indexForCodepoint(alloc, 'M', .regular, .text)).?; - const face = try options.font_group.group.faceFromIndex(index); - break :metrics face.metrics; - }; - log.debug("cell dimensions={}", .{metrics}); - - // Set the sprite font up - options.font_group.group.sprite = font.sprite.Face{ - .width = metrics.cell_width, - .height = metrics.cell_height, - .thickness = metrics.underline_thickness * - @as(u32, if (options.config.font_thicken) 2 else 1), - .underline_position = metrics.underline_position, - }; - // Create the font shaper. We initially create a shaper that can support // a width of 160 which is a common width for modern screens to help // avoid allocations later. @@ -427,15 +408,34 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { var shaders = try Shaders.init(alloc, device, custom_shaders); errdefer shaders.deinit(alloc); - // Font atlas textures - const texture_greyscale = try initAtlasTexture(device, &options.font_group.atlas_greyscale); - const texture_color = try initAtlasTexture(device, &options.font_group.atlas_color); + // Initialize all the data that requires a critical font section. + const font_critical: struct { + metrics: font.Metrics, + texture_greyscale: objc.Object, + texture_color: objc.Object, + } = font_critical: { + const grid = options.font_grid; + grid.lock.lockShared(); + defer grid.lock.unlockShared(); + + // Font atlas textures + const greyscale = try initAtlasTexture(device, &grid.atlas_greyscale); + errdefer deinitMTLResource(greyscale); + const color = try initAtlasTexture(device, &grid.atlas_color); + errdefer deinitMTLResource(color); + + break :font_critical .{ + .metrics = grid.metrics, + .texture_greyscale = greyscale, + .texture_color = color, + }; + }; return Metal{ .alloc = alloc, .config = options.config, .surface_mailbox = options.surface_mailbox, - .grid_metrics = metrics, + .grid_metrics = font_critical.metrics, .screen_size = null, .padding = options.padding, .focused = true, @@ -450,13 +450,13 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { .uniforms = .{ .projection_matrix = undefined, .cell_size = undefined, - .strikethrough_position = @floatFromInt(metrics.strikethrough_position), - .strikethrough_thickness = @floatFromInt(metrics.strikethrough_thickness), + .strikethrough_position = @floatFromInt(font_critical.metrics.strikethrough_position), + .strikethrough_thickness = @floatFromInt(font_critical.metrics.strikethrough_thickness), .min_contrast = options.config.min_contrast, }, // Fonts - .font_group = options.font_group, + .font_grid = options.font_grid, .font_shaper = font_shaper, // Shaders @@ -469,8 +469,8 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { .device = device, .queue = queue, .layer = layer, - .texture_greyscale = texture_greyscale, - .texture_color = texture_color, + .texture_greyscale = font_critical.texture_greyscale, + .texture_color = font_critical.texture_color, .custom_shader_state = custom_shader_state, }; } @@ -566,6 +566,7 @@ pub fn setFocus(self: *Metal, focus: bool) !void { /// Must be called on the render thread. pub fn setFontSize(self: *Metal, size: font.face.DesiredSize) !void { log.info("set font size={}", .{size}); + if (true) @panic("TODO"); // TODO(fontmem) // Set our new size, this will also reset our font atlas. try self.font_group.setSize(size); @@ -1381,10 +1382,15 @@ pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void { // so to be safe we just always reset it. This has a performance hit // when its not necessary but config reloading shouldn't be so // common to cause a problem. - self.font_group.reset(); - self.font_group.group.styles = config.font_styles; - self.font_group.atlas_greyscale.clear(); - self.font_group.atlas_color.clear(); + // + // TODO(fontmem): we no longer do this. the surface should handle + // font changes, create a new grid for us, and send it via message + // passing or something. + // + // self.font_group.reset(); + // self.font_group.group.styles = config.font_styles; + // self.font_group.atlas_greyscale.clear(); + // self.font_group.atlas_color.clear(); // We always redo the font shaper in case font features changed. We // could check to see if there was an actual config change but this is diff --git a/src/renderer/Options.zig b/src/renderer/Options.zig index 18e98c73c..fd7b9d714 100644 --- a/src/renderer/Options.zig +++ b/src/renderer/Options.zig @@ -8,7 +8,7 @@ const Config = @import("../config.zig").Config; /// The derived configuration for this renderer implementation. config: renderer.Renderer.DerivedConfig, -/// The font group that should be used. +/// The font grid that should be used. font_grid: *font.SharedGrid, /// Padding options for the viewport. From 329697779a662792d9e88c387044ef2b9c12a82f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 5 Apr 2024 18:51:26 -0700 Subject: [PATCH 26/53] renderer/metal: convert more --- src/font/shaper/coretext.zig | 5 +++-- src/font/shaper/run.zig | 20 ++++++++++---------- src/renderer/Metal.zig | 2 +- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index d3d57f7b1..56e172aa3 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -9,6 +9,7 @@ const DeferredFace = font.DeferredFace; const Group = font.Group; const GroupCache = font.GroupCache; const Library = font.Library; +const SharedGrid = font.SharedGrid; const Style = font.Style; const Presentation = font.Presentation; const terminal = @import("../../terminal/main.zig"); @@ -189,7 +190,7 @@ pub const Shaper = struct { pub fn runIterator( self: *Shaper, - group: *GroupCache, + grid: *SharedGrid, screen: *const terminal.Screen, row: terminal.Pin, selection: ?terminal.Selection, @@ -197,7 +198,7 @@ pub const Shaper = struct { ) font.shape.RunIterator { return .{ .hooks = .{ .shaper = self }, - .group = group, + .grid = grid, .screen = screen, .row = row, .selection = selection, diff --git a/src/font/shaper/run.zig b/src/font/shaper/run.zig index b5c29ec3f..2d982411f 100644 --- a/src/font/shaper/run.zig +++ b/src/font/shaper/run.zig @@ -15,17 +15,17 @@ pub const TextRun = struct { /// The total number of cells produced by this run. cells: u16, - /// The font group that built this run. - group: *font.GroupCache, + /// The font grid that built this run. + grid: *font.SharedGrid, /// The font index to use for the glyphs of this run. - font_index: font.Group.FontIndex, + font_index: font.Collection.Index, }; /// RunIterator is an iterator that yields text runs. pub const RunIterator = struct { hooks: font.Shaper.RunIteratorHook, - group: *font.GroupCache, + grid: *font.SharedGrid, screen: *const terminal.Screen, row: terminal.Pin, selection: ?terminal.Selection = null, @@ -49,7 +49,7 @@ pub const RunIterator = struct { if (self.i >= max) return null; // Track the font for our current run - var current_font: font.Group.FontIndex = .{}; + var current_font: font.Collection.Index = .{}; // Allow the hook to prepare try self.hooks.prepare(); @@ -117,7 +117,7 @@ pub const RunIterator = struct { } else emoji: { // If we're not a grapheme, our individual char could be // an emoji so we want to check if we expect emoji presentation. - // The font group indexForCodepoint we use below will do this + // The font grid indexForCodepoint we use below will do this // automatically. break :emoji null; }; @@ -160,7 +160,7 @@ pub const RunIterator = struct { // grapheme, i.e. combining characters), we need to find a font // that supports all of them. const font_info: struct { - idx: font.Group.FontIndex, + idx: font.Collection.Index, fallback: ?u32 = null, } = font_info: { // If we find a font that supports this entire grapheme @@ -231,7 +231,7 @@ pub const RunIterator = struct { return TextRun{ .offset = @intCast(self.i), .cells = @intCast(j - self.i), - .group = self.group, + .grid = self.grid, .font_index = current_font, }; } @@ -248,7 +248,7 @@ pub const RunIterator = struct { cell: *terminal.Cell, style: font.Style, presentation: ?font.Presentation, - ) !?font.Group.FontIndex { + ) !?font.Collection.Index { // Get the font index for the primary codepoint. const primary_cp: u32 = if (cell.isEmpty() or cell.codepoint() == 0) ' ' else cell.codepoint(); const primary = try self.group.indexForCodepoint( @@ -265,7 +265,7 @@ pub const RunIterator = struct { // If this is a grapheme, we need to find a font that supports // all of the codepoints in the grapheme. const cps = self.row.grapheme(cell) orelse return primary; - var candidates = try std.ArrayList(font.Group.FontIndex).initCapacity(alloc, cps.len + 1); + var candidates = try std.ArrayList(font.Collection.Index).initCapacity(alloc, cps.len + 1); defer candidates.deinit(); candidates.appendAssumeCapacity(primary); diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 4abb4c0fd..f5db7471c 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -1649,7 +1649,7 @@ fn rebuildCells( // Split our row into runs and shape each one. var iter = self.font_shaper.runIterator( - self.font_group, + self.font_grid, screen, row, row_selection, From c99b27d3647b96e698121f0599a550dce3652b42 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 5 Apr 2024 19:00:41 -0700 Subject: [PATCH 27/53] font: SharedGrid.getIndex --- src/font/SharedGrid.zig | 68 ++++++++++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 18 deletions(-) diff --git a/src/font/SharedGrid.zig b/src/font/SharedGrid.zig index 1333f2849..c43b15d03 100644 --- a/src/font/SharedGrid.zig +++ b/src/font/SharedGrid.zig @@ -143,6 +143,39 @@ pub fn cellSize(self: *SharedGrid) renderer.CellSize { }; } +/// Get the font index for a given codepoint. This is cached. +pub fn getIndex( + self: *SharedGrid, + alloc: Allocator, + cp: u32, + style: Style, + p: ?Presentation, +) !?Collection.Index { + const key: CodepointKey = .{ .style = style, .codepoint = cp, .presentation = p }; + + // Fast path: the cache has the value. This is almost always true and + // only requires a read lock. + { + self.lock.lockShared(); + defer self.lock.unlockShared(); + if (self.codepoints.get(key)) |v| return v; + } + + // Slow path: we need to search this codepoint + self.lock.lock(); + defer self.lock.unlock(); + + // Try to get it, if it is now in the cache another thread beat us to it. + const gop = try self.codepoints.getOrPut(alloc, key); + if (gop.found_existing) return gop.value_ptr.*; + errdefer self.codepoints.removeByPtr(gop.key_ptr); + + // Load a value and cache it. This even caches negative matches. + const value = self.resolver.getIndex(alloc, cp, style, p); + gop.value_ptr.* = value; + return value; +} + const CodepointKey = struct { style: Style, codepoint: u32, @@ -179,7 +212,7 @@ fn testGrid(mode: TestMode, alloc: Allocator, lib: Library) !SharedGrid { return try init(alloc, r, false); } -test "SharedGrid inits metrics" { +test getIndex { const testing = std.testing; const alloc = testing.allocator; // const testEmoji = @import("test.zig").fontEmoji; @@ -190,21 +223,20 @@ test "SharedGrid inits metrics" { var grid = try testGrid(.normal, alloc, lib); defer grid.deinit(alloc); - // Visible ASCII. Do it twice to verify cache is used. - // var i: u32 = 32; - // while (i < 127) : (i += 1) { - // const idx = (try cache.indexForCodepoint(alloc, i, .regular, null)).?; - // try testing.expectEqual(Style.regular, idx.style); - // try testing.expectEqual(@as(Group.FontIndex.IndexInt, 0), idx.idx); - // - // // Render - // const face = try cache.group.faceFromIndex(idx); - // const glyph_index = face.glyphIndex(i).?; - // _ = try cache.renderGlyph( - // alloc, - // idx, - // glyph_index, - // .{}, - // ); - // } + // Visible ASCII. + for (32..127) |i| { + const idx = (try grid.getIndex(alloc, @intCast(i), .regular, null)).?; + try testing.expectEqual(Style.regular, idx.style); + try testing.expectEqual(@as(Collection.Index.IndexInt, 0), idx.idx); + } + + // Do it again without a resolver set to ensure we only hit the cache + const old_resolver = grid.resolver; + grid.resolver = undefined; + defer grid.resolver = old_resolver; + for (32..127) |i| { + const idx = (try grid.getIndex(alloc, @intCast(i), .regular, null)).?; + try testing.expectEqual(Style.regular, idx.style); + try testing.expectEqual(@as(Collection.Index.IndexInt, 0), idx.idx); + } } From 90ea733cbd1aecced75124c30fa76433482a1b36 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 5 Apr 2024 19:07:45 -0700 Subject: [PATCH 28/53] font: SharedGrid hasCodepoint --- src/font/SharedGrid.zig | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/font/SharedGrid.zig b/src/font/SharedGrid.zig index c43b15d03..8c1537537 100644 --- a/src/font/SharedGrid.zig +++ b/src/font/SharedGrid.zig @@ -176,6 +176,22 @@ pub fn getIndex( return value; } +/// Returns true if the given font index has the codepoint and presentation. +pub fn hasCodepoint( + self: *SharedGrid, + idx: Collection.Index, + cp: u32, + p: ?Presentation, +) bool { + self.lock.lockShared(); + defer self.lock.unlockShared(); + return self.resolver.collection.hasCodepoint( + idx, + cp, + if (p) |v| .{ .explicit = v } else .{ .any = {} }, + ); +} + const CodepointKey = struct { style: Style, codepoint: u32, @@ -228,6 +244,7 @@ test getIndex { const idx = (try grid.getIndex(alloc, @intCast(i), .regular, null)).?; try testing.expectEqual(Style.regular, idx.style); try testing.expectEqual(@as(Collection.Index.IndexInt, 0), idx.idx); + try testing.expect(grid.hasCodepoint(idx, @intCast(i), null)); } // Do it again without a resolver set to ensure we only hit the cache From c88137d2545b1dd5fd55bf576b218a0e6e33d1a7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 5 Apr 2024 20:21:13 -0700 Subject: [PATCH 29/53] font/shaper: work on new grid APIs --- src/font/shaper/coretext.zig | 19 ++++++++++++++++++- src/font/shaper/run.zig | 12 ++++++------ 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index 56e172aa3..2ea316637 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -232,7 +232,24 @@ pub const Shaper = struct { // Get our font. We have to apply the font features we want for // the font here. const run_font: *macos.text.Font = font: { - const face = try run.group.group.faceFromIndex(run.font_index); + // The CoreText shaper relies on CoreText and CoreText claims + // that CTFonts are threadsafe. See: + // https://developer.apple.com/documentation/coretext/ + // + // Quote: + // All individual functions in Core Text are thread-safe. Font + // objects (CTFont, CTFontDescriptor, and associated objects) can + // be used simultaneously by multiple operations, work queues, or + // threads. However, the layout objects (CTTypesetter, + // CTFramesetter, CTRun, CTLine, CTFrame, and associated objects) + // should be used in a single operation, work queue, or thread. + // + // Because of this, we only acquire the read lock to grab the + // face and set it up, then release it. + run.grid.lock.lockShared(); + defer run.grid.lock.unlockShared(); + + const face = try run.grid.resolver.collection.getFace(run.font_index); const original = face.font; const attrs = try self.features.attrsDict(face.quirks_disable_default_font_features); diff --git a/src/font/shaper/run.zig b/src/font/shaper/run.zig index 2d982411f..36e7ef2ab 100644 --- a/src/font/shaper/run.zig +++ b/src/font/shaper/run.zig @@ -174,7 +174,7 @@ pub const RunIterator = struct { // Otherwise we need a fallback character. Prefer the // official replacement character. - if (try self.group.indexForCodepoint( + if (try self.grid.getIndex( alloc, 0xFFFD, // replacement char font_style, @@ -182,7 +182,7 @@ pub const RunIterator = struct { )) |idx| break :font_info .{ .idx = idx, .fallback = 0xFFFD }; // Fallback to space - if (try self.group.indexForCodepoint( + if (try self.grid.getIndex( alloc, ' ', font_style, @@ -251,7 +251,7 @@ pub const RunIterator = struct { ) !?font.Collection.Index { // Get the font index for the primary codepoint. const primary_cp: u32 = if (cell.isEmpty() or cell.codepoint() == 0) ' ' else cell.codepoint(); - const primary = try self.group.indexForCodepoint( + const primary = try self.grid.getIndex( alloc, primary_cp, style, @@ -275,7 +275,7 @@ pub const RunIterator = struct { // Find a font that supports this codepoint. If none support this // then the whole grapheme can't be rendered so we return null. - const idx = try self.group.indexForCodepoint( + const idx = try self.grid.getIndex( alloc, cp, style, @@ -286,11 +286,11 @@ pub const RunIterator = struct { // We need to find a candidate that has ALL of our codepoints for (candidates.items) |idx| { - if (!self.group.group.hasCodepoint(idx, primary_cp, presentation)) continue; + if (!self.grid.hasCodepoint(idx, primary_cp, presentation)) continue; for (cps) |cp| { // Ignore Emoji ZWJs if (cp == 0xFE0E or cp == 0xFE0F or cp == 0x200D) continue; - if (!self.group.group.hasCodepoint(idx, cp, presentation)) break; + if (!self.grid.hasCodepoint(idx, cp, presentation)) break; } else { // If the while completed, then we have a candidate that // supports all of our codepoints. From c45747bf1fd0637012f942366cc6840e178656b2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 5 Apr 2024 20:50:35 -0700 Subject: [PATCH 30/53] font: implement many rendering, caching functions for SharedGrid --- src/font/CodepointResolver.zig | 49 ++++++++++++++++++++++ src/font/SharedGrid.zig | 76 +++++++++++++++++++++++++++++++++- src/font/main.zig | 2 +- src/renderer/Metal.zig | 31 +++++++------- src/renderer/cell.zig | 4 +- 5 files changed, 141 insertions(+), 21 deletions(-) diff --git a/src/font/CodepointResolver.zig b/src/font/CodepointResolver.zig index f902c2275..370d933cb 100644 --- a/src/font/CodepointResolver.zig +++ b/src/font/CodepointResolver.zig @@ -15,13 +15,16 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const ziglyph = @import("ziglyph"); const font = @import("main.zig"); +const Atlas = font.Atlas; const CodepointMap = font.CodepointMap; const Collection = font.Collection; const Discover = font.Discover; const DiscoveryDescriptor = font.discovery.Descriptor; const Face = font.Face; +const Glyph = font.Glyph; const Library = font.Library; const Presentation = font.Presentation; +const RenderOptions = font.face.RenderOptions; const SpriteFace = font.SpriteFace; const Style = font.Style; @@ -286,6 +289,52 @@ fn getIndexCodepointOverride( return null; } +/// Returns the presentation for a specific font index. This is useful for +/// determining what atlas is needed. +pub fn getPresentation(self: *CodepointResolver, index: Collection.Index) !Presentation { + if (index.special()) |sp| return switch (sp) { + .sprite => .text, + }; + + const face = try self.collection.getFace(index); + return face.presentation; +} + +/// Render a glyph by glyph index into the given font atlas and return +/// metadata about it. +/// +/// This performs no caching, it is up to the caller to cache calls to this +/// if they want. This will also not resize the atlas if it is full. +/// +/// IMPORTANT: this renders by /glyph index/ and not by /codepoint/. The caller +/// is expected to translate codepoints to glyph indexes in some way. The most +/// trivial way to do this is to get the Face and call glyphIndex. If you're +/// doing text shaping, the text shaping library (i.e. HarfBuzz) will automatically +/// determine glyph indexes for a text run. +pub fn renderGlyph( + self: *CodepointResolver, + alloc: Allocator, + atlas: *Atlas, + index: Collection.Index, + glyph_index: u32, + opts: RenderOptions, +) !Glyph { + // Special-case fonts are rendered directly. + if (index.special()) |sp| switch (sp) { + .sprite => return try self.sprite.?.renderGlyph( + alloc, + atlas, + glyph_index, + opts, + ), + }; + + const face = try self.collection.getFace(index); + const glyph = try face.renderGlyph(alloc, atlas, glyph_index, opts); + // log.warn("GLYPH={}", .{glyph}); + return glyph; +} + /// Packed array of booleans to indicate if a style is enabled or not. pub const StyleStatus = std.EnumArray(Style, bool); diff --git a/src/font/SharedGrid.zig b/src/font/SharedGrid.zig index 8c1537537..c24ac08f5 100644 --- a/src/font/SharedGrid.zig +++ b/src/font/SharedGrid.zig @@ -43,7 +43,7 @@ const log = std.log.scoped(.font_shared_grid); codepoints: std.AutoHashMapUnmanaged(CodepointKey, ?Collection.Index) = .{}, /// Cache for glyph renders into the atlas. -glyphs: std.AutoHashMapUnmanaged(GlyphKey, Glyph) = .{}, +glyphs: std.AutoHashMapUnmanaged(GlyphKey, Render) = .{}, /// The texture atlas to store renders in. The Glyph data in the glyphs /// cache is dependent on the atlas matching. @@ -59,7 +59,9 @@ resolver: CodepointResolver, metrics: Metrics, /// The RwLock used to protect the shared grid. Callers are expected to use -/// this directly if they need to i.e. access the atlas directly. +/// this directly if they need to i.e. access the atlas directly. Because +/// callers can use this lock directly, maintainers need to be extra careful +/// to review call sites to ensure they are using the lock correctly. lock: std.Thread.RwLock, /// Initialize the grid. @@ -192,6 +194,76 @@ pub fn hasCodepoint( ); } +pub const Render = struct { + glyph: Glyph, + presentation: Presentation, +}; + +/// Render a glyph. This automatically determines the correct texture +/// atlas to use and caches the result. +pub fn renderGlyph( + self: *SharedGrid, + alloc: Allocator, + index: Collection.Index, + glyph_index: u32, + opts: RenderOptions, +) !Render { + const key: GlyphKey = .{ .index = index, .glyph = glyph_index, .opts = opts }; + + // Fast path: the cache has the value. This is almost always true and + // only requires a read lock. + { + self.lock.lockShared(); + defer self.lock.unlockShared(); + if (self.glyphs.get(key)) |v| return v; + } + + // Slow path: we need to search this codepoint + self.lock.lock(); + defer self.lock.unlock(); + + const gop = try self.glyphs.getOrPut(alloc, key); + if (gop.found_existing) return gop.value_ptr.*; + + // Get the presentation to determine what atlas to use + const p = try self.resolver.getPresentation(index); + const atlas: *font.Atlas = switch (p) { + .text => &self.atlas_greyscale, + .emoji => &self.atlas_color, + }; + + // Render into the atlas + const glyph = self.resolver.renderGlyph( + alloc, + atlas, + index, + glyph_index, + opts, + ) catch |err| switch (err) { + // If the atlas is full, we resize it + error.AtlasFull => blk: { + try atlas.grow(alloc, atlas.size * 2); + break :blk try self.resolver.renderGlyph( + alloc, + atlas, + index, + glyph_index, + opts, + ); + }, + + else => return err, + }; + + // Cache and return + gop.value_ptr.* = .{ + .glyph = glyph, + .presentation = p, + }; + + return gop.value_ptr.*; +} + const CodepointKey = struct { style: Style, codepoint: u32, diff --git a/src/font/main.zig b/src/font/main.zig index be1888822..0a0d376e9 100644 --- a/src/font/main.zig +++ b/src/font/main.zig @@ -152,7 +152,7 @@ pub const Presentation = enum(u1) { }; /// A FontIndex that can be used to use the sprite font directly. -pub const sprite_index = Group.FontIndex.initSpecial(.sprite); +pub const sprite_index = Collection.Index.initSpecial(.sprite); test { // For non-wasm we want to test everything we can diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index f5db7471c..b7bb85c4c 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -1874,7 +1874,7 @@ fn updateCell( // If the cell has a character, draw it if (cell.hasText()) fg: { // Render - const glyph = try self.font_group.renderGlyph( + const render = try self.font_grid.renderGlyph( self.alloc, shaper_run.font_index, shaper_cell.glyph_index orelse break :fg, @@ -1885,9 +1885,8 @@ fn updateCell( ); const mode: mtl_shaders.Cell.Mode = switch (try fgMode( - &self.font_group.group, + render.presentation, cell_pin, - shaper_run, )) { .normal => .fg, .color => .fg_color, @@ -1900,11 +1899,11 @@ fn updateCell( .cell_width = cell.gridWidth(), .color = .{ colors.fg.r, colors.fg.g, colors.fg.b, alpha }, .bg_color = bg, - .glyph_pos = .{ glyph.atlas_x, glyph.atlas_y }, - .glyph_size = .{ glyph.width, glyph.height }, + .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, + .glyph_size = .{ render.glyph.width, render.glyph.height }, .glyph_offset = .{ - glyph.offset_x + shaper_cell.x_offset, - glyph.offset_y + shaper_cell.y_offset, + render.glyph.offset_x + shaper_cell.x_offset, + render.glyph.offset_y + shaper_cell.y_offset, }, }); } @@ -1919,7 +1918,7 @@ fn updateCell( .curly => .underline_curly, }; - const glyph = try self.font_group.renderGlyph( + const render = try self.font_grid.renderGlyph( self.alloc, font.sprite_index, @intFromEnum(sprite), @@ -1937,9 +1936,9 @@ fn updateCell( .cell_width = cell.gridWidth(), .color = .{ color.r, color.g, color.b, alpha }, .bg_color = bg, - .glyph_pos = .{ glyph.atlas_x, glyph.atlas_y }, - .glyph_size = .{ glyph.width, glyph.height }, - .glyph_offset = .{ glyph.offset_x, glyph.offset_y }, + .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, + .glyph_size = .{ render.glyph.width, render.glyph.height }, + .glyph_offset = .{ render.glyph.offset_x, render.glyph.offset_y }, }); } @@ -1988,7 +1987,7 @@ fn addCursor( .underline => .underline, }; - const glyph = self.font_group.renderGlyph( + const render = self.font_grid.renderGlyph( self.alloc, font.sprite_index, @intFromEnum(sprite), @@ -2010,9 +2009,9 @@ fn addCursor( .cell_width = if (wide) 2 else 1, .color = .{ color.r, color.g, color.b, alpha }, .bg_color = .{ 0, 0, 0, 0 }, - .glyph_pos = .{ glyph.atlas_x, glyph.atlas_y }, - .glyph_size = .{ glyph.width, glyph.height }, - .glyph_offset = .{ glyph.offset_x, glyph.offset_y }, + .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, + .glyph_size = .{ render.glyph.width, render.glyph.height }, + .glyph_offset = .{ render.glyph.offset_x, render.glyph.offset_y }, }); return &self.cells.items[self.cells.items.len - 1]; @@ -2024,6 +2023,8 @@ fn addPreeditCell( x: usize, y: usize, ) !void { + if (true) @panic("TODO"); // TODO(fontmem) + // Preedit is rendered inverted const bg = self.foreground_color; const fg = self.background_color; diff --git a/src/renderer/cell.zig b/src/renderer/cell.zig index 44087da44..92993f660 100644 --- a/src/renderer/cell.zig +++ b/src/renderer/cell.zig @@ -20,11 +20,9 @@ pub const FgMode = enum { /// meant to be called from the typical updateCell function within a /// renderer. pub fn fgMode( - group: *font.Group, + presentation: font.Presentation, cell_pin: terminal.Pin, - shaper_run: font.shape.TextRun, ) !FgMode { - const presentation = try group.presentationFromIndex(shaper_run.font_index); return switch (presentation) { // Emoji is always full size and color. .emoji => .color, From de2b0f68578ae092f29dbd413a8051914ccc1ef9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 5 Apr 2024 21:10:10 -0700 Subject: [PATCH 31/53] font: Atlas updated to use monotonic ID --- src/font/Atlas.zig | 49 ++++++++-------- src/font/shaper/coretext.zig | 105 ++++++++++++++++------------------- 2 files changed, 73 insertions(+), 81 deletions(-) diff --git a/src/font/Atlas.zig b/src/font/Atlas.zig index 01ee5caa6..fcdf6ec53 100644 --- a/src/font/Atlas.zig +++ b/src/font/Atlas.zig @@ -38,18 +38,16 @@ nodes: std.ArrayListUnmanaged(Node) = .{}, /// different formats, you must use multiple atlases or convert the textures. format: Format = .greyscale, -/// This will be set to true when the atlas has data set on it. It is up -/// to the user of the atlas to set this to false when they observe the value. -/// This is a useful value to know if you need to send new data to the GPU or -/// not. -modified: bool = false, +/// This will be incremented every time the atlas is modified. This is useful +/// for knowing if the texture data has changed since the last time it was +/// sent to the GPU. It is up the user of the atlas to read this value atomically +/// to observe it. +modified: std.atomic.Value(usize) = .{ .raw = 0 }, -/// This will be set to true when the atlas has been resized. It is up -/// to the user of the atlas to set this to false when they observe the value. -/// The resized value is useful for sending textures to the GPU to know if -/// a new texture needs to be allocated or if an existing one can be -/// updated in-place. -resized: bool = false, +/// This will be incremented every time the atlas is resized. This is useful +/// for knowing if a GPU texture can be updated in-place or if it requires +/// a resize operation. +resized: std.atomic.Value(usize) = .{ .raw = 0 }, pub const Format = enum(u8) { greyscale = 0, @@ -99,7 +97,6 @@ pub fn init(alloc: Allocator, size: u32, format: Format) !Atlas { // This sets up our initial state result.clear(); - result.modified = false; return result; } @@ -243,7 +240,7 @@ pub fn set(self: *Atlas, reg: Region, data: []const u8) void { ); } - self.modified = true; + _ = self.modified.fetchAdd(1, .monotonic); } // Grow the texture to the new size, preserving all previously written data. @@ -284,13 +281,13 @@ pub fn grow(self: *Atlas, alloc: Allocator, size_new: u32) Allocator.Error!void }, data_old[size_old * self.format.depth() ..]); // We are both modified and resized - self.modified = true; - self.resized = true; + _ = self.modified.fetchAdd(1, .monotonic); + _ = self.resized.fetchAdd(1, .monotonic); } // Empty the atlas. This doesn't reclaim any previously allocated memory. pub fn clear(self: *Atlas) void { - self.modified = true; + _ = self.modified.fetchAdd(1, .monotonic); @memset(self.data, 0); self.nodes.clearRetainingCapacity(); @@ -475,8 +472,9 @@ test "exact fit" { var atlas = try init(alloc, 34, .greyscale); // +2 for 1px border defer atlas.deinit(alloc); + const modified = atlas.modified.load(.monotonic); _ = try atlas.reserve(alloc, 32, 32); - try testing.expect(!atlas.modified); + try testing.expectEqual(modified, atlas.modified.load(.monotonic)); try testing.expectError(Error.AtlasFull, atlas.reserve(alloc, 1, 1)); } @@ -505,9 +503,10 @@ test "writing data" { defer atlas.deinit(alloc); const reg = try atlas.reserve(alloc, 2, 2); - try testing.expect(!atlas.modified); + const old = atlas.modified.load(.monotonic); atlas.set(reg, &[_]u8{ 1, 2, 3, 4 }); - try testing.expect(atlas.modified); + const new = atlas.modified.load(.monotonic); + try testing.expect(new > old); // 33 because of the 1px border and so on try testing.expectEqual(@as(u8, 1), atlas.data[33]); @@ -531,14 +530,14 @@ test "grow" { try testing.expectEqual(@as(u8, 3), atlas.data[9]); try testing.expectEqual(@as(u8, 4), atlas.data[10]); - // Reset our state - atlas.modified = false; - atlas.resized = false; - // Expand by exactly 1 should fit our new 1x1 block. + const old_modified = atlas.modified.load(.monotonic); + const old_resized = atlas.resized.load(.monotonic); try atlas.grow(alloc, atlas.size + 1); - try testing.expect(atlas.modified); - try testing.expect(atlas.resized); + const new_modified = atlas.modified.load(.monotonic); + const new_resized = atlas.resized.load(.monotonic); + try testing.expect(new_modified > old_modified); + try testing.expect(new_resized > old_resized); _ = try atlas.reserve(alloc, 1, 1); // Ensure our data is still set. Not the offsets change due to size. diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index 2ea316637..3aaaef5d3 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -5,6 +5,7 @@ const macos = @import("macos"); const trace = @import("tracy").trace; const font = @import("../main.zig"); const Face = font.Face; +const Collection = font.Collection; const DeferredFace = font.DeferredFace; const Group = font.Group; const GroupCache = font.GroupCache; @@ -397,7 +398,7 @@ test "run iterator" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -416,7 +417,7 @@ test "run iterator" { var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -436,7 +437,7 @@ test "run iterator" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -483,7 +484,7 @@ test "run iterator: empty cells with background set" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -524,7 +525,7 @@ test "shape" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -559,7 +560,7 @@ test "shape nerd fonts" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -587,7 +588,7 @@ test "shape inconsolata ligs" { var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -611,7 +612,7 @@ test "shape inconsolata ligs" { var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -643,7 +644,7 @@ test "shape monaspace ligs" { var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -675,7 +676,7 @@ test "shape emoji width" { var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -715,7 +716,7 @@ test "shape emoji width long" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -752,7 +753,7 @@ test "shape variation selector VS15" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -787,7 +788,7 @@ test "shape variation selector VS16" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -819,7 +820,7 @@ test "shape with empty cells in between" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -857,7 +858,7 @@ test "shape Chinese characters" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -884,13 +885,6 @@ test "shape box glyphs" { var testdata = try testShaper(alloc); defer testdata.deinit(); - // Setup the box font - testdata.cache.group.sprite = font.sprite.Face{ - .width = 18, - .height = 36, - .thickness = 2, - }; - var buf: [32]u8 = undefined; var buf_idx: usize = 0; buf_idx += try std.unicode.utf8Encode(0x2500, buf[buf_idx..]); // horiz line @@ -904,7 +898,7 @@ test "shape box glyphs" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -940,7 +934,7 @@ test "shape selection boundary" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, terminal.Selection.init( @@ -963,7 +957,7 @@ test "shape selection boundary" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, terminal.Selection.init( @@ -986,7 +980,7 @@ test "shape selection boundary" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, terminal.Selection.init( @@ -1009,7 +1003,7 @@ test "shape selection boundary" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, terminal.Selection.init( @@ -1032,7 +1026,7 @@ test "shape selection boundary" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, terminal.Selection.init( @@ -1068,7 +1062,7 @@ test "shape cursor boundary" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -1087,7 +1081,7 @@ test "shape cursor boundary" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -1106,7 +1100,7 @@ test "shape cursor boundary" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -1125,7 +1119,7 @@ test "shape cursor boundary" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -1157,7 +1151,7 @@ test "shape cursor boundary and colored emoji" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -1176,7 +1170,7 @@ test "shape cursor boundary and colored emoji" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -1193,7 +1187,7 @@ test "shape cursor boundary and colored emoji" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -1223,7 +1217,7 @@ test "shape cell attribute change" { var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -1247,7 +1241,7 @@ test "shape cell attribute change" { var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -1272,7 +1266,7 @@ test "shape cell attribute change" { var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -1297,7 +1291,7 @@ test "shape cell attribute change" { var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -1321,7 +1315,7 @@ test "shape cell attribute change" { var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -1339,13 +1333,13 @@ test "shape cell attribute change" { const TestShaper = struct { alloc: Allocator, shaper: Shaper, - cache: *GroupCache, + grid: *SharedGrid, lib: Library, pub fn deinit(self: *TestShaper) void { self.shaper.deinit(); - self.cache.deinit(self.alloc); - self.alloc.destroy(self.cache); + self.grid.deinit(self.alloc); + self.alloc.destroy(self.grid); self.lib.deinit(); } }; @@ -1373,17 +1367,11 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper { var lib = try Library.init(); errdefer lib.deinit(); - var cache_ptr = try alloc.create(GroupCache); - errdefer alloc.destroy(cache_ptr); - cache_ptr.* = try GroupCache.init(alloc, try Group.init( - alloc, - lib, - .{ .points = 12 }, - )); - errdefer cache_ptr.*.deinit(alloc); + var c = try Collection.init(alloc); + c.load_options = .{ .library = lib }; // Setup group - _ = try cache_ptr.group.addFace(.regular, .{ .loaded = try Face.init( + _ = try c.add(alloc, .regular, .{ .loaded = try Face.init( lib, testFont, .{ .size = .{ .points = 12 } }, @@ -1391,7 +1379,7 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper { if (font.options.backend != .coretext) { // Coretext doesn't support Noto's format - _ = try cache_ptr.group.addFace(.regular, .{ .loaded = try Face.init( + _ = try c.add(alloc, .regular, .{ .loaded = try Face.init( lib, testEmoji, .{ .size = .{ .points = 12 } }, @@ -1408,21 +1396,26 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper { defer disco_it.deinit(); var face = (try disco_it.next()).?; errdefer face.deinit(); - _ = try cache_ptr.group.addFace(.regular, .{ .deferred = face }); + _ = try c.add(alloc, .regular, .{ .deferred = face }); } - _ = try cache_ptr.group.addFace(.regular, .{ .loaded = try Face.init( + _ = try c.add(alloc, .regular, .{ .loaded = try Face.init( lib, testEmojiText, .{ .size = .{ .points = 12 } }, ) }); + const grid_ptr = try alloc.create(SharedGrid); + errdefer alloc.destroy(grid_ptr); + grid_ptr.* = try SharedGrid.init(alloc, .{ .collection = c }, false); + errdefer grid_ptr.*.deinit(alloc); + var shaper = try Shaper.init(alloc, .{}); errdefer shaper.deinit(); return TestShaper{ .alloc = alloc, .shaper = shaper, - .cache = cache_ptr, + .grid = grid_ptr, .lib = lib, }; } From 29b172b3ac72271ec8dad040e8cf0a47f2638d43 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 5 Apr 2024 21:13:10 -0700 Subject: [PATCH 32/53] renderer/metal: use new atlas APIs --- src/renderer/Metal.zig | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index b7bb85c4c..998decb10 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -118,6 +118,8 @@ queue: objc.Object, // MTLCommandQueue layer: objc.Object, // CAMetalLayer texture_greyscale: objc.Object, // MTLTexture texture_color: objc.Object, // MTLTexture +texture_greyscale_modified: usize = 0, +texture_color_modified: usize = 0, /// Custom shader state. This is only set if we have custom shaders. custom_shader_state: ?CustomShaderState = null, @@ -774,13 +776,21 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { }; // If our font atlas changed, sync the texture data - if (self.font_group.atlas_greyscale.modified) { - try syncAtlasTexture(self.device, &self.font_group.atlas_greyscale, &self.texture_greyscale); - self.font_group.atlas_greyscale.modified = false; + texture: { + const modified = self.font_grid.atlas_greyscale.modified.load(.monotonic); + if (modified <= self.texture_greyscale_modified) break :texture; + self.font_grid.lock.lockShared(); + defer self.font_grid.lock.unlockShared(); + self.texture_greyscale_modified = self.font_grid.atlas_greyscale.modified.load(.monotonic); + try syncAtlasTexture(self.device, &self.font_grid.atlas_greyscale, &self.texture_greyscale); } - if (self.font_group.atlas_color.modified) { - try syncAtlasTexture(self.device, &self.font_group.atlas_color, &self.texture_color); - self.font_group.atlas_color.modified = false; + texture: { + const modified = self.font_grid.atlas_color.modified.load(.monotonic); + if (modified <= self.texture_color_modified) break :texture; + self.font_grid.lock.lockShared(); + defer self.font_grid.lock.unlockShared(); + self.texture_color_modified = self.font_grid.atlas_color.modified.load(.monotonic); + try syncAtlasTexture(self.device, &self.font_grid.atlas_color, &self.texture_color); } // Command buffer (MTLCommandBuffer) From 2a06cf54ba02c9a1dd7289600780fefb258b9bd9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 5 Apr 2024 21:28:50 -0700 Subject: [PATCH 33/53] core: App asserts the font grid set is empty on close --- src/App.zig | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/App.zig b/src/App.zig index d9b5e67f2..41a7887b2 100644 --- a/src/App.zig +++ b/src/App.zig @@ -75,7 +75,10 @@ pub fn destroy(self: *App) void { self.surfaces.deinit(self.alloc); // Clean up our font group cache - // TODO(fontmem): assert all ref counts are zero + // We should have zero items in the grid set at this point because + // destroy only gets called when the app is shutting down and this + // should gracefully close all surfaces. + assert(self.font_grid_set.count() == 0); self.font_grid_set.deinit(); self.alloc.destroy(self); From 6aa659c4b5c7ad23c1fb2197e3f85f115b137d46 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 5 Apr 2024 21:39:39 -0700 Subject: [PATCH 34/53] renderer/metal: re-enable preedit rendering --- src/font/SharedGrid.zig | 33 ++++++++++++++++++++++++++++++++- src/renderer/Metal.zig | 32 +++++++++----------------------- 2 files changed, 41 insertions(+), 24 deletions(-) diff --git a/src/font/SharedGrid.zig b/src/font/SharedGrid.zig index c24ac08f5..3f4d3599a 100644 --- a/src/font/SharedGrid.zig +++ b/src/font/SharedGrid.zig @@ -199,7 +199,38 @@ pub const Render = struct { presentation: Presentation, }; -/// Render a glyph. This automatically determines the correct texture +/// Render a codepoint. This uses the first font index that has the codepoint +/// and matches the presentation requested. If the codepoint cannot be found +/// in any font, an null render is returned. +pub fn renderCodepoint( + self: *SharedGrid, + alloc: Allocator, + cp: u32, + style: Style, + p: ?Presentation, + opts: RenderOptions, +) !?Render { + // Note: we could optimize the below to use way less locking, but + // at the time of writing this codepath is only called for preedit + // text which is relatively rare and almost non-existent in multiple + // surfaces at the same time. + + // Get the font that has the codepoint + const index = try self.getIndex(alloc, cp, style, p) orelse return null; + + // Get the glyph for the font + const glyph_index = glyph_index: { + self.lock.lockShared(); + defer self.lock.unlockShared(); + const face = try self.resolver.collection.getFace(index); + break :glyph_index face.glyphIndex(cp) orelse return null; + }; + + // Render + return try self.renderGlyph(alloc, index, glyph_index, opts); +} + +/// Render a glyph index. This automatically determines the correct texture /// atlas to use and caches the result. pub fn renderGlyph( self: *SharedGrid, diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 998decb10..ea94a796d 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -2033,39 +2033,25 @@ fn addPreeditCell( x: usize, y: usize, ) !void { - if (true) @panic("TODO"); // TODO(fontmem) - // Preedit is rendered inverted const bg = self.foreground_color; const fg = self.background_color; - // Get the font for this codepoint. - const font_index = if (self.font_group.indexForCodepoint( + // Render the glyph for our preedit text + const render_ = self.font_grid.renderCodepoint( self.alloc, @intCast(cp.codepoint), .regular, .text, - )) |index| index orelse return else |_| return; - - // Get the font face so we can get the glyph - const face = self.font_group.group.faceFromIndex(font_index) catch |err| { - log.warn("error getting face for font_index={} err={}", .{ font_index, err }); - return; - }; - - // Use the face to now get the glyph index - const glyph_index = face.glyphIndex(@intCast(cp.codepoint)) orelse return; - - // Render the glyph for our preedit text - const glyph = self.font_group.renderGlyph( - self.alloc, - font_index, - glyph_index, .{ .grid_metrics = self.grid_metrics }, ) catch |err| { log.warn("error rendering preedit glyph err={}", .{err}); return; }; + const render = render_ orelse { + log.warn("failed to find font for preedit codepoint={X}", .{cp.codepoint}); + return; + }; // Add our opaque background cell self.cells_bg.appendAssumeCapacity(.{ @@ -2083,9 +2069,9 @@ fn addPreeditCell( .cell_width = if (cp.wide) 2 else 1, .color = .{ fg.r, fg.g, fg.b, 255 }, .bg_color = .{ bg.r, bg.g, bg.b, 255 }, - .glyph_pos = .{ glyph.atlas_x, glyph.atlas_y }, - .glyph_size = .{ glyph.width, glyph.height }, - .glyph_offset = .{ glyph.offset_x, glyph.offset_y }, + .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, + .glyph_size = .{ render.glyph.width, render.glyph.height }, + .glyph_offset = .{ render.glyph.offset_x, render.glyph.offset_y }, }); } From b77513de1a6bcfd9d23fc35bcfb5cb86e8cabaaf Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 5 Apr 2024 21:48:53 -0700 Subject: [PATCH 35/53] font/harfbuzz: work with new font structures --- src/font/Collection.zig | 2 + src/font/shaper/harfbuzz.zig | 118 +++++++++++++++++------------------ 2 files changed, 60 insertions(+), 60 deletions(-) diff --git a/src/font/Collection.zig b/src/font/Collection.zig index 89d4aee4c..38f4b92f8 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -554,6 +554,8 @@ test getIndex { } test autoItalicize { + if (comptime !@hasDecl(Face, "italicize")) return error.SkipZigTest; + const testing = std.testing; const alloc = testing.allocator; const testFont = @import("test.zig").fontRegular; diff --git a/src/font/shaper/harfbuzz.zig b/src/font/shaper/harfbuzz.zig index 04143c090..13988ecf3 100644 --- a/src/font/shaper/harfbuzz.zig +++ b/src/font/shaper/harfbuzz.zig @@ -4,10 +4,10 @@ const Allocator = std.mem.Allocator; const harfbuzz = @import("harfbuzz"); const font = @import("../main.zig"); const Face = font.Face; +const Collection = font.Collection; const DeferredFace = font.DeferredFace; -const Group = font.Group; -const GroupCache = font.GroupCache; const Library = font.Library; +const SharedGrid = font.SharedGrid; const Style = font.Style; const Presentation = font.Presentation; const terminal = @import("../../terminal/main.zig"); @@ -83,7 +83,7 @@ pub const Shaper = struct { /// and assume the y value matches. pub fn runIterator( self: *Shaper, - group: *GroupCache, + grid: *SharedGrid, screen: *const terminal.Screen, row: terminal.Pin, selection: ?terminal.Selection, @@ -91,7 +91,7 @@ pub const Shaper = struct { ) font.shape.RunIterator { return .{ .hooks = .{ .shaper = self }, - .group = group, + .grid = grid, .screen = screen, .row = row, .selection = selection, @@ -110,7 +110,13 @@ pub const Shaper = struct { // We only do shaping if the font is not a special-case. For special-case // fonts, the codepoint == glyph_index so we don't need to run any shaping. if (run.font_index.special() == null) { - const face = try run.group.group.faceFromIndex(run.font_index); + // We have to lock the grid to get the face and unfortunately + // freetype faces (typically used with harfbuzz) are not thread + // safe so this has to be an exclusive lock. + run.grid.lock.lock(); + defer run.grid.lock.unlock(); + + const face = try run.grid.resolver.collection.getFace(run.font_index); const i = if (!face.quirks_disable_default_font_features) 0 else i: { // If we are disabling default font features we just offset // our features by the hardcoded items because always @@ -251,7 +257,7 @@ test "run iterator" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -270,7 +276,7 @@ test "run iterator" { var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -290,7 +296,7 @@ test "run iterator" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -342,7 +348,7 @@ test "run iterator: empty cells with background set" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -385,7 +391,7 @@ test "shape" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -414,7 +420,7 @@ test "shape inconsolata ligs" { var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -439,7 +445,7 @@ test "shape inconsolata ligs" { var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -473,7 +479,7 @@ test "shape monaspace ligs" { var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -507,7 +513,7 @@ test "shape emoji width" { var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -547,7 +553,7 @@ test "shape emoji width long" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -586,7 +592,7 @@ test "shape variation selector VS15" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -623,7 +629,7 @@ test "shape variation selector VS16" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -657,7 +663,7 @@ test "shape with empty cells in between" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -695,7 +701,7 @@ test "shape Chinese characters" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -722,13 +728,6 @@ test "shape box glyphs" { var testdata = try testShaper(alloc); defer testdata.deinit(); - // Setup the box font - testdata.cache.group.sprite = font.sprite.Face{ - .width = 18, - .height = 36, - .thickness = 2, - }; - var buf: [32]u8 = undefined; var buf_idx: usize = 0; buf_idx += try std.unicode.utf8Encode(0x2500, buf[buf_idx..]); // horiz line @@ -742,7 +741,7 @@ test "shape box glyphs" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -779,7 +778,7 @@ test "shape selection boundary" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, terminal.Selection.init( @@ -802,7 +801,7 @@ test "shape selection boundary" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, terminal.Selection.init( @@ -825,7 +824,7 @@ test "shape selection boundary" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, terminal.Selection.init( @@ -848,7 +847,7 @@ test "shape selection boundary" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, terminal.Selection.init( @@ -871,7 +870,7 @@ test "shape selection boundary" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, terminal.Selection.init( @@ -907,7 +906,7 @@ test "shape cursor boundary" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -926,7 +925,7 @@ test "shape cursor boundary" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -945,7 +944,7 @@ test "shape cursor boundary" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -964,7 +963,7 @@ test "shape cursor boundary" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -996,7 +995,7 @@ test "shape cursor boundary and colored emoji" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -1015,7 +1014,7 @@ test "shape cursor boundary and colored emoji" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -1032,7 +1031,7 @@ test "shape cursor boundary and colored emoji" { // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -1062,7 +1061,7 @@ test "shape cell attribute change" { var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -1086,7 +1085,7 @@ test "shape cell attribute change" { var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -1111,7 +1110,7 @@ test "shape cell attribute change" { var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -1136,7 +1135,7 @@ test "shape cell attribute change" { var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -1160,7 +1159,7 @@ test "shape cell attribute change" { var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -1178,13 +1177,13 @@ test "shape cell attribute change" { const TestShaper = struct { alloc: Allocator, shaper: Shaper, - cache: *GroupCache, + grid: *SharedGrid, lib: Library, pub fn deinit(self: *TestShaper) void { self.shaper.deinit(); - self.cache.deinit(self.alloc); - self.alloc.destroy(self.cache); + self.grid.deinit(self.alloc); + self.alloc.destroy(self.grid); self.lib.deinit(); } }; @@ -1210,17 +1209,11 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper { var lib = try Library.init(); errdefer lib.deinit(); - var cache_ptr = try alloc.create(GroupCache); - errdefer alloc.destroy(cache_ptr); - cache_ptr.* = try GroupCache.init(alloc, try Group.init( - alloc, - lib, - .{ .points = 12 }, - )); - errdefer cache_ptr.*.deinit(alloc); + var c = try Collection.init(alloc); + c.load_options = .{ .library = lib }; // Setup group - _ = try cache_ptr.group.addFace(.regular, .{ .loaded = try Face.init( + _ = try c.add(alloc, .regular, .{ .loaded = try Face.init( lib, testFont, .{ .size = .{ .points = 12 } }, @@ -1228,7 +1221,7 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper { if (font.options.backend != .coretext) { // Coretext doesn't support Noto's format - _ = try cache_ptr.group.addFace(.regular, .{ .loaded = try Face.init( + _ = try c.add(alloc, .regular, .{ .loaded = try Face.init( lib, testEmoji, .{ .size = .{ .points = 12 } }, @@ -1245,21 +1238,26 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper { defer disco_it.deinit(); var face = (try disco_it.next()).?; errdefer face.deinit(); - _ = try cache_ptr.group.addFace(.regular, .{ .deferred = face }); + _ = try c.add(alloc, .regular, .{ .deferred = face }); } - _ = try cache_ptr.group.addFace(.regular, .{ .loaded = try Face.init( + _ = try c.add(alloc, .regular, .{ .loaded = try Face.init( lib, testEmojiText, .{ .size = .{ .points = 12 } }, ) }); + const grid_ptr = try alloc.create(SharedGrid); + errdefer alloc.destroy(grid_ptr); + grid_ptr.* = try SharedGrid.init(alloc, .{ .collection = c }, false); + errdefer grid_ptr.*.deinit(alloc); + var shaper = try Shaper.init(alloc, .{}); errdefer shaper.deinit(); return TestShaper{ .alloc = alloc, .shaper = shaper, - .cache = cache_ptr, + .grid = grid_ptr, .lib = lib, }; } From 45f518851d00d2954b2bf33230b80ddd09aacce1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 5 Apr 2024 22:00:51 -0700 Subject: [PATCH 36/53] renderer/opengl: convert to SharedGrid, new windows/tabs are frozen --- src/renderer/OpenGL.zig | 387 +++++++++++++++++++--------------------- 1 file changed, 186 insertions(+), 201 deletions(-) diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 47d660b34..c587795f6 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -72,8 +72,12 @@ gl_cells_written: usize = 0, gl_state: ?GLState = null, /// The font structures. -font_group: *font.GroupCache, +font_grid: *font.SharedGrid, font_shaper: font.Shaper, +texture_greyscale_modified: usize = 0, +texture_greyscale_resized: usize = 0, +texture_color_modified: usize = 0, +texture_color_resized: usize = 0, /// True if the window is focused focused: bool, @@ -333,14 +337,13 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL { }); errdefer shaper.deinit(); - // Setup our font metrics uniform - const metrics = try resetFontMetrics( - alloc, - options.font_group, - options.config.font_thicken, - ); + // For the remainder of the setup we lock our font grid data because + // we're reading it. + const grid = options.font_grid; + grid.lock.lockShared(); + defer grid.lock.unlockShared(); - var gl_state = try GLState.init(alloc, options.config, options.font_group); + var gl_state = try GLState.init(alloc, options.config, grid); errdefer gl_state.deinit(); return OpenGL{ @@ -348,10 +351,10 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL { .config = options.config, .cells_bg = .{}, .cells = .{}, - .grid_metrics = metrics, + .grid_metrics = grid.metrics, .screen_size = null, .gl_state = gl_state, - .font_group = options.font_group, + .font_grid = grid, .font_shaper = shaper, .draw_background = options.config.background, .focused = true, @@ -360,7 +363,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL { .cursor_color = options.config.cursor_color, .padding = options.padding, .surface_mailbox = options.surface_mailbox, - .deferred_font_size = .{ .metrics = metrics }, + .deferred_font_size = .{ .metrics = grid.metrics }, .deferred_config = .{}, }; } @@ -470,15 +473,16 @@ pub fn displayRealize(self: *OpenGL) !void { if (single_threaded_draw) self.draw_mutex.lock(); defer if (single_threaded_draw) self.draw_mutex.unlock(); - // Reset our GPU uniforms - const metrics = try resetFontMetrics( - self.alloc, - self.font_group, - self.config.font_thicken, - ); - // Make our new state - var gl_state = try GLState.init(self.alloc, self.config, self.font_group); + var gl_state = gl_state: { + self.font_grid.lock.lockShared(); + defer self.font_grid.lock.unlockShared(); + break :gl_state try GLState.init( + self.alloc, + self.config, + self.font_grid, + ); + }; errdefer gl_state.deinit(); // Unrealize if we have to @@ -491,14 +495,16 @@ pub fn displayRealize(self: *OpenGL) !void { // reflush everything self.gl_cells_size = 0; self.gl_cells_written = 0; - self.font_group.atlas_greyscale.modified = true; - self.font_group.atlas_color.modified = true; + self.texture_greyscale_modified = 0; + self.texture_color_modified = 0; + self.texture_greyscale_resized = 0; + self.texture_color_resized = 0; // We need to reset our uniforms if (self.screen_size) |size| { self.deferred_screen_size = .{ .size = size }; } - self.deferred_font_size = .{ .metrics = metrics }; + self.deferred_font_size = .{ .metrics = self.grid_metrics }; self.deferred_config = .{}; } @@ -585,64 +591,40 @@ pub fn setFocus(self: *OpenGL, focus: bool) !void { /// /// Must be called on the render thread. pub fn setFontSize(self: *OpenGL, size: font.face.DesiredSize) !void { - if (single_threaded_draw) self.draw_mutex.lock(); - defer if (single_threaded_draw) self.draw_mutex.unlock(); - - log.info("set font size={}", .{size}); - - // Set our new size, this will also reset our font atlas. - try self.font_group.setSize(size); - - // Reset our GPU uniforms - const metrics = try resetFontMetrics( - self.alloc, - self.font_group, - self.config.font_thicken, - ); - - // Defer our GPU updates - self.deferred_font_size = .{ .metrics = metrics }; - - // Recalculate our cell size. If it is the same as before, then we do - // nothing since the grid size couldn't have possibly changed. - if (std.meta.eql(self.grid_metrics, metrics)) return; - self.grid_metrics = metrics; - - // Notify the window that the cell size changed. - _ = self.surface_mailbox.push(.{ - .cell_size = .{ - .width = metrics.cell_width, - .height = metrics.cell_height, - }, - }, .{ .forever = {} }); -} - -/// Reload the font metrics, recalculate cell size, and send that all -/// down to the GPU. -fn resetFontMetrics( - alloc: Allocator, - font_group: *font.GroupCache, - font_thicken: bool, -) !font.face.Metrics { - // Get our cell metrics based on a regular font ascii 'M'. Why 'M'? - // Doesn't matter, any normal ASCII will do we're just trying to make - // sure we use the regular font. - const metrics = metrics: { - const index = (try font_group.indexForCodepoint(alloc, 'M', .regular, .text)).?; - const face = try font_group.group.faceFromIndex(index); - break :metrics face.metrics; - }; - log.debug("cell dimensions={}", .{metrics}); - - // Set details for our sprite font - font_group.group.sprite = font.sprite.Face{ - .width = metrics.cell_width, - .height = metrics.cell_height, - .thickness = metrics.underline_thickness * @as(u32, if (font_thicken) 2 else 1), - .underline_position = metrics.underline_position, - }; - - return metrics; + _ = self; + _ = size; + if (true) @panic("TODO"); // TODO(fontmem) + // + // if (single_threaded_draw) self.draw_mutex.lock(); + // defer if (single_threaded_draw) self.draw_mutex.unlock(); + // + // log.info("set font size={}", .{size}); + // + // // Set our new size, this will also reset our font atlas. + // try self.font_group.setSize(size); + // + // // Reset our GPU uniforms + // const metrics = try resetFontMetrics( + // self.alloc, + // self.font_group, + // self.config.font_thicken, + // ); + // + // // Defer our GPU updates + // self.deferred_font_size = .{ .metrics = metrics }; + // + // // Recalculate our cell size. If it is the same as before, then we do + // // nothing since the grid size couldn't have possibly changed. + // if (std.meta.eql(self.grid_metrics, metrics)) return; + // self.grid_metrics = metrics; + // + // // Notify the window that the cell size changed. + // _ = self.surface_mailbox.push(.{ + // .cell_size = .{ + // .width = metrics.cell_width, + // .height = metrics.cell_height, + // }, + // }, .{ .forever = {} }); } /// The primary render callback that is completely thread-safe. @@ -1056,7 +1038,7 @@ pub fn rebuildCells( // Split our row into runs and shape each one. var iter = self.font_shaper.runIterator( - self.font_group, + self.font_grid, screen, row, selection, @@ -1170,33 +1152,21 @@ fn addPreeditCell( const bg = self.foreground_color; const fg = self.background_color; - // Get the font for this codepoint. - const font_index = if (self.font_group.indexForCodepoint( + // Render the glyph for our preedit text + const render_ = self.font_grid.renderCodepoint( self.alloc, @intCast(cp.codepoint), .regular, .text, - )) |index| index orelse return else |_| return; - - // Get the font face so we can get the glyph - const face = self.font_group.group.faceFromIndex(font_index) catch |err| { - log.warn("error getting face for font_index={} err={}", .{ font_index, err }); - return; - }; - - // Use the face to now get the glyph index - const glyph_index = face.glyphIndex(@intCast(cp.codepoint)) orelse return; - - // Render the glyph for our preedit text - const glyph = self.font_group.renderGlyph( - self.alloc, - font_index, - glyph_index, .{ .grid_metrics = self.grid_metrics }, ) catch |err| { log.warn("error rendering preedit glyph err={}", .{err}); return; }; + const render = render_ orelse { + log.warn("failed to find font for preedit codepoint={X}", .{cp.codepoint}); + return; + }; // Add our opaque background cell self.cells_bg.appendAssumeCapacity(.{ @@ -1226,12 +1196,12 @@ fn addPreeditCell( .grid_col = @intCast(x), .grid_row = @intCast(y), .grid_width = if (cp.wide) 2 else 1, - .glyph_x = glyph.atlas_x, - .glyph_y = glyph.atlas_y, - .glyph_width = glyph.width, - .glyph_height = glyph.height, - .glyph_offset_x = glyph.offset_x, - .glyph_offset_y = glyph.offset_y, + .glyph_x = render.glyph.atlas_x, + .glyph_y = render.glyph.atlas_y, + .glyph_width = render.glyph.width, + .glyph_height = render.glyph.height, + .glyph_offset_x = render.glyph.offset_x, + .glyph_offset_y = render.glyph.offset_y, .r = fg.r, .g = fg.g, .b = fg.b, @@ -1275,13 +1245,13 @@ fn addCursor( .underline => .underline, }; - const glyph = self.font_group.renderGlyph( + const render = self.font_grid.renderGlyph( self.alloc, font.sprite_index, @intFromEnum(sprite), .{ - .grid_metrics = self.grid_metrics, .cell_width = if (wide) 2 else 1, + .grid_metrics = self.grid_metrics, }, ) catch |err| { log.warn("error rendering cursor glyph err={}", .{err}); @@ -1301,12 +1271,12 @@ fn addCursor( .bg_g = 0, .bg_b = 0, .bg_a = 0, - .glyph_x = glyph.atlas_x, - .glyph_y = glyph.atlas_y, - .glyph_width = glyph.width, - .glyph_height = glyph.height, - .glyph_offset_x = glyph.offset_x, - .glyph_offset_y = glyph.offset_y, + .glyph_x = render.glyph.atlas_x, + .glyph_y = render.glyph.atlas_y, + .glyph_width = render.glyph.width, + .glyph_height = render.glyph.height, + .glyph_offset_x = render.glyph.offset_x, + .glyph_offset_y = render.glyph.offset_y, }); return &self.cells.items[self.cells.items.len - 1]; @@ -1455,7 +1425,7 @@ fn updateCell( // If the cell has a character, draw it if (cell.hasText()) fg: { // Render - const glyph = try self.font_group.renderGlyph( + const render = try self.font_grid.renderGlyph( self.alloc, shaper_run.font_index, shaper_cell.glyph_index orelse break :fg, @@ -1467,9 +1437,8 @@ fn updateCell( // If we're rendering a color font, we use the color atlas const mode: CellProgram.CellMode = switch (try fgMode( - &self.font_group.group, + render.presentation, cell_pin, - shaper_run, )) { .normal => .fg, .color => .fg_color, @@ -1481,12 +1450,12 @@ fn updateCell( .grid_col = @intCast(x), .grid_row = @intCast(y), .grid_width = cell.gridWidth(), - .glyph_x = glyph.atlas_x, - .glyph_y = glyph.atlas_y, - .glyph_width = glyph.width, - .glyph_height = glyph.height, - .glyph_offset_x = glyph.offset_x + shaper_cell.x_offset, - .glyph_offset_y = glyph.offset_y + shaper_cell.y_offset, + .glyph_x = render.glyph.atlas_x, + .glyph_y = render.glyph.atlas_y, + .glyph_width = render.glyph.width, + .glyph_height = render.glyph.height, + .glyph_offset_x = render.glyph.offset_x + shaper_cell.x_offset, + .glyph_offset_y = render.glyph.offset_y + shaper_cell.y_offset, .r = colors.fg.r, .g = colors.fg.g, .b = colors.fg.b, @@ -1508,13 +1477,13 @@ fn updateCell( .curly => .underline_curly, }; - const underline_glyph = try self.font_group.renderGlyph( + const render = try self.font_grid.renderGlyph( self.alloc, font.sprite_index, @intFromEnum(sprite), .{ - .grid_metrics = self.grid_metrics, .cell_width = if (cell.wide == .wide) 2 else 1, + .grid_metrics = self.grid_metrics, }, ); @@ -1525,12 +1494,12 @@ fn updateCell( .grid_col = @intCast(x), .grid_row = @intCast(y), .grid_width = cell.gridWidth(), - .glyph_x = underline_glyph.atlas_x, - .glyph_y = underline_glyph.atlas_y, - .glyph_width = underline_glyph.width, - .glyph_height = underline_glyph.height, - .glyph_offset_x = underline_glyph.offset_x, - .glyph_offset_y = underline_glyph.offset_y, + .glyph_x = render.glyph.atlas_x, + .glyph_y = render.glyph.atlas_y, + .glyph_width = render.glyph.width, + .glyph_height = render.glyph.height, + .glyph_offset_x = render.glyph.offset_x, + .glyph_offset_y = render.glyph.offset_y, .r = color.r, .g = color.g, .b = color.b, @@ -1587,10 +1556,12 @@ pub fn changeConfig(self: *OpenGL, config: *DerivedConfig) !void { // so to be safe we just always reset it. This has a performance hit // when its not necessary but config reloading shouldn't be so // common to cause a problem. - self.font_group.reset(); - self.font_group.group.styles = config.font_styles; - self.font_group.atlas_greyscale.clear(); - self.font_group.atlas_color.clear(); + // + // TODO(fontmem): see Metal + // self.font_group.reset(); + // self.font_group.group.styles = config.font_styles; + // self.font_group.atlas_greyscale.clear(); + // self.font_group.atlas_color.clear(); // We always redo the font shaper in case font features changed. We // could check to see if there was an actual config change but this is @@ -1657,71 +1628,85 @@ pub fn setScreenSize( fn flushAtlas(self: *OpenGL) !void { const gl_state = self.gl_state orelse return; - { - const atlas = &self.font_group.atlas_greyscale; - if (atlas.modified) { - atlas.modified = false; - var texbind = try gl_state.texture.bind(.@"2D"); - defer texbind.unbind(); + texture: { + // If the texture isn't modified we do nothing + const atlas = &self.font_grid.atlas_greyscale; + const modified = atlas.modified.load(.monotonic); + if (modified <= self.texture_greyscale_modified) break :texture; + self.texture_greyscale_modified = modified; - if (atlas.resized) { - atlas.resized = false; - try texbind.image2D( - 0, - .red, - @intCast(atlas.size), - @intCast(atlas.size), - 0, - .red, - .UnsignedByte, - atlas.data.ptr, - ); - } else { - try texbind.subImage2D( - 0, - 0, - 0, - @intCast(atlas.size), - @intCast(atlas.size), - .red, - .UnsignedByte, - atlas.data.ptr, - ); - } + // If it is modified we need to grab a read-lock + self.font_grid.lock.lockShared(); + defer self.font_grid.lock.unlockShared(); + + var texbind = try gl_state.texture.bind(.@"2D"); + defer texbind.unbind(); + + const resized = atlas.resized.load(.monotonic); + if (resized > self.texture_greyscale_resized) { + self.texture_greyscale_resized = resized; + try texbind.image2D( + 0, + .red, + @intCast(atlas.size), + @intCast(atlas.size), + 0, + .red, + .UnsignedByte, + atlas.data.ptr, + ); + } else { + try texbind.subImage2D( + 0, + 0, + 0, + @intCast(atlas.size), + @intCast(atlas.size), + .red, + .UnsignedByte, + atlas.data.ptr, + ); } } - { - const atlas = &self.font_group.atlas_color; - if (atlas.modified) { - atlas.modified = false; - var texbind = try gl_state.texture_color.bind(.@"2D"); - defer texbind.unbind(); + texture: { + // If the texture isn't modified we do nothing + const atlas = &self.font_grid.atlas_color; + const modified = atlas.modified.load(.monotonic); + if (modified <= self.texture_color_modified) break :texture; + self.texture_color_modified = modified; - if (atlas.resized) { - atlas.resized = false; - try texbind.image2D( - 0, - .rgba, - @intCast(atlas.size), - @intCast(atlas.size), - 0, - .bgra, - .UnsignedByte, - atlas.data.ptr, - ); - } else { - try texbind.subImage2D( - 0, - 0, - 0, - @intCast(atlas.size), - @intCast(atlas.size), - .bgra, - .UnsignedByte, - atlas.data.ptr, - ); - } + // If it is modified we need to grab a read-lock + self.font_grid.lock.lockShared(); + defer self.font_grid.lock.unlockShared(); + + var texbind = try gl_state.texture_color.bind(.@"2D"); + defer texbind.unbind(); + + const resized = atlas.resized.load(.monotonic); + if (resized > self.texture_color_resized) { + self.texture_color_resized = resized; + try texbind.image2D( + 0, + .rgba, + @intCast(atlas.size), + @intCast(atlas.size), + 0, + .bgra, + .UnsignedByte, + atlas.data.ptr, + ); + } else { + try texbind.subImage2D( + 0, + 0, + 0, + @intCast(atlas.size), + @intCast(atlas.size), + .bgra, + .UnsignedByte, + atlas.data.ptr, + ); } } } @@ -1999,7 +1984,7 @@ const GLState = struct { pub fn init( alloc: Allocator, config: DerivedConfig, - font_group: *font.GroupCache, + font_grid: *font.SharedGrid, ) !GLState { var arena = ArenaAllocator.init(alloc); defer arena.deinit(); @@ -2045,12 +2030,12 @@ const GLState = struct { try texbind.image2D( 0, .red, - @intCast(font_group.atlas_greyscale.size), - @intCast(font_group.atlas_greyscale.size), + @intCast(font_grid.atlas_greyscale.size), + @intCast(font_grid.atlas_greyscale.size), 0, .red, .UnsignedByte, - font_group.atlas_greyscale.data.ptr, + font_grid.atlas_greyscale.data.ptr, ); } @@ -2066,12 +2051,12 @@ const GLState = struct { try texbind.image2D( 0, .rgba, - @intCast(font_group.atlas_color.size), - @intCast(font_group.atlas_color.size), + @intCast(font_grid.atlas_color.size), + @intCast(font_grid.atlas_color.size), 0, .bgra, .UnsignedByte, - font_group.atlas_color.data.ptr, + font_grid.atlas_color.data.ptr, ); } From a22ca8e4c1e54d38e88a86fdb40892ebbede0c07 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 6 Apr 2024 09:34:34 -0700 Subject: [PATCH 37/53] renderer/opengl: clean up texture sync --- src/renderer/OpenGL.zig | 142 +++++++++++++++++++--------------------- 1 file changed, 66 insertions(+), 76 deletions(-) diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index c587795f6..7e7b05ec1 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -1627,88 +1627,78 @@ pub fn setScreenSize( /// Updates the font texture atlas if it is dirty. fn flushAtlas(self: *OpenGL) !void { const gl_state = self.gl_state orelse return; + try flushAtlasSingle( + &self.font_grid.lock, + gl_state.texture, + &self.font_grid.atlas_greyscale, + &self.texture_greyscale_modified, + &self.texture_greyscale_resized, + .red, + .red, + ); + try flushAtlasSingle( + &self.font_grid.lock, + gl_state.texture_color, + &self.font_grid.atlas_color, + &self.texture_color_modified, + &self.texture_color_resized, + .rgba, + .bgra, + ); +} - texture: { - // If the texture isn't modified we do nothing - const atlas = &self.font_grid.atlas_greyscale; - const modified = atlas.modified.load(.monotonic); - if (modified <= self.texture_greyscale_modified) break :texture; - self.texture_greyscale_modified = modified; +/// Flush a single atlas, grabbing all necessary locks, checking for +/// changes, etc. +fn flushAtlasSingle( + lock: *std.Thread.RwLock, + texture: gl.Texture, + atlas: *font.Atlas, + modified: *usize, + resized: *usize, + interal_format: gl.Texture.InternalFormat, + format: gl.Texture.Format, +) !void { + // If the texture isn't modified we do nothing + const new_modified = atlas.modified.load(.monotonic); + if (new_modified <= modified.*) return; - // If it is modified we need to grab a read-lock - self.font_grid.lock.lockShared(); - defer self.font_grid.lock.unlockShared(); + // If it is modified we need to grab a read-lock + lock.lockShared(); + defer lock.unlockShared(); - var texbind = try gl_state.texture.bind(.@"2D"); - defer texbind.unbind(); + var texbind = try texture.bind(.@"2D"); + defer texbind.unbind(); - const resized = atlas.resized.load(.monotonic); - if (resized > self.texture_greyscale_resized) { - self.texture_greyscale_resized = resized; - try texbind.image2D( - 0, - .red, - @intCast(atlas.size), - @intCast(atlas.size), - 0, - .red, - .UnsignedByte, - atlas.data.ptr, - ); - } else { - try texbind.subImage2D( - 0, - 0, - 0, - @intCast(atlas.size), - @intCast(atlas.size), - .red, - .UnsignedByte, - atlas.data.ptr, - ); - } + const new_resized = atlas.resized.load(.monotonic); + if (new_resized > resized.*) { + try texbind.image2D( + 0, + interal_format, + @intCast(atlas.size), + @intCast(atlas.size), + 0, + format, + .UnsignedByte, + atlas.data.ptr, + ); + + // Only update the resized number after successful resize + resized.* = new_resized; + } else { + try texbind.subImage2D( + 0, + 0, + 0, + @intCast(atlas.size), + @intCast(atlas.size), + format, + .UnsignedByte, + atlas.data.ptr, + ); } - texture: { - // If the texture isn't modified we do nothing - const atlas = &self.font_grid.atlas_color; - const modified = atlas.modified.load(.monotonic); - if (modified <= self.texture_color_modified) break :texture; - self.texture_color_modified = modified; - - // If it is modified we need to grab a read-lock - self.font_grid.lock.lockShared(); - defer self.font_grid.lock.unlockShared(); - - var texbind = try gl_state.texture_color.bind(.@"2D"); - defer texbind.unbind(); - - const resized = atlas.resized.load(.monotonic); - if (resized > self.texture_color_resized) { - self.texture_color_resized = resized; - try texbind.image2D( - 0, - .rgba, - @intCast(atlas.size), - @intCast(atlas.size), - 0, - .bgra, - .UnsignedByte, - atlas.data.ptr, - ); - } else { - try texbind.subImage2D( - 0, - 0, - 0, - @intCast(atlas.size), - @intCast(atlas.size), - .bgra, - .UnsignedByte, - atlas.data.ptr, - ); - } - } + // Update our modified tracker after successful update + modified.* = atlas.modified.load(.monotonic); } /// Render renders the current cell state. This will not modify any of From 00f677fd51e38b0a313aefcf641467341f1a7b94 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 6 Apr 2024 10:08:52 -0700 Subject: [PATCH 38/53] font: SharedGridSet locks and is thread-safe --- src/font/SharedGridSet.zig | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/font/SharedGridSet.zig b/src/font/SharedGridSet.zig index e629b9595..4b3303ab1 100644 --- a/src/font/SharedGridSet.zig +++ b/src/font/SharedGridSet.zig @@ -5,9 +5,8 @@ //! This structure allows expensive font information such as //! the font atlas, glyph cache, font faces, etc. to be shared. //! -//! This structure itself is not thread-safe. It is expected that a single -//! main app thread handles initializing new values and dispensing them to -//! the appropriate threads. +//! This structure is thread-safe when the operations are documented +//! as thread-safe. const SharedGridSet = @This(); const std = @import("std"); @@ -44,6 +43,9 @@ font_lib: Library, /// Font discovery mechanism. font_discover: ?Discover = null, +/// Lock to protect multi-threaded access to the map. +lock: std.Thread.Mutex = .{}, + /// Initialize a new SharedGridSet. pub fn init(alloc: Allocator) !SharedGridSet { var font_lib = try Library.init(); @@ -74,7 +76,9 @@ pub fn deinit(self: *SharedGridSet) void { } /// Returns the number of cached grids. -pub fn count(self: *const SharedGridSet) usize { +pub fn count(self: *SharedGridSet) usize { + self.lock.lock(); + defer self.lock.unlock(); return self.map.count(); } @@ -94,6 +98,9 @@ pub fn ref( var key = try Key.init(self.alloc, config); errdefer key.deinit(); + self.lock.lock(); + defer self.lock.unlock(); + const gop = try self.map.getOrPut(self.alloc, key); if (gop.found_existing) { log.debug("found cached grid for font config", .{}); @@ -273,6 +280,9 @@ fn collection( /// Decrement the ref count for the given key. If the ref count is zero, /// the grid will be deinitialized and removed from the map.j:w pub fn deref(self: *SharedGridSet, key: Key) void { + self.lock.lock(); + defer self.lock.unlock(); + const entry = self.map.getEntry(key) orelse return; assert(entry.value_ptr.ref >= 1); From efb8146c286881cf3a7d432527a651dc0d850989 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 6 Apr 2024 10:20:16 -0700 Subject: [PATCH 39/53] config: RepeatableString.clone should clone all the strings too --- src/config/Config.zig | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 6ca64ca01..eb4f21d18 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2498,9 +2498,14 @@ pub const RepeatableString = struct { /// Deep copy of the struct. Required by Config. pub fn clone(self: *const Self, alloc: Allocator) !Self { - return .{ - .list = try self.list.clone(alloc), - }; + // Copy the list and all the strings in the list. + const list = try self.list.clone(alloc); + for (list.items) |*item| { + const copy = try alloc.dupeZ(u8, item.*); + item.* = copy; + } + + return .{ .list = list }; } /// The number of itemsin the list From 07a5dd044275849a16ad5fe68a4666770426687a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 6 Apr 2024 10:28:57 -0700 Subject: [PATCH 40/53] config: FontStyle.clone --- src/config/Config.zig | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/config/Config.zig b/src/config/Config.zig index eb4f21d18..34adf7b56 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -3248,6 +3248,14 @@ pub const FontStyle = union(enum) { }; } + /// Deep copy of the struct. Required by Config. + pub fn clone(self: Self, alloc: Allocator) !Self { + return switch (self) { + .default, .false => self, + .name => |v| .{ .name = try alloc.dupeZ(u8, v) }, + }; + } + /// Used by Formatter pub fn formatEntry(self: Self, formatter: anytype) !void { switch (self) { From b9efd837982d83961c2d7a30fa1d45364d5796c2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 6 Apr 2024 10:37:26 -0700 Subject: [PATCH 41/53] font: SharedGridSet uses DerivedConfig --- src/Surface.zig | 10 ++++- src/config.zig | 8 +++- src/config/Config.zig | 2 + src/font/SharedGridSet.zig | 91 +++++++++++++++++++++++++++++++++++--- 4 files changed, 100 insertions(+), 11 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 25a8b800d..187a64b83 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -205,6 +205,7 @@ const DerivedConfig = struct { confirm_close_surface: bool, cursor_click_to_move: bool, desktop_notifications: bool, + font: font.SharedGridSet.DerivedConfig, mouse_interval: u64, mouse_hide_while_typing: bool, mouse_scroll_multiplier: f64, @@ -262,6 +263,7 @@ const DerivedConfig = struct { .confirm_close_surface = config.@"confirm-close-surface", .cursor_click_to_move = config.@"cursor-click-to-move", .desktop_notifications = config.@"desktop-notifications", + .font = try font.SharedGridSet.DerivedConfig.init(alloc, config), .mouse_interval = config.@"click-repeat-interval" * 1_000_000, // 500ms .mouse_hide_while_typing = config.@"mouse-hide-while-typing", .mouse_scroll_multiplier = config.@"mouse-scroll-multiplier", @@ -297,6 +299,10 @@ pub fn init( rt_app: *apprt.runtime.App, rt_surface: *apprt.runtime.Surface, ) !void { + // Get our configuration + var derived_config = try DerivedConfig.init(alloc, config); + errdefer derived_config.deinit(); + // Initialize our renderer with our initialized surface. try Renderer.surfaceInit(rt_surface); @@ -322,7 +328,7 @@ pub fn init( // Setup our font group. This will reuse an existing font group if // it was already loaded. const font_grid_key, const font_grid = try app.font_grid_set.ref( - config, + &derived_config.font, font_size, ); @@ -427,7 +433,7 @@ pub fn init( .grid_size = .{}, .cell_size = cell_size, .padding = padding, - .config = try DerivedConfig.init(alloc, config), + .config = derived_config, }; // Report initial cell size on surface creation diff --git a/src/config.zig b/src/config.zig index 73c014a01..ba87fb6db 100644 --- a/src/config.zig +++ b/src/config.zig @@ -8,14 +8,18 @@ pub const edit = @import("config/edit.zig"); pub const url = @import("config/url.zig"); // Field types +pub const ClipboardAccess = Config.ClipboardAccess; pub const CopyOnSelect = Config.CopyOnSelect; +pub const CustomShaderAnimation = Config.CustomShaderAnimation; +pub const FontStyle = Config.FontStyle; pub const Keybinds = Config.Keybinds; pub const MouseShiftCapture = Config.MouseShiftCapture; -pub const CustomShaderAnimation = Config.CustomShaderAnimation; pub const NonNativeFullscreen = Config.NonNativeFullscreen; pub const OptionAsAlt = Config.OptionAsAlt; +pub const RepeatableCodepointMap = Config.RepeatableCodepointMap; +pub const RepeatableFontVariation = Config.RepeatableFontVariation; +pub const RepeatableString = Config.RepeatableString; pub const ShellIntegrationFeatures = Config.ShellIntegrationFeatures; -pub const ClipboardAccess = Config.ClipboardAccess; // Alternate APIs pub const CAPI = @import("config/CAPI.zig"); diff --git a/src/config/Config.zig b/src/config/Config.zig index 34adf7b56..03b6183c1 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2965,6 +2965,8 @@ pub const RepeatableCodepointMap = struct { /// Deep copy of the struct. Required by Config. pub fn clone(self: *const Self, alloc: Allocator) !Self { + // TODO(fontmem): clone the codemap descriptors + return .{ .map = .{ .list = try self.map.list.clone(alloc) }, }; diff --git a/src/font/SharedGridSet.zig b/src/font/SharedGridSet.zig index 4b3303ab1..03f805d18 100644 --- a/src/font/SharedGridSet.zig +++ b/src/font/SharedGridSet.zig @@ -92,7 +92,7 @@ pub fn count(self: *SharedGridSet) usize { /// owned by the set and will be freed when the ref count reaches zero. pub fn ref( self: *SharedGridSet, - config: *const Config, + config: *const DerivedConfig, font_size: DesiredSize, ) !struct { Key, *SharedGrid } { var key = try Key.init(self.alloc, config); @@ -340,6 +340,74 @@ const ReffedGrid = struct { ref: u32 = 0, }; +/// This is the configuration required to create a key without having +/// to keep the full Ghostty configuration around. +pub const DerivedConfig = struct { + arena: ArenaAllocator, + + @"font-family": configpkg.RepeatableString, + @"font-family-bold": configpkg.RepeatableString, + @"font-family-italic": configpkg.RepeatableString, + @"font-family-bold-italic": configpkg.RepeatableString, + @"font-style": configpkg.FontStyle, + @"font-style-bold": configpkg.FontStyle, + @"font-style-italic": configpkg.FontStyle, + @"font-style-bold-italic": configpkg.FontStyle, + @"font-size": u8, + @"font-variation": configpkg.RepeatableFontVariation, + @"font-variation-bold": configpkg.RepeatableFontVariation, + @"font-variation-italic": configpkg.RepeatableFontVariation, + @"font-variation-bold-italic": configpkg.RepeatableFontVariation, + @"font-codepoint-map": configpkg.RepeatableCodepointMap, + @"font-thicken": bool, + @"adjust-cell-width": ?Metrics.Modifier, + @"adjust-cell-height": ?Metrics.Modifier, + @"adjust-font-baseline": ?Metrics.Modifier, + @"adjust-underline-position": ?Metrics.Modifier, + @"adjust-underline-thickness": ?Metrics.Modifier, + @"adjust-strikethrough-position": ?Metrics.Modifier, + @"adjust-strikethrough-thickness": ?Metrics.Modifier, + + pub fn init(alloc_gpa: Allocator, config: *const Config) !DerivedConfig { + var arena = ArenaAllocator.init(alloc_gpa); + errdefer arena.deinit(); + const alloc = arena.allocator(); + + return .{ + .@"font-family" = try config.@"font-family".clone(alloc), + .@"font-family-bold" = try config.@"font-family-bold".clone(alloc), + .@"font-family-italic" = try config.@"font-family-italic".clone(alloc), + .@"font-family-bold-italic" = try config.@"font-family-bold-italic".clone(alloc), + .@"font-style" = try config.@"font-style".clone(alloc), + .@"font-style-bold" = try config.@"font-style-bold".clone(alloc), + .@"font-style-italic" = try config.@"font-style-italic".clone(alloc), + .@"font-style-bold-italic" = try config.@"font-style-bold-italic".clone(alloc), + .@"font-size" = config.@"font-size", + .@"font-variation" = try config.@"font-variation".clone(alloc), + .@"font-variation-bold" = try config.@"font-variation-bold".clone(alloc), + .@"font-variation-italic" = try config.@"font-variation-italic".clone(alloc), + .@"font-variation-bold-italic" = try config.@"font-variation-bold-italic".clone(alloc), + .@"font-codepoint-map" = try config.@"font-codepoint-map".clone(alloc), + .@"font-thicken" = config.@"font-thicken", + .@"adjust-cell-width" = config.@"adjust-cell-width", + .@"adjust-cell-height" = config.@"adjust-cell-height", + .@"adjust-font-baseline" = config.@"adjust-font-baseline", + .@"adjust-underline-position" = config.@"adjust-underline-position", + .@"adjust-underline-thickness" = config.@"adjust-underline-thickness", + .@"adjust-strikethrough-position" = config.@"adjust-strikethrough-position", + .@"adjust-strikethrough-thickness" = config.@"adjust-strikethrough-thickness", + + // This must be last so the arena contains all our allocations + // from above since Zig does assignment in order. + .arena = arena, + }; + } + + pub fn deinit(self: *DerivedConfig) void { + self.arena.deinit(); + } +}; + /// The key used to uniquely identify a font configuration. pub const Key = struct { arena: ArenaAllocator, @@ -376,7 +444,7 @@ pub const Key = struct { pub fn init( alloc_gpa: Allocator, - config: *const Config, + config: *const DerivedConfig, ) !Key { var arena = ArenaAllocator.init(alloc_gpa); errdefer arena.deinit(); @@ -522,7 +590,10 @@ test "Key" { var cfg = try Config.default(alloc); defer cfg.deinit(); - var k = try Key.init(alloc, &cfg); + var keycfg = try DerivedConfig.init(alloc, &cfg); + defer keycfg.deinit(); + + var k = try Key.init(alloc, &keycfg); defer k.deinit(); try testing.expect(k.hashcode() > 0); @@ -535,15 +606,21 @@ test SharedGridSet { var set = try SharedGridSet.init(alloc); defer set.deinit(); - var config = try Config.default(alloc); - defer config.deinit(); + var cfg = try Config.default(alloc); + defer cfg.deinit(); + + var keycfg = try DerivedConfig.init(alloc, &cfg); + defer keycfg.deinit(); + + var k = try Key.init(alloc, &keycfg); + defer k.deinit(); // Get a grid for the given config - const key1, const grid1 = try set.ref(&config, .{ .points = 12 }); + const key1, const grid1 = try set.ref(&keycfg, .{ .points = 12 }); try testing.expectEqual(@as(usize, 1), set.count()); // Get another - const key2, const grid2 = try set.ref(&config, .{ .points = 12 }); + const key2, const grid2 = try set.ref(&keycfg, .{ .points = 12 }); try testing.expectEqual(@as(usize, 1), set.count()); // They should be pointer equivalent From ebd31ad50fd5d89006db458ce734f02550933f80 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 6 Apr 2024 10:45:44 -0700 Subject: [PATCH 42/53] font: SharedGridSet DerivedConfig does not take font size --- src/font/SharedGridSet.zig | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/font/SharedGridSet.zig b/src/font/SharedGridSet.zig index 03f805d18..a012786c0 100644 --- a/src/font/SharedGridSet.zig +++ b/src/font/SharedGridSet.zig @@ -95,7 +95,7 @@ pub fn ref( config: *const DerivedConfig, font_size: DesiredSize, ) !struct { Key, *SharedGrid } { - var key = try Key.init(self.alloc, config); + var key = try Key.init(self.alloc, config, font_size); errdefer key.deinit(); self.lock.lock(); @@ -353,7 +353,6 @@ pub const DerivedConfig = struct { @"font-style-bold": configpkg.FontStyle, @"font-style-italic": configpkg.FontStyle, @"font-style-bold-italic": configpkg.FontStyle, - @"font-size": u8, @"font-variation": configpkg.RepeatableFontVariation, @"font-variation-bold": configpkg.RepeatableFontVariation, @"font-variation-italic": configpkg.RepeatableFontVariation, @@ -382,7 +381,6 @@ pub const DerivedConfig = struct { .@"font-style-bold" = try config.@"font-style-bold".clone(alloc), .@"font-style-italic" = try config.@"font-style-italic".clone(alloc), .@"font-style-bold-italic" = try config.@"font-style-bold-italic".clone(alloc), - .@"font-size" = config.@"font-size", .@"font-variation" = try config.@"font-variation".clone(alloc), .@"font-variation-bold" = try config.@"font-variation-bold".clone(alloc), .@"font-variation-italic" = try config.@"font-variation-italic".clone(alloc), @@ -445,6 +443,7 @@ pub const Key = struct { pub fn init( alloc_gpa: Allocator, config: *const DerivedConfig, + font_size: DesiredSize, ) !Key { var arena = ArenaAllocator.init(alloc_gpa); errdefer arena.deinit(); @@ -456,7 +455,7 @@ pub const Key = struct { try descriptors.append(.{ .family = family, .style = config.@"font-style".nameValue(), - .size = config.@"font-size", + .size = font_size.points, .variations = config.@"font-variation".list.items, }); } @@ -474,7 +473,7 @@ pub const Key = struct { try descriptors.append(.{ .family = family, .style = style, - .size = config.@"font-size", + .size = font_size.points, .bold = style == null, .variations = config.@"font-variation".list.items, }); @@ -484,7 +483,7 @@ pub const Key = struct { try descriptors.append(.{ .family = family, .style = style, - .size = config.@"font-size", + .size = font_size.points, .italic = style == null, .variations = config.@"font-variation".list.items, }); @@ -494,7 +493,7 @@ pub const Key = struct { try descriptors.append(.{ .family = family, .style = style, - .size = config.@"font-size", + .size = font_size.points, .bold = style == null, .italic = style == null, .variations = config.@"font-variation".list.items, @@ -593,7 +592,7 @@ test "Key" { var keycfg = try DerivedConfig.init(alloc, &cfg); defer keycfg.deinit(); - var k = try Key.init(alloc, &keycfg); + var k = try Key.init(alloc, &keycfg, .{ .points = 12 }); defer k.deinit(); try testing.expect(k.hashcode() > 0); @@ -612,9 +611,6 @@ test SharedGridSet { var keycfg = try DerivedConfig.init(alloc, &cfg); defer keycfg.deinit(); - var k = try Key.init(alloc, &keycfg); - defer k.deinit(); - // Get a grid for the given config const key1, const grid1 = try set.ref(&keycfg, .{ .points = 12 }); try testing.expectEqual(@as(usize, 1), set.count()); From b8d11e57c9e4b20d4ef4fa9972693bb753be9e80 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 6 Apr 2024 10:55:58 -0700 Subject: [PATCH 43/53] renderer/Metal: change font size works again --- src/Surface.zig | 19 +++++++++++++++++-- src/renderer/Metal.zig | 25 ++++++++----------------- src/renderer/Thread.zig | 9 +++++++-- src/renderer/message.zig | 19 +++++++++++++++---- 4 files changed, 47 insertions(+), 25 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 187a64b83..095c94afd 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -970,11 +970,26 @@ pub fn setFontSize(self: *Surface, size: font.face.DesiredSize) void { // Update our font size so future changes work self.font_size = size; - // Notify our render thread of the font size. This triggers everything else. + // We need to build up a new font stack for this font size. + const font_grid_key, const font_grid = self.app.font_grid_set.ref( + &self.config.font, + self.font_size, + ) catch unreachable; + errdefer self.app.font_grid_set.deref(font_grid_key); + + // Notify our render thread of the new font stack _ = self.renderer_thread.mailbox.push(.{ - .font_size = size, + .font_grid = .{ + .grid = font_grid, + .set = &self.app.font_grid_set, + .old_key = self.font_grid_key, + .new_key = font_grid_key, + }, }, .{ .forever = {} }); + // Once we've sent the key we can replace our key + self.font_grid_key = font_grid_key; + // Schedule render which also drains our mailbox self.queueRender() catch unreachable; } diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index ea94a796d..6c75f9ad2 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -566,18 +566,17 @@ pub fn setFocus(self: *Metal, focus: bool) !void { /// Set the new font size. /// /// Must be called on the render thread. -pub fn setFontSize(self: *Metal, size: font.face.DesiredSize) !void { - log.info("set font size={}", .{size}); - if (true) @panic("TODO"); // TODO(fontmem) - - // Set our new size, this will also reset our font atlas. - try self.font_group.setSize(size); +pub fn setFontGrid(self: *Metal, grid: *font.SharedGrid) !void { + // Update our grid + self.font_grid = grid; + self.texture_greyscale_modified = 0; + self.texture_color_modified = 0; // Recalculate our metrics const metrics = metrics: { - const index = (try self.font_group.indexForCodepoint(self.alloc, 'M', .regular, .text)).?; - const face = try self.font_group.group.faceFromIndex(index); - break :metrics face.metrics; + grid.lock.lockShared(); + defer grid.lock.unlockShared(); + break :metrics grid.metrics; }; // Update our uniforms @@ -597,14 +596,6 @@ pub fn setFontSize(self: *Metal, size: font.face.DesiredSize) !void { if (std.meta.eql(self.grid_metrics, metrics)) return; self.grid_metrics = metrics; - // Set the sprite font up - self.font_group.group.sprite = font.sprite.Face{ - .width = metrics.cell_width, - .height = metrics.cell_height, - .thickness = metrics.underline_thickness * @as(u32, if (self.config.font_thicken) 2 else 1), - .underline_position = metrics.underline_position, - }; - // Notify the window that the cell size changed. _ = self.surface_mailbox.push(.{ .cell_size = .{ diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index 113f6761c..d73705653 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -321,8 +321,13 @@ fn drainMailbox(self: *Thread) !void { } }, - .font_size => |size| { - try self.renderer.setFontSize(size); + .font_grid => |grid| if (self.renderer.setFontGrid(grid.grid)) { + // Success, deref our old grid + grid.set.deref(grid.old_key); + } else |err| { + // Error, deref our new grid since we didn't use it. + grid.set.deref(grid.new_key); + return err; }, .foreground_color => |color| { diff --git a/src/renderer/message.zig b/src/renderer/message.zig index b215bd4d2..c15854266 100644 --- a/src/renderer/message.zig +++ b/src/renderer/message.zig @@ -22,10 +22,21 @@ pub const Message = union(enum) { /// restarting the timer. reset_cursor_blink: void, - /// Change the font size. This should recalculate the grid size and - /// send a grid size change message back to the window thread if - /// the size changes. - font_size: font.face.DesiredSize, + /// Change the font grid. This can happen for any number of reasons + /// including a font size change, family change, etc. + font_grid: struct { + grid: *font.SharedGrid, + set: *font.SharedGridSet, + + // The key for the new grid. If adopting the new grid fails for any + // reason, the old grid should be kept but the new key should be + // dereferenced. + new_key: font.SharedGridSet.Key, + + // After accepting the new grid, the old grid must be dereferenced + // using the fields below. + old_key: font.SharedGridSet.Key, + }, /// Change the foreground color. This can be done separately from changing /// the config file in response to an OSC 10 command. From f51dad445feb2ddc41cb28ddc7d14e55034e7824 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 6 Apr 2024 10:56:21 -0700 Subject: [PATCH 44/53] core: add todo for next time --- src/Surface.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Surface.zig b/src/Surface.zig index 095c94afd..3c7b9a815 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -988,6 +988,7 @@ pub fn setFontSize(self: *Surface, size: font.face.DesiredSize) void { }, .{ .forever = {} }); // Once we've sent the key we can replace our key + // TODO(fontmem): we should not store this anymore self.font_grid_key = font_grid_key; // Schedule render which also drains our mailbox From 506ba854fa160852e0622c89f41f7463abdff1e2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 6 Apr 2024 19:33:49 -0700 Subject: [PATCH 45/53] core: font size changes work --- src/Surface.zig | 26 +++++++++++++++----------- src/apprt/glfw.zig | 2 +- src/apprt/surface.zig | 3 --- src/renderer/Metal.zig | 25 +++++-------------------- src/renderer/Options.zig | 2 +- src/renderer/Thread.zig | 8 ++------ 6 files changed, 24 insertions(+), 42 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 3c7b9a815..f142515c5 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -642,8 +642,6 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { try self.rt_surface.setMouseShape(shape); }, - .cell_size => |size| try self.setCellSize(size), - .clipboard_read => |clipboard| { if (self.config.clipboard_read == .deny) { log.info("application attempted to read clipboard, but 'clipboard-read' is set to deny", .{}); @@ -966,18 +964,25 @@ fn setCellSize(self: *Surface, size: renderer.CellSize) !void { /// Change the font size. /// /// This can only be called from the main thread. -pub fn setFontSize(self: *Surface, size: font.face.DesiredSize) void { +pub fn setFontSize(self: *Surface, size: font.face.DesiredSize) !void { // Update our font size so future changes work self.font_size = size; // We need to build up a new font stack for this font size. - const font_grid_key, const font_grid = self.app.font_grid_set.ref( + const font_grid_key, const font_grid = try self.app.font_grid_set.ref( &self.config.font, self.font_size, - ) catch unreachable; + ); errdefer self.app.font_grid_set.deref(font_grid_key); - // Notify our render thread of the new font stack + // Set our cell size + try self.setCellSize(.{ + .width = font_grid.metrics.cell_width, + .height = font_grid.metrics.cell_height, + }); + + // Notify our render thread of the new font stack. The renderer + // MUST accept the new font grid and deref the old. _ = self.renderer_thread.mailbox.push(.{ .font_grid = .{ .grid = font_grid, @@ -988,7 +993,6 @@ pub fn setFontSize(self: *Surface, size: font.face.DesiredSize) void { }, .{ .forever = {} }); // Once we've sent the key we can replace our key - // TODO(fontmem): we should not store this anymore self.font_grid_key = font_grid_key; // Schedule render which also drains our mailbox @@ -1699,7 +1703,7 @@ pub fn contentScaleCallback(self: *Surface, content_scale: apprt.ContentScale) ! return; } - self.setFontSize(size); + try self.setFontSize(size); // Update our padding which is dependent on DPI. self.padding = padding: { @@ -2998,7 +3002,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool var size = self.font_size; size.points +|= delta; - self.setFontSize(size); + try self.setFontSize(size); }, .decrease_font_size => |delta| { @@ -3006,7 +3010,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool var size = self.font_size; size.points = @max(1, size.points -| delta); - self.setFontSize(size); + try self.setFontSize(size); }, .reset_font_size => { @@ -3014,7 +3018,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool var size = self.font_size; size.points = self.config.original_font_size; - self.setFontSize(size); + try self.setFontSize(size); }, .clear_screen => { diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 55e8069e7..932e27de5 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -246,7 +246,7 @@ pub const App = struct { // If we have a parent, inherit some properties if (self.config.@"window-inherit-font-size") { if (parent_) |parent| { - surface.core_surface.setFontSize(parent.font_size); + try surface.core_surface.setFontSize(parent.font_size); } } diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index 3060b7a5c..ae3ba050a 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -21,9 +21,6 @@ pub const Message = union(enum) { /// Set the mouse shape. set_mouse_shape: terminal.MouseShape, - /// Change the cell size. - cell_size: renderer.CellSize, - /// Read the clipboard and write to the pty. clipboard_read: apprt.Clipboard, diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 6c75f9ad2..e9465db40 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -566,18 +566,16 @@ pub fn setFocus(self: *Metal, focus: bool) !void { /// Set the new font size. /// /// Must be called on the render thread. -pub fn setFontGrid(self: *Metal, grid: *font.SharedGrid) !void { +pub fn setFontGrid(self: *Metal, grid: *font.SharedGrid) void { // Update our grid self.font_grid = grid; self.texture_greyscale_modified = 0; self.texture_color_modified = 0; - // Recalculate our metrics - const metrics = metrics: { - grid.lock.lockShared(); - defer grid.lock.unlockShared(); - break :metrics grid.metrics; - }; + // Get our metrics from the grid. This doesn't require a lock because + // the metrics are never recalculated. + const metrics = grid.metrics; + self.grid_metrics = metrics; // Update our uniforms self.uniforms = .{ @@ -590,19 +588,6 @@ pub fn setFontGrid(self: *Metal, grid: *font.SharedGrid) !void { .strikethrough_thickness = @floatFromInt(metrics.strikethrough_thickness), .min_contrast = self.uniforms.min_contrast, }; - - // Recalculate our cell size. If it is the same as before, then we do - // nothing since the grid size couldn't have possibly changed. - if (std.meta.eql(self.grid_metrics, metrics)) return; - self.grid_metrics = metrics; - - // Notify the window that the cell size changed. - _ = self.surface_mailbox.push(.{ - .cell_size = .{ - .width = metrics.cell_width, - .height = metrics.cell_height, - }, - }, .{ .forever = {} }); } /// Update the frame data. diff --git a/src/renderer/Options.zig b/src/renderer/Options.zig index fd7b9d714..8c68affe8 100644 --- a/src/renderer/Options.zig +++ b/src/renderer/Options.zig @@ -8,7 +8,7 @@ const Config = @import("../config.zig").Config; /// The derived configuration for this renderer implementation. config: renderer.Renderer.DerivedConfig, -/// The font grid that should be used. +/// The font grid that should be used along with the key for deref-ing. font_grid: *font.SharedGrid, /// Padding options for the viewport. diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index d73705653..91a213132 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -321,13 +321,9 @@ fn drainMailbox(self: *Thread) !void { } }, - .font_grid => |grid| if (self.renderer.setFontGrid(grid.grid)) { - // Success, deref our old grid + .font_grid => |grid| { + self.renderer.setFontGrid(grid.grid); grid.set.deref(grid.old_key); - } else |err| { - // Error, deref our new grid since we didn't use it. - grid.set.deref(grid.new_key); - return err; }, .foreground_color => |color| { From 3afeac99e90e5733e63aa365e52a6b4081083968 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 6 Apr 2024 19:35:56 -0700 Subject: [PATCH 46/53] renderer/opengl: support font size change --- src/renderer/OpenGL.zig | 42 +++++++---------------------------------- 1 file changed, 7 insertions(+), 35 deletions(-) diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 7e7b05ec1..13329a727 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -590,41 +590,13 @@ pub fn setFocus(self: *OpenGL, focus: bool) !void { /// Set the new font size. /// /// Must be called on the render thread. -pub fn setFontSize(self: *OpenGL, size: font.face.DesiredSize) !void { - _ = self; - _ = size; - if (true) @panic("TODO"); // TODO(fontmem) - // - // if (single_threaded_draw) self.draw_mutex.lock(); - // defer if (single_threaded_draw) self.draw_mutex.unlock(); - // - // log.info("set font size={}", .{size}); - // - // // Set our new size, this will also reset our font atlas. - // try self.font_group.setSize(size); - // - // // Reset our GPU uniforms - // const metrics = try resetFontMetrics( - // self.alloc, - // self.font_group, - // self.config.font_thicken, - // ); - // - // // Defer our GPU updates - // self.deferred_font_size = .{ .metrics = metrics }; - // - // // Recalculate our cell size. If it is the same as before, then we do - // // nothing since the grid size couldn't have possibly changed. - // if (std.meta.eql(self.grid_metrics, metrics)) return; - // self.grid_metrics = metrics; - // - // // Notify the window that the cell size changed. - // _ = self.surface_mailbox.push(.{ - // .cell_size = .{ - // .width = metrics.cell_width, - // .height = metrics.cell_height, - // }, - // }, .{ .forever = {} }); +pub fn setFontGrid(self: *OpenGL, grid: *font.SharedGrid) void { + if (single_threaded_draw) self.draw_mutex.lock(); + defer if (single_threaded_draw) self.draw_mutex.unlock(); + + // Defer our GPU updates + self.deferred_font_size = .{ .metrics = grid.metrics }; + self.grid_metrics = grid.metrics; } /// The primary render callback that is completely thread-safe. From 84f30a60644196d6da825ea1a1d1a63f9c646ba0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 6 Apr 2024 19:36:59 -0700 Subject: [PATCH 47/53] apprt/gtk: setFontsize should have error --- src/apprt/gtk/Surface.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 34108a6f1..c7aaf9343 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -416,7 +416,7 @@ fn realize(self: *Surface) !void { // If we have a font size we want, set that now if (self.font_size) |size| { - self.core_surface.setFontSize(size); + try self.core_surface.setFontSize(size); } // Set the intial color scheme From 21605eaab759f848cc70d1b9e3e8cfca69d98809 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 6 Apr 2024 19:37:29 -0700 Subject: [PATCH 48/53] apprt/embedded: setfontsize --- src/apprt/embedded.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 741cf7cc3..d9f9d35f2 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -414,7 +414,7 @@ pub const Surface = struct { if (opts.font_size != 0) { var font_size = self.core_surface.font_size; font_size.points = opts.font_size; - self.core_surface.setFontSize(font_size); + try self.core_surface.setFontSize(font_size); } } From 2f61f7d6a398b6bf9caf2ce485e3686cca37705e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 6 Apr 2024 19:49:14 -0700 Subject: [PATCH 49/53] font: fix hashing of descriptor strings --- src/font/SharedGridSet.zig | 2 +- src/font/discovery.zig | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/font/SharedGridSet.zig b/src/font/SharedGridSet.zig index a012786c0..8295993a3 100644 --- a/src/font/SharedGridSet.zig +++ b/src/font/SharedGridSet.zig @@ -556,7 +556,7 @@ pub const Key = struct { const autoHash = std.hash.autoHash; autoHash(hasher, self.descriptors.len); for (self.descriptors) |d| d.hash(hasher); - autoHash(hasher, self.codepoint_map); + self.codepoint_map.hash(hasher); autoHash(hasher, self.metric_modifiers.count()); if (self.metric_modifiers.count() > 0) { inline for (@typeInfo(Metrics.Key).Enum.fields) |field| { diff --git a/src/font/discovery.zig b/src/font/discovery.zig index c926445fd..13a994f76 100644 --- a/src/font/discovery.zig +++ b/src/font/discovery.zig @@ -60,8 +60,9 @@ pub const Descriptor = struct { /// Hash the descriptor with the given hasher. pub fn hash(self: Descriptor, hasher: anytype) void { const autoHash = std.hash.autoHash; - autoHash(hasher, self.family); - autoHash(hasher, self.style); + const autoHashStrat = std.hash.autoHashStrat; + autoHashStrat(hasher, self.family, .Deep); + autoHashStrat(hasher, self.style, .Deep); autoHash(hasher, self.codepoint); autoHash(hasher, self.size); autoHash(hasher, self.bold); From 3b0b9c2cfd84c49edbbacf2c91faefcadbf4a998 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 6 Apr 2024 19:55:00 -0700 Subject: [PATCH 50/53] renderer/opengl: set the font grid --- src/renderer/OpenGL.zig | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 13329a727..83eb2d752 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -594,9 +594,16 @@ pub fn setFontGrid(self: *OpenGL, grid: *font.SharedGrid) void { if (single_threaded_draw) self.draw_mutex.lock(); defer if (single_threaded_draw) self.draw_mutex.unlock(); + // Reset our font grid + self.font_grid = grid; + self.grid_metrics = grid.metrics; + self.texture_greyscale_modified = 0; + self.texture_greyscale_resized = 0; + self.texture_color_modified = 0; + self.texture_color_resized = 0; + // Defer our GPU updates self.deferred_font_size = .{ .metrics = grid.metrics }; - self.grid_metrics = grid.metrics; } /// The primary render callback that is completely thread-safe. From e3402cef4dcc4e61d68fd8c2cc652063414882b2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 6 Apr 2024 20:00:22 -0700 Subject: [PATCH 51/53] address many fontmem todos --- src/font/SharedGrid.zig | 3 --- src/font/face/Metrics.zig | 10 ++++------ src/renderer/Metal.zig | 15 --------------- src/renderer/OpenGL.zig | 12 ------------ 4 files changed, 4 insertions(+), 36 deletions(-) diff --git a/src/font/SharedGrid.zig b/src/font/SharedGrid.zig index 3f4d3599a..03f364570 100644 --- a/src/font/SharedGrid.zig +++ b/src/font/SharedGrid.zig @@ -18,9 +18,6 @@ //! reinitialized and all surfaces should switch over to using that one. const SharedGrid = @This(); -// TODO(fontmem): -// - consider config changes and how they affect the shared grid. - const std = @import("std"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; diff --git a/src/font/face/Metrics.zig b/src/font/face/Metrics.zig index 621f0ddbf..df96d5a6d 100644 --- a/src/font/face/Metrics.zig +++ b/src/font/face/Metrics.zig @@ -174,12 +174,10 @@ pub const Modifier = union(enum) { const autoHash = std.hash.autoHash; autoHash(hasher, std.meta.activeTag(self)); switch (self) { - // floats can't be hashed directly so we round it to the - // nearest int and then hash that. This is not perfect but - // hash collisions due to the modifier being wrong are really - // rare so we should fix this up later. - // TODO(fontmem): make better - .percent => |v| autoHash(hasher, @as(i64, @intFromFloat(v))), + // floats can't be hashed directly so we bitcast to i64. + // for the purpose of what we're trying to do this seems + // good enough but I would prefer value hashing. + .percent => |v| autoHash(hasher, @as(i64, @bitCast(v))), .absolute => |v| autoHash(hasher, v), } } diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index e9465db40..a5e87b59b 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -1363,21 +1363,6 @@ fn prepKittyGraphics( /// Update the configuration. pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void { - // On configuration change we always reset our font group. There - // are a variety of configurations that can change font settings - // so to be safe we just always reset it. This has a performance hit - // when its not necessary but config reloading shouldn't be so - // common to cause a problem. - // - // TODO(fontmem): we no longer do this. the surface should handle - // font changes, create a new grid for us, and send it via message - // passing or something. - // - // self.font_group.reset(); - // self.font_group.group.styles = config.font_styles; - // self.font_group.atlas_greyscale.clear(); - // self.font_group.atlas_color.clear(); - // We always redo the font shaper in case font features changed. We // could check to see if there was an actual config change but this is // easier and rare enough to not cause performance issues. diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 83eb2d752..d6669ccfa 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -1530,18 +1530,6 @@ fn gridSize(self: *const OpenGL, screen_size: renderer.ScreenSize) renderer.Grid /// Update the configuration. pub fn changeConfig(self: *OpenGL, config: *DerivedConfig) !void { - // On configuration change we always reset our font group. There - // are a variety of configurations that can change font settings - // so to be safe we just always reset it. This has a performance hit - // when its not necessary but config reloading shouldn't be so - // common to cause a problem. - // - // TODO(fontmem): see Metal - // self.font_group.reset(); - // self.font_group.group.styles = config.font_styles; - // self.font_group.atlas_greyscale.clear(); - // self.font_group.atlas_color.clear(); - // We always redo the font shaper in case font features changed. We // could check to see if there was an actual config change but this is // easier and rare enough to not cause performance issues. From 06df9b78679e42aff28aaa359ae1b587dd66337e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 6 Apr 2024 20:10:57 -0700 Subject: [PATCH 52/53] font: remove old files --- src/font/Group.zig | 1179 ------------------------------------ src/font/GroupCache.zig | 381 ------------ src/font/GroupCacheSet.zig | 518 ---------------- src/font/main.zig | 5 - src/renderer/Metal.zig | 4 +- src/renderer/OpenGL.zig | 4 +- 6 files changed, 4 insertions(+), 2087 deletions(-) delete mode 100644 src/font/Group.zig delete mode 100644 src/font/GroupCache.zig delete mode 100644 src/font/GroupCacheSet.zig diff --git a/src/font/Group.zig b/src/font/Group.zig deleted file mode 100644 index 4637be3d9..000000000 --- a/src/font/Group.zig +++ /dev/null @@ -1,1179 +0,0 @@ -//! A font group is a a set of multiple font faces of potentially different -//! styles that are used together to find glyphs. They usually share sizing -//! properties so that they can be used interchangeably with each other in cases -//! a codepoint doesn't map cleanly. For example, if a user requests a bold -//! char and it doesn't exist we can fallback to a regular non-bold char so -//! we show SOMETHING. -//! -//! Note this is made specifically for terminals so it has some features -//! that aren't generally helpful, such as detecting and drawing the terminal -//! box glyphs and requiring cell sizes for such glyphs. -const Group = @This(); - -const std = @import("std"); -const assert = std.debug.assert; -const Allocator = std.mem.Allocator; -const ziglyph = @import("ziglyph"); - -const font = @import("main.zig"); -const DeferredFace = @import("main.zig").DeferredFace; -const Face = @import("main.zig").Face; -const Library = @import("main.zig").Library; -const Glyph = @import("main.zig").Glyph; -const Style = @import("main.zig").Style; -const Presentation = @import("main.zig").Presentation; -const options = @import("main.zig").options; -const quirks = @import("../quirks.zig"); - -const log = std.log.scoped(.font_group); - -/// Packed array to map our styles to a set of faces. -// Note: this is not the most efficient way to store these, but there is -// usually only one font group for the entire process so this isn't the -// most important memory efficiency we can look for. This is totally opaque -// to the user so we can change this later. -const StyleArray = std.EnumArray(Style, std.ArrayListUnmanaged(GroupFace)); - -/// Packed array of booleans to indicate if a style is enabled or not. -pub const StyleStatus = std.EnumArray(Style, bool); - -/// Map of descriptors to faces. This is used with manual codepoint maps -/// to ensure that we don't load the same font multiple times. -/// -/// Note that the current implementation will load the same font multiple -/// times if the font used for a codepoint map is identical to a font used -/// for a regular style. That's just an inefficient choice made now because -/// the implementation is simpler and codepoint maps matching a regular -/// font is a rare case. -const DescriptorCache = std.HashMapUnmanaged( - font.discovery.Descriptor, - ?FontIndex, - struct { - const KeyType = font.discovery.Descriptor; - - pub fn hash(ctx: @This(), k: KeyType) u64 { - _ = ctx; - return k.hashcode(); - } - - pub fn eql(ctx: @This(), a: KeyType, b: KeyType) bool { - return ctx.hash(a) == ctx.hash(b); - } - }, - std.hash_map.default_max_load_percentage, -); - -/// The requested presentation for a codepoint. -const PresentationMode = union(enum) { - /// The codepoint has an explicit presentation that is required, - /// i.e. VS15/V16. - explicit: Presentation, - - /// The codepoint has no explicit presentation and we should use - /// the presentation from the UCd. - default: Presentation, - - /// The codepoint can be any presentation. - any: void, -}; - -/// The allocator for this group -alloc: Allocator, - -/// The library being used for all the faces. -lib: Library, - -/// The desired font size. All fonts in a group must share the same size. -size: font.face.DesiredSize, - -/// Metric modifiers to apply to loaded fonts. The Group takes ownership -/// over the memory and will use the associated allocator to free it. -metric_modifiers: ?font.face.Metrics.ModifierSet = null, - -/// The available faces we have. This shouldn't be modified manually. -/// Instead, use the functions available on Group. -faces: StyleArray, - -/// The set of statuses and whether they're enabled or not. This defaults -/// to true. This can be changed at runtime with no ill effect. If you -/// change this at runtime and are using a GroupCache, the GroupCache -/// must be reset. -styles: StyleStatus = StyleStatus.initFill(true), - -/// If discovery is available, we'll look up fonts where we can't find -/// the codepoint. This can be set after initialization. -discover: ?*font.Discover = null, - -/// A map of codepoints to font requests for codepoint-level overrides. -/// The memory associated with the map is owned by the caller and is not -/// modified or freed by Group. -codepoint_map: ?font.CodepointMap = null, - -/// The descriptor cache is used to cache the descriptor to font face -/// mapping for codepoint maps. -descriptor_cache: DescriptorCache = .{}, - -/// Set this to a non-null value to enable sprite glyph drawing. If this -/// isn't enabled we'll just fall through to trying to use regular fonts -/// to render sprite glyphs. But more than likely, if this isn't set then -/// terminal rendering will look wrong. -sprite: ?font.sprite.Face = null, - -/// A face in a group can be deferred or loaded. A deferred face -/// is not yet fully loaded and only represents the font descriptor -/// and usually uses less resources. A loaded face is fully parsed, -/// ready to rasterize, and usually uses more resources than a -/// deferred version. -/// -/// A face can also be a "fallback" variant that is still either -/// deferred or loaded. Today, there is only one different between -/// fallback and non-fallback (or "explicit") faces: the handling -/// of emoji presentation. -/// -/// For explicit faces, when an explicit emoji presentation is -/// not requested, we will use any glyph for that codepoint found -/// even if the font presentation does not match the UCD -/// (Unicode Character Database) value. When an explicit presentation -/// is requested (via either VS15/V16), that is always honored. -/// The reason we do this is because we assume that if a user -/// explicitly chosen a font face (hence it is "explicit" and -/// not "fallback"), they want to use any glyphs possible within that -/// font face. Fallback fonts on the other hand are picked as a -/// last resort, so we should prefer exactness if possible. -pub const GroupFace = union(enum) { - deferred: DeferredFace, // Not loaded - loaded: Face, // Loaded, explicit use - - // The same as deferred/loaded but fallback font semantics (see large - // comment above GroupFace). - fallback_deferred: DeferredFace, - fallback_loaded: Face, - - pub fn deinit(self: *GroupFace) void { - switch (self.*) { - inline .deferred, - .loaded, - .fallback_deferred, - .fallback_loaded, - => |*v| v.deinit(), - } - } - - /// True if this face satisfies the given codepoint and presentation. - fn hasCodepoint(self: GroupFace, cp: u32, p_mode: PresentationMode) bool { - return switch (self) { - // Non-fallback fonts require explicit presentation matching but - // otherwise don't care about presentation - .deferred => |v| switch (p_mode) { - .explicit => |p| v.hasCodepoint(cp, p), - .default, .any => v.hasCodepoint(cp, null), - }, - - .loaded => |face| switch (p_mode) { - .explicit => |p| face.presentation == p and face.glyphIndex(cp) != null, - .default, .any => face.glyphIndex(cp) != null, - }, - - // Fallback fonts require exact presentation matching. - .fallback_deferred => |v| switch (p_mode) { - .explicit, .default => |p| v.hasCodepoint(cp, p), - .any => v.hasCodepoint(cp, null), - }, - - .fallback_loaded => |face| switch (p_mode) { - .explicit, - .default, - => |p| face.presentation == p and face.glyphIndex(cp) != null, - .any => face.glyphIndex(cp) != null, - }, - }; - } -}; - -/// Initializes an empty group. This is not useful until faces are added -/// and finalizeInit is called. -pub fn init( - alloc: Allocator, - lib: Library, - size: font.face.DesiredSize, -) !Group { - var result = Group{ .alloc = alloc, .lib = lib, .size = size, .faces = undefined }; - - // Initialize all our styles to initially sized lists. - var i: usize = 0; - while (i < StyleArray.len) : (i += 1) { - result.faces.values[i] = .{}; - try result.faces.values[i].ensureTotalCapacityPrecise(alloc, 2); - } - - return result; -} - -pub fn deinit(self: *Group) void { - { - var it = self.faces.iterator(); - while (it.next()) |entry| { - for (entry.value.items) |*item| item.deinit(); - entry.value.deinit(self.alloc); - } - } - - if (self.metric_modifiers) |*v| v.deinit(self.alloc); - - self.descriptor_cache.deinit(self.alloc); -} - -/// Returns the options for initializing a face based on the options associated -/// with this font group. -pub fn faceOptions(self: *const Group) font.face.Options { - return .{ - .size = self.size, - .metric_modifiers = if (self.metric_modifiers) |*v| v else null, - }; -} - -/// Add a face to the list for the given style. This face will be added as -/// next in priority if others exist already, i.e. it'll be the _last_ to be -/// searched for a glyph in that list. -/// -/// The group takes ownership of the face. The face will be deallocated when -/// the group is deallocated. -pub fn addFace(self: *Group, style: Style, face: GroupFace) !FontIndex { - const list = self.faces.getPtr(style); - - // We have some special indexes so we must never pass those. - if (list.items.len >= FontIndex.Special.start - 1) return error.GroupFull; - - const idx = list.items.len; - try list.append(self.alloc, face); - return .{ .style = style, .idx = @intCast(idx) }; -} - -/// This will automatically create an italicized font from the regular -/// font face if we don't have any italicized fonts. -pub fn italicize(self: *Group) !void { - // If we have an italic font, do nothing. - const italic_list = self.faces.getPtr(.italic); - if (italic_list.items.len > 0) return; - - // Not all font backends support auto-italicization. - if (comptime !@hasDecl(Face, "italicize")) { - log.warn("no italic font face available, italics will not render", .{}); - return; - } - - // Our regular font. If we have no regular font we also do nothing. - const regular = regular: { - const list = self.faces.get(.regular); - if (list.items.len == 0) return; - - // Find our first font that is text. - for (0..list.items.len) |i| { - const face = try self.faceFromIndex(.{ - .style = .regular, - .idx = @intCast(i), - }); - if (face.presentation == .text) break :regular face; - } - - return; - }; - - // Try to italicize it. - const face = try regular.italicize(self.faceOptions()); - try italic_list.append(self.alloc, .{ .loaded = face }); - - var buf: [128]u8 = undefined; - if (face.name(&buf)) |name| { - log.info("font auto-italicized: {s}", .{name}); - } else |_| {} -} - -/// Resize the fonts to the desired size. -pub fn setSize(self: *Group, size: font.face.DesiredSize) !void { - // Note: there are some issues here with partial failure. We don't - // currently handle it in any meaningful way if one face can resize - // but another can't. - - // Set our size for future loads - self.size = size; - - // Resize all our faces that are loaded - var it = self.faces.iterator(); - while (it.next()) |entry| { - for (entry.value.items) |*elem| switch (elem.*) { - .deferred, .fallback_deferred => continue, - .loaded, .fallback_loaded => |*f| try f.setSize(self.faceOptions()), - }; - } -} - -/// This represents a specific font in the group. -pub const FontIndex = packed struct(FontIndex.Backing) { - const Backing = u16; - const backing_bits = @typeInfo(Backing).Int.bits; - - /// The number of bits we use for the index. - const idx_bits = backing_bits - @typeInfo(@typeInfo(Style).Enum.tag_type).Int.bits; - pub const IndexInt = @Type(.{ .Int = .{ .signedness = .unsigned, .bits = idx_bits } }); - - /// The special-case fonts that we support. - pub const Special = enum(IndexInt) { - // We start all special fonts at this index so they can be detected. - pub const start = std.math.maxInt(IndexInt); - - /// Sprite drawing, this is rendered JIT using 2D graphics APIs. - sprite = start, - }; - - style: Style = .regular, - idx: IndexInt = 0, - - /// Initialize a special font index. - pub fn initSpecial(v: Special) FontIndex { - return .{ .style = .regular, .idx = @intFromEnum(v) }; - } - - /// Convert to int - pub fn int(self: FontIndex) Backing { - return @bitCast(self); - } - - /// Returns true if this is a "special" index which doesn't map to - /// a real font face. We can still render it but there is no face for - /// this font. - pub fn special(self: FontIndex) ?Special { - if (self.idx < Special.start) return null; - return @enumFromInt(self.idx); - } - - test { - // We never want to take up more than a byte since font indexes are - // everywhere so if we increase the size of this we'll dramatically - // increase our memory usage. - try std.testing.expectEqual(@sizeOf(Backing), @sizeOf(FontIndex)); - - // Just so we're aware when this changes. The current maximum number - // of fonts for a style is 13 bits or 8192 fonts. - try std.testing.expectEqual(13, idx_bits); - } -}; - -/// Looks up the font that should be used for a specific codepoint. -/// The font index is valid as long as font faces aren't removed. This -/// isn't cached; it is expected that downstream users handle caching if -/// that is important. -/// -/// Optionally, a presentation format can be specified. This presentation -/// format will be preferred but if it can't be found in this format, -/// any format will be accepted. If presentation is null, the UCD -/// (Unicode Character Database) will be used to determine the default -/// presentation for the codepoint. -/// a code point. -/// -/// This logic is relatively complex so the exact algorithm is documented -/// here. If this gets out of sync with the code, ask questions. -/// -/// 1. If a font style is requested that is disabled (i.e. bold), -/// we start over with the regular font style. The regular font style -/// cannot be disabled, but it can be replaced with a stylized font -/// face. -/// -/// 2. If there is a codepoint override for the codepoint, we satisfy -/// that requirement if we can, no matter what style or presentation. -/// -/// 3. If this is a sprite codepoint (such as an underline), then the -/// sprite font always is the result. -/// -/// 4. If the exact style and presentation request can be satisfied by -/// one of our loaded fonts, we return that value. We search loaded -/// fonts in the order they're added to the group, so the caller must -/// set the priority order. -/// -/// 5. If the style isn't regular, we restart this process at this point -/// but with the regular style. This lets us fall back to regular with -/// our loaded fonts before trying a fallback. We'd rather show a regular -/// version of a codepoint from a loaded font than find a new font in -/// the correct style because styles in other fonts often change -/// metrics like glyph widths. -/// -/// 6. If the style is regular, and font discovery is enabled, we look -/// for a fallback font to satisfy our request. -/// -/// 7. Finally, as a last resort, we fall back to restarting this whole -/// process with a regular font face satisfying ANY presentation for -/// the codepoint. If this fails, we return null. -/// -pub fn indexForCodepoint( - self: *Group, - cp: u32, - style: Style, - p: ?Presentation, -) ?FontIndex { - // If we've disabled a font style, then fall back to regular. - if (style != .regular and !self.styles.get(style)) { - return self.indexForCodepoint(cp, .regular, p); - } - - // Codepoint overrides. - if (self.indexForCodepointOverride(cp)) |idx_| { - if (idx_) |idx| return idx; - } else |err| { - log.warn("codepoint override failed codepoint={} err={}", .{ cp, err }); - } - - // If we have sprite drawing enabled, check if our sprite face can - // handle this. - if (self.sprite) |sprite| { - if (sprite.hasCodepoint(cp, p)) { - return FontIndex.initSpecial(.sprite); - } - } - - // Build our presentation mode. If we don't have an explicit presentation - // given then we use the UCD (Unicode Character Database) to determine - // the default presentation. Note there is some inefficiency here because - // we'll do this muliple times if we recurse, but this is a cached function - // call higher up (GroupCache) so this should be rare. - const p_mode: PresentationMode = if (p) |v| .{ .explicit = v } else .{ - .default = if (ziglyph.emoji.isEmojiPresentation(@intCast(cp))) - .emoji - else - .text, - }; - - // If we can find the exact value, then return that. - if (self.indexForCodepointExact(cp, style, p_mode)) |value| return value; - - // If we're not a regular font style, try looking for a regular font - // that will satisfy this request. Blindly looking for unmatched styled - // fonts to satisfy one codepoint results in some ugly rendering. - if (style != .regular) { - if (self.indexForCodepoint(cp, .regular, p)) |value| return value; - } - - // If we are regular, try looking for a fallback using discovery. - if (style == .regular and font.Discover != void) { - log.debug("searching for a fallback font for cp={x}", .{cp}); - if (self.discover) |disco| discover: { - var disco_it = disco.discover(self.alloc, .{ - .codepoint = cp, - .size = self.size.points, - .bold = style == .bold or style == .bold_italic, - .italic = style == .italic or style == .bold_italic, - .monospace = false, - }) catch break :discover; - defer disco_it.deinit(); - - while (true) { - var deferred_face = (disco_it.next() catch |err| { - log.warn("fallback search failed with error err={}", .{err}); - break; - }) orelse break; - - // Discovery is supposed to only return faces that have our - // codepoint but we can't search presentation in discovery so - // we have to check it here. - const face: GroupFace = .{ .fallback_deferred = deferred_face }; - if (!face.hasCodepoint(cp, p_mode)) { - deferred_face.deinit(); - continue; - } - - var buf: [256]u8 = undefined; - log.info("found codepoint 0x{x} in fallback face={s}", .{ - cp, - deferred_face.name(&buf) catch "", - }); - return self.addFace(style, face) catch { - deferred_face.deinit(); - break :discover; - }; - } - - log.debug("no fallback face found for cp={x}", .{cp}); - } - } - - // If this is already regular, we're done falling back. - if (style == .regular and p == null) return null; - - // For non-regular fonts, we fall back to regular with any presentation - return self.indexForCodepointExact(cp, .regular, .{ .any = {} }); -} - -fn indexForCodepointExact( - self: Group, - cp: u32, - style: Style, - p_mode: PresentationMode, -) ?FontIndex { - for (self.faces.get(style).items, 0..) |elem, i| { - if (elem.hasCodepoint(cp, p_mode)) { - return FontIndex{ - .style = style, - .idx = @intCast(i), - }; - } - } - - // Not found - return null; -} - -/// Checks if the codepoint is in the map of codepoint overrides, -/// finds the override font, and returns it. -fn indexForCodepointOverride(self: *Group, cp: u32) !?FontIndex { - if (comptime font.Discover == void) return null; - const map = self.codepoint_map orelse return null; - - // If we have a codepoint too large or isn't in the map, then we - // don't have an override. - const cp_u21 = std.math.cast(u21, cp) orelse return null; - const desc = map.get(cp_u21) orelse return null; - - // Fast path: the descriptor is already loaded. - const idx_: ?FontIndex = self.descriptor_cache.get(desc) orelse idx: { - // Slow path: we have to find this descriptor and load the font - const discover = self.discover orelse return null; - var disco_it = try discover.discover(self.alloc, desc); - defer disco_it.deinit(); - - const face = (try disco_it.next()) orelse { - log.warn( - "font lookup for codepoint map failed codepoint={} err=FontNotFound", - .{cp}, - ); - - // Add null to the cache so we don't do a lookup again later. - try self.descriptor_cache.put(self.alloc, desc, null); - return null; - }; - - // Add the font to our list of fonts so we can get an index for it, - // and ensure the index is stored in the descriptor cache for next time. - const idx = try self.addFace(.regular, .{ .deferred = face }); - try self.descriptor_cache.put(self.alloc, desc, idx); - - break :idx idx; - }; - - // The descriptor cache will populate null if the descriptor is not found - // to avoid expensive discoveries later. - const idx = idx_ orelse return null; - - // We need to verify that this index has the codepoint we want. - if (self.hasCodepoint(idx, cp, null)) { - log.debug("codepoint override based on config codepoint={} family={s}", .{ - cp, - desc.family orelse "", - }); - - return idx; - } - - return null; -} - -/// Check if a specific font index has a specific codepoint. This does not -/// necessarily force the font to load. The presentation value "p" will -/// verify the Emoji representation matches if it is non-null. If "p" is -/// null then any presentation will be accepted. -pub fn hasCodepoint(self: *Group, index: FontIndex, cp: u32, p: ?Presentation) bool { - const list = self.faces.getPtr(index.style); - if (index.idx >= list.items.len) return false; - return list.items[index.idx].hasCodepoint( - cp, - if (p) |v| .{ .explicit = v } else .{ .any = {} }, - ); -} - -/// Returns the presentation for a specific font index. This is useful for -/// determining what atlas is needed. -pub fn presentationFromIndex(self: *Group, index: FontIndex) !font.Presentation { - if (index.special()) |sp| switch (sp) { - .sprite => return .text, - }; - - const face = try self.faceFromIndex(index); - return face.presentation; -} - -/// Return the Face represented by a given FontIndex. Note that special -/// fonts (i.e. box glyphs) do not have a face. The returned face pointer is -/// only valid until the set of faces change. -pub fn faceFromIndex(self: *Group, index: FontIndex) !*Face { - if (index.special() != null) return error.SpecialHasNoFace; - const list = self.faces.getPtr(index.style); - const item = &list.items[index.idx]; - return switch (item.*) { - inline .deferred, .fallback_deferred => |*d, tag| deferred: { - const face = try d.load(self.lib, self.faceOptions()); - d.deinit(); - item.* = switch (tag) { - .deferred => .{ .loaded = face }, - .fallback_deferred => .{ .fallback_loaded = face }, - else => unreachable, - }; - - break :deferred switch (tag) { - .deferred => &item.loaded, - .fallback_deferred => &item.fallback_loaded, - else => unreachable, - }; - }, - - .loaded, .fallback_loaded => |*f| f, - }; -} - -/// Render a glyph by glyph index into the given font atlas and return -/// metadata about it. -/// -/// This performs no caching, it is up to the caller to cache calls to this -/// if they want. This will also not resize the atlas if it is full. -/// -/// IMPORTANT: this renders by /glyph index/ and not by /codepoint/. The caller -/// is expected to translate codepoints to glyph indexes in some way. The most -/// trivial way to do this is to get the Face and call glyphIndex. If you're -/// doing text shaping, the text shaping library (i.e. HarfBuzz) will automatically -/// determine glyph indexes for a text run. -pub fn renderGlyph( - self: *Group, - alloc: Allocator, - atlas: *font.Atlas, - index: FontIndex, - glyph_index: u32, - opts: font.face.RenderOptions, -) !Glyph { - // Special-case fonts are rendered directly. - if (index.special()) |sp| switch (sp) { - .sprite => return try self.sprite.?.renderGlyph( - alloc, - atlas, - glyph_index, - opts, - ), - }; - - const face = try self.faceFromIndex(index); - const glyph = try face.renderGlyph(alloc, atlas, glyph_index, opts); - // log.warn("GLYPH={}", .{glyph}); - return glyph; -} - -/// The wasm-compatible API. -pub const Wasm = struct { - const wasm = @import("../os/wasm.zig"); - const alloc = wasm.alloc; - - export fn group_new(pts: u16) ?*Group { - return group_new_(pts) catch null; - } - - fn group_new_(pts: u16) !*Group { - var group = try Group.init(alloc, .{}, .{ .points = pts }); - errdefer group.deinit(); - - const result = try alloc.create(Group); - errdefer alloc.destroy(result); - result.* = group; - return result; - } - - export fn group_free(ptr: ?*Group) void { - if (ptr) |v| { - v.deinit(); - alloc.destroy(v); - } - } - - export fn group_init_sprite_face(self: *Group) void { - return group_init_sprite_face_(self) catch |err| { - log.warn("error initializing sprite face err={}", .{err}); - return; - }; - } - - fn group_init_sprite_face_(self: *Group) !void { - const metrics = metrics: { - const index = self.indexForCodepoint('M', .regular, .text).?; - const face = try self.faceFromIndex(index); - break :metrics face.metrics; - }; - - // Set details for our sprite font - self.sprite = font.sprite.Face{ - .width = metrics.cell_width, - .height = metrics.cell_height, - .thickness = 2, - .underline_position = metrics.underline_position, - }; - } - - export fn group_add_face(self: *Group, style: u16, face: *font.DeferredFace) void { - return self.addFace(@enumFromInt(style), face.*) catch |err| { - log.warn("error adding face to group err={}", .{err}); - return; - }; - } - - export fn group_set_size(self: *Group, size: u16) void { - return self.setSize(.{ .points = size }) catch |err| { - log.warn("error setting group size err={}", .{err}); - return; - }; - } - - /// Presentation is negative for doesn't matter. - export fn group_index_for_codepoint(self: *Group, cp: u32, style: u16, p: i16) i16 { - const presentation: ?Presentation = if (p < 0) null else @enumFromInt(p); - const idx = self.indexForCodepoint( - cp, - @enumFromInt(style), - presentation, - ) orelse return -1; - return @intCast(@as(u8, @bitCast(idx))); - } - - export fn group_render_glyph( - self: *Group, - atlas: *font.Atlas, - idx: i16, - cp: u32, - max_height: u16, - ) ?*Glyph { - return group_render_glyph_(self, atlas, idx, cp, max_height) catch |err| { - log.warn("error rendering group glyph err={}", .{err}); - return null; - }; - } - - fn group_render_glyph_( - self: *Group, - atlas: *font.Atlas, - idx_: i16, - cp: u32, - max_height_: u16, - ) !*Glyph { - const idx = @as(FontIndex, @bitCast(@as(u8, @intCast(idx_)))); - const max_height = if (max_height_ <= 0) null else max_height_; - const glyph = try self.renderGlyph(alloc, atlas, idx, cp, .{ - .max_height = max_height, - }); - - const result = try alloc.create(Glyph); - errdefer alloc.destroy(result); - result.* = glyph; - return result; - } -}; - -// X -test { - const testing = std.testing; - const alloc = testing.allocator; - const testFont = @import("test.zig").fontRegular; - const testEmoji = @import("test.zig").fontEmoji; - const testEmojiText = @import("test.zig").fontEmojiText; - - var atlas_greyscale = try font.Atlas.init(alloc, 512, .greyscale); - defer atlas_greyscale.deinit(alloc); - - var lib = try Library.init(); - defer lib.deinit(); - - var group = try init(alloc, lib, .{ .points = 12 }); - defer group.deinit(); - - _ = try group.addFace( - .regular, - .{ .loaded = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } }) }, - ); - - if (font.options.backend != .coretext) { - // Coretext doesn't support Noto's format - _ = try group.addFace( - .regular, - .{ .loaded = try Face.init(lib, testEmoji, .{ .size = .{ .points = 12 } }) }, - ); - } - _ = try group.addFace( - .regular, - .{ .loaded = try Face.init(lib, testEmojiText, .{ .size = .{ .points = 12 } }) }, - ); - - // Should find all visible ASCII - var i: u32 = 32; - while (i < 127) : (i += 1) { - const idx = group.indexForCodepoint(i, .regular, null).?; - try testing.expectEqual(Style.regular, idx.style); - try testing.expectEqual(@as(FontIndex.IndexInt, 0), idx.idx); - - // Render it - const face = try group.faceFromIndex(idx); - const glyph_index = face.glyphIndex(i).?; - _ = try group.renderGlyph( - alloc, - &atlas_greyscale, - idx, - glyph_index, - .{}, - ); - } - - // Try emoji - { - const idx = group.indexForCodepoint('🥸', .regular, null).?; - try testing.expectEqual(Style.regular, idx.style); - try testing.expectEqual(@as(FontIndex.IndexInt, 1), idx.idx); - } - - // Try text emoji - { - const idx = group.indexForCodepoint(0x270C, .regular, .text).?; - try testing.expectEqual(Style.regular, idx.style); - const text_idx = if (font.options.backend == .coretext) 1 else 2; - try testing.expectEqual(@as(FontIndex.IndexInt, text_idx), idx.idx); - } - { - const idx = group.indexForCodepoint(0x270C, .regular, .emoji).?; - try testing.expectEqual(Style.regular, idx.style); - try testing.expectEqual(@as(FontIndex.IndexInt, 1), idx.idx); - } - - // Box glyph should be null since we didn't set a box font - { - try testing.expect(group.indexForCodepoint(0x1FB00, .regular, null) == null); - } -} - -// X -test "disabled font style" { - const testing = std.testing; - const alloc = testing.allocator; - const testFont = @import("test.zig").fontRegular; - - var atlas_greyscale = try font.Atlas.init(alloc, 512, .greyscale); - defer atlas_greyscale.deinit(alloc); - - var lib = try Library.init(); - defer lib.deinit(); - - var group = try init(alloc, lib, .{ .points = 12 }); - defer group.deinit(); - - // Disable bold - group.styles.set(.bold, false); - - // Same font but we can test the style in the index - const opts: font.face.Options = .{ .size = .{ .points = 12 } }; - _ = try group.addFace(.regular, .{ .loaded = try Face.init(lib, testFont, opts) }); - _ = try group.addFace(.bold, .{ .loaded = try Face.init(lib, testFont, opts) }); - _ = try group.addFace(.italic, .{ .loaded = try Face.init(lib, testFont, opts) }); - - // Regular should work fine - { - const idx = group.indexForCodepoint('A', .regular, null).?; - try testing.expectEqual(Style.regular, idx.style); - try testing.expectEqual(@as(FontIndex.IndexInt, 0), idx.idx); - } - - // Bold should go to regular - { - const idx = group.indexForCodepoint('A', .bold, null).?; - try testing.expectEqual(Style.regular, idx.style); - try testing.expectEqual(@as(FontIndex.IndexInt, 0), idx.idx); - } - - // Italic should still work - { - const idx = group.indexForCodepoint('A', .italic, null).?; - try testing.expectEqual(Style.italic, idx.style); - try testing.expectEqual(@as(FontIndex.IndexInt, 0), idx.idx); - } -} - -// X -test "face count limit" { - const testing = std.testing; - const alloc = testing.allocator; - const testFont = @import("test.zig").fontRegular; - - var atlas_greyscale = try font.Atlas.init(alloc, 512, .greyscale); - defer atlas_greyscale.deinit(alloc); - - var lib = try Library.init(); - defer lib.deinit(); - - const opts: font.face.Options = .{ .size = .{ .points = 12 } }; - var group = try init(alloc, lib, opts.size); - defer group.deinit(); - - for (0..FontIndex.Special.start - 1) |_| { - _ = try group.addFace(.regular, .{ .loaded = try Face.init(lib, testFont, opts) }); - } - - try testing.expectError(error.GroupFull, group.addFace( - .regular, - .{ .loaded = try Face.init(lib, testFont, opts) }, - )); -} - -// X -test "box glyph" { - const testing = std.testing; - const alloc = testing.allocator; - - var atlas_greyscale = try font.Atlas.init(alloc, 512, .greyscale); - defer atlas_greyscale.deinit(alloc); - - var lib = try Library.init(); - defer lib.deinit(); - - var group = try init(alloc, lib, .{ .points = 12 }); - defer group.deinit(); - - // Set box font - group.sprite = font.sprite.Face{ .width = 18, .height = 36, .thickness = 2 }; - - // Should find a box glyph - const idx = group.indexForCodepoint(0x2500, .regular, null).?; - try testing.expectEqual(Style.regular, idx.style); - try testing.expectEqual(@intFromEnum(FontIndex.Special.sprite), idx.idx); - - // Should render it - const glyph = try group.renderGlyph( - alloc, - &atlas_greyscale, - idx, - 0x2500, - .{}, - ); - try testing.expectEqual(@as(u32, 36), glyph.height); -} - -test "resize" { - const testing = std.testing; - const alloc = testing.allocator; - const testFont = @import("test.zig").fontRegular; - - var atlas_greyscale = try font.Atlas.init(alloc, 512, .greyscale); - defer atlas_greyscale.deinit(alloc); - - var lib = try Library.init(); - defer lib.deinit(); - - var group = try init(alloc, lib, .{ .points = 12, .xdpi = 96, .ydpi = 96 }); - defer group.deinit(); - - _ = try group.addFace(.regular, .{ .loaded = try Face.init( - lib, - testFont, - .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, - ) }); - - // Load a letter - { - const idx = group.indexForCodepoint('A', .regular, null).?; - const face = try group.faceFromIndex(idx); - const glyph_index = face.glyphIndex('A').?; - const glyph = try group.renderGlyph( - alloc, - &atlas_greyscale, - idx, - glyph_index, - .{}, - ); - - try testing.expectEqual(@as(u32, 11), glyph.height); - } - - // Resize - try group.setSize(.{ .points = 24, .xdpi = 96, .ydpi = 96 }); - { - const idx = group.indexForCodepoint('A', .regular, null).?; - const face = try group.faceFromIndex(idx); - const glyph_index = face.glyphIndex('A').?; - const glyph = try group.renderGlyph( - alloc, - &atlas_greyscale, - idx, - glyph_index, - .{}, - ); - - try testing.expectEqual(@as(u32, 21), glyph.height); - } -} - -test "discover monospace with fontconfig and freetype" { - if (options.backend != .fontconfig_freetype) return error.SkipZigTest; - - const testing = std.testing; - const alloc = testing.allocator; - const Discover = @import("main.zig").Discover; - - // Search for fonts - var fc = Discover.init(); - var it = try fc.discover(alloc, .{ .family = "monospace", .size = 12 }); - defer it.deinit(); - - // Initialize the group with the deferred face - var lib = try Library.init(); - defer lib.deinit(); - var group = try init(alloc, lib, .{ .points = 12 }); - defer group.deinit(); - _ = try group.addFace(.regular, .{ .deferred = (try it.next()).? }); - - // Should find all visible ASCII - var atlas_greyscale = try font.Atlas.init(alloc, 512, .greyscale); - defer atlas_greyscale.deinit(alloc); - var i: u32 = 32; - while (i < 127) : (i += 1) { - const idx = group.indexForCodepoint(i, .regular, null).?; - try testing.expectEqual(Style.regular, idx.style); - try testing.expectEqual(@as(FontIndex.IndexInt, 0), idx.idx); - - // Render it - const face = try group.faceFromIndex(idx); - const glyph_index = face.glyphIndex(i).?; - _ = try group.renderGlyph( - alloc, - &atlas_greyscale, - idx, - glyph_index, - .{}, - ); - } -} - -test "faceFromIndex returns pointer" { - const testing = std.testing; - const alloc = testing.allocator; - const testFont = @import("test.zig").fontRegular; - - var atlas_greyscale = try font.Atlas.init(alloc, 512, .greyscale); - defer atlas_greyscale.deinit(alloc); - - var lib = try Library.init(); - defer lib.deinit(); - - var group = try init(alloc, lib, .{ .points = 12, .xdpi = 96, .ydpi = 96 }); - defer group.deinit(); - - _ = try group.addFace(.regular, .{ .loaded = try Face.init( - lib, - testFont, - .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, - ) }); - - { - const idx = group.indexForCodepoint('A', .regular, null).?; - const face1 = try group.faceFromIndex(idx); - const face2 = try group.faceFromIndex(idx); - try testing.expectEqual(@intFromPtr(face1), @intFromPtr(face2)); - } -} - -test "indexFromCodepoint: prefer emoji in non-fallback font" { - // CoreText can't load Noto - if (font.options.backend == .coretext) return error.SkipZigTest; - - const testing = std.testing; - const alloc = testing.allocator; - const testCozette = @import("test.zig").fontCozette; - const testEmoji = @import("test.zig").fontEmoji; - - var atlas_greyscale = try font.Atlas.init(alloc, 512, .greyscale); - defer atlas_greyscale.deinit(alloc); - - var lib = try Library.init(); - defer lib.deinit(); - - var group = try init(alloc, lib, .{ .points = 12 }); - defer group.deinit(); - - _ = try group.addFace( - .regular, - .{ .loaded = try Face.init( - lib, - testCozette, - .{ .size = .{ .points = 12 } }, - ) }, - ); - _ = try group.addFace( - .regular, - .{ .fallback_loaded = try Face.init( - lib, - testEmoji, - .{ .size = .{ .points = 12 } }, - ) }, - ); - - // The "alien" emoji is default emoji presentation but present - // in Cozette as text. Since Cozette isn't a fallback, we shoulod - // load it from there. - { - const idx = group.indexForCodepoint(0x1F47D, .regular, null).?; - try testing.expectEqual(Style.regular, idx.style); - try testing.expectEqual(@as(FontIndex.IndexInt, 0), idx.idx); - } - - // If we specifically request emoji, we should find it in the fallback. - { - const idx = group.indexForCodepoint(0x1F47D, .regular, .emoji).?; - try testing.expectEqual(Style.regular, idx.style); - try testing.expectEqual(@as(FontIndex.IndexInt, 1), idx.idx); - } -} - -test "indexFromCodepoint: prefer emoji with correct presentation" { - // CoreText can't load Noto - if (font.options.backend == .coretext) return error.SkipZigTest; - - const testing = std.testing; - const alloc = testing.allocator; - const testCozette = @import("test.zig").fontCozette; - const testEmoji = @import("test.zig").fontEmoji; - - var atlas_greyscale = try font.Atlas.init(alloc, 512, .greyscale); - defer atlas_greyscale.deinit(alloc); - - var lib = try Library.init(); - defer lib.deinit(); - - var group = try init(alloc, lib, .{ .points = 12 }); - defer group.deinit(); - - _ = try group.addFace( - .regular, - .{ .loaded = try Face.init( - lib, - testEmoji, - .{ .size = .{ .points = 12 } }, - ) }, - ); - _ = try group.addFace( - .regular, - .{ .loaded = try Face.init( - lib, - testCozette, - .{ .size = .{ .points = 12 } }, - ) }, - ); - - // Check that we check the default presentation - { - const idx = group.indexForCodepoint(0x1F47D, .regular, null).?; - try testing.expectEqual(Style.regular, idx.style); - try testing.expectEqual(@as(FontIndex.IndexInt, 0), idx.idx); - } - - // If we specifically request text - { - const idx = group.indexForCodepoint(0x1F47D, .regular, .text).?; - try testing.expectEqual(Style.regular, idx.style); - try testing.expectEqual(@as(FontIndex.IndexInt, 1), idx.idx); - } -} diff --git a/src/font/GroupCache.zig b/src/font/GroupCache.zig deleted file mode 100644 index 99e5548bf..000000000 --- a/src/font/GroupCache.zig +++ /dev/null @@ -1,381 +0,0 @@ -//! A glyph cache sits on top of a Group and caches the results from it. -const GroupCache = @This(); - -const std = @import("std"); -const assert = std.debug.assert; -const Allocator = std.mem.Allocator; - -const font = @import("main.zig"); -const Face = @import("main.zig").Face; -const DeferredFace = @import("main.zig").DeferredFace; -const Library = @import("main.zig").Library; -const Glyph = @import("main.zig").Glyph; -const Style = @import("main.zig").Style; -const Group = @import("main.zig").Group; -const Presentation = @import("main.zig").Presentation; - -const log = std.log.scoped(.font_groupcache); - -/// Cache for codepoints to font indexes in a group. -codepoints: std.AutoHashMapUnmanaged(CodepointKey, ?Group.FontIndex) = .{}, - -/// Cache for glyph renders. -glyphs: std.AutoHashMapUnmanaged(GlyphKey, Glyph) = .{}, - -/// The underlying font group. Users are expected to use this directly -/// to setup the group or make changes. Beware some changes require a reset -/// (see reset). -group: Group, - -/// The texture atlas to store renders in. The GroupCache has to store these -/// because the cached Glyph result is dependent on the Atlas. -atlas_greyscale: font.Atlas, -atlas_color: font.Atlas, - -const CodepointKey = struct { - style: Style, - codepoint: u32, - presentation: ?Presentation, -}; - -const GlyphKey = struct { - index: Group.FontIndex, - glyph: u32, - opts: font.face.RenderOptions, -}; - -/// The GroupCache takes ownership of Group and will free it. -pub fn init(alloc: Allocator, group: Group) !GroupCache { - var atlas_greyscale = try font.Atlas.init(alloc, 512, .greyscale); - errdefer atlas_greyscale.deinit(alloc); - var atlas_color = try font.Atlas.init(alloc, 512, .rgba); - errdefer atlas_color.deinit(alloc); - - var result: GroupCache = .{ - .group = group, - .atlas_greyscale = atlas_greyscale, - .atlas_color = atlas_color, - }; - - // We set an initial capacity that can fit a good number of characters. - // This number was picked empirically based on my own terminal usage. - try result.codepoints.ensureTotalCapacity(alloc, 128); - try result.glyphs.ensureTotalCapacity(alloc, 128); - - return result; -} - -pub fn deinit(self: *GroupCache, alloc: Allocator) void { - self.codepoints.deinit(alloc); - self.glyphs.deinit(alloc); - self.atlas_greyscale.deinit(alloc); - self.atlas_color.deinit(alloc); - self.group.deinit(); -} - -/// Reset the cache. This should be called: -/// -/// - If an Atlas was reset -/// - If a font group font size was changed -/// - If a font group font set was changed -/// -pub fn reset(self: *GroupCache) void { - self.codepoints.clearRetainingCapacity(); - self.glyphs.clearRetainingCapacity(); -} - -/// Resize the fonts in the group. This will clear the cache. -pub fn setSize(self: *GroupCache, size: font.face.DesiredSize) !void { - try self.group.setSize(size); - - // Reset our internal state - self.reset(); - - // Clear our atlases - self.atlas_greyscale.clear(); - self.atlas_color.clear(); -} - -/// Get the font index for a given codepoint. This is cached. -pub fn indexForCodepoint( - self: *GroupCache, - alloc: Allocator, - cp: u32, - style: Style, - p: ?Presentation, -) !?Group.FontIndex { - const key: CodepointKey = .{ .style = style, .codepoint = cp, .presentation = p }; - const gop = try self.codepoints.getOrPut(alloc, key); - - // If it is in the cache, use it. - if (gop.found_existing) return gop.value_ptr.*; - - // Load a value and cache it. This even caches negative matches. - const value = self.group.indexForCodepoint(cp, style, p); - gop.value_ptr.* = value; - return value; -} - -/// Render a glyph. This automatically determines the correct texture -/// atlas to use and caches the result. -pub fn renderGlyph( - self: *GroupCache, - alloc: Allocator, - index: Group.FontIndex, - glyph_index: u32, - opts: font.face.RenderOptions, -) !Glyph { - const key: GlyphKey = .{ .index = index, .glyph = glyph_index, .opts = opts }; - const gop = try self.glyphs.getOrPut(alloc, key); - - // If it is in the cache, use it. - if (gop.found_existing) return gop.value_ptr.*; - - // Uncached, render it - const atlas: *font.Atlas = switch (try self.group.presentationFromIndex(index)) { - .text => &self.atlas_greyscale, - .emoji => &self.atlas_color, - }; - const glyph = self.group.renderGlyph( - alloc, - atlas, - index, - glyph_index, - opts, - ) catch |err| switch (err) { - // If the atlas is full, we resize it - error.AtlasFull => blk: { - try atlas.grow(alloc, atlas.size * 2); - break :blk try self.group.renderGlyph( - alloc, - atlas, - index, - glyph_index, - opts, - ); - }, - - else => return err, - }; - - // Cache and return - gop.value_ptr.* = glyph; - return glyph; -} - -test { - const testing = std.testing; - const alloc = testing.allocator; - const testFont = @import("test.zig").fontRegular; - // const testEmoji = @import("test.zig").fontEmoji; - - var atlas_greyscale = try font.Atlas.init(alloc, 512, .greyscale); - defer atlas_greyscale.deinit(alloc); - - var lib = try Library.init(); - defer lib.deinit(); - - var cache = try init(alloc, try Group.init( - alloc, - lib, - .{ .points = 12 }, - )); - defer cache.deinit(alloc); - - // Setup group - _ = try cache.group.addFace( - .regular, - .{ .loaded = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } }) }, - ); - var group = cache.group; - - // Visible ASCII. Do it twice to verify cache. - var i: u32 = 32; - while (i < 127) : (i += 1) { - const idx = (try cache.indexForCodepoint(alloc, i, .regular, null)).?; - try testing.expectEqual(Style.regular, idx.style); - try testing.expectEqual(@as(Group.FontIndex.IndexInt, 0), idx.idx); - - // Render - const face = try cache.group.faceFromIndex(idx); - const glyph_index = face.glyphIndex(i).?; - _ = try cache.renderGlyph( - alloc, - idx, - glyph_index, - .{}, - ); - } - - // Do it again, but reset the group so that we know for sure its not hitting it - { - cache.group = undefined; - defer cache.group = group; - - i = 32; - while (i < 127) : (i += 1) { - const idx = (try cache.indexForCodepoint(alloc, i, .regular, null)).?; - try testing.expectEqual(Style.regular, idx.style); - try testing.expectEqual(@as(Group.FontIndex.IndexInt, 0), idx.idx); - - // Render - const face = try group.faceFromIndex(idx); - const glyph_index = face.glyphIndex(i).?; - _ = try cache.renderGlyph( - alloc, - idx, - glyph_index, - .{}, - ); - } - } -} - -/// The wasm-compatible API. -pub const Wasm = struct { - const wasm = @import("../os/wasm.zig"); - const alloc = wasm.alloc; - - export fn group_cache_new(group: *Group) ?*GroupCache { - return group_cache_new_(group) catch null; - } - - fn group_cache_new_(group: *Group) !*GroupCache { - var gc = try GroupCache.init(alloc, group.*); - errdefer gc.deinit(alloc); - - const result = try alloc.create(GroupCache); - errdefer alloc.destroy(result); - result.* = gc; - return result; - } - - export fn group_cache_free(ptr: ?*GroupCache) void { - if (ptr) |v| { - v.deinit(alloc); - alloc.destroy(v); - } - } - - export fn group_cache_set_size(self: *GroupCache, size: u16) void { - return self.setSize(.{ .points = size }) catch |err| { - log.warn("error setting group cache size err={}", .{err}); - return; - }; - } - - /// Presentation is negative for doesn't matter. - export fn group_cache_index_for_codepoint(self: *GroupCache, cp: u32, style: u16, p: i16) i16 { - const presentation: ?Presentation = if (p < 0) null else @enumFromInt(p); - if (self.indexForCodepoint( - alloc, - cp, - @enumFromInt(style), - presentation, - )) |idx| { - return @intCast(@as(u8, @bitCast(idx orelse return -1))); - } else |err| { - log.warn("error getting index for codepoint from group cache size err={}", .{err}); - return -1; - } - } - - export fn group_cache_render_glyph( - self: *GroupCache, - idx: i16, - cp: u32, - max_height: u16, - ) ?*Glyph { - return group_cache_render_glyph_(self, idx, cp, max_height) catch |err| { - log.warn("error rendering group cache glyph err={}", .{err}); - return null; - }; - } - - fn group_cache_render_glyph_( - self: *GroupCache, - idx_: i16, - cp: u32, - max_height_: u16, - ) !*Glyph { - const idx = @as(Group.FontIndex, @bitCast(@as(u8, @intCast(idx_)))); - const max_height = if (max_height_ <= 0) null else max_height_; - const glyph = try self.renderGlyph(alloc, idx, cp, .{ - .max_height = max_height, - }); - - const result = try alloc.create(Glyph); - errdefer alloc.destroy(result); - result.* = glyph; - return result; - } - - export fn group_cache_atlas_greyscale(self: *GroupCache) *font.Atlas { - return &self.atlas_greyscale; - } - - export fn group_cache_atlas_color(self: *GroupCache) *font.Atlas { - return &self.atlas_color; - } -}; - -test "resize" { - const testing = std.testing; - const alloc = testing.allocator; - const testFont = @import("test.zig").fontRegular; - // const testEmoji = @import("test.zig").fontEmoji; - - var atlas_greyscale = try font.Atlas.init(alloc, 512, .greyscale); - defer atlas_greyscale.deinit(alloc); - - var lib = try Library.init(); - defer lib.deinit(); - - var cache = try init(alloc, try Group.init( - alloc, - lib, - .{ .points = 12 }, - )); - defer cache.deinit(alloc); - - // Setup group - _ = try cache.group.addFace( - .regular, - .{ .loaded = try Face.init( - lib, - testFont, - .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, - ) }, - ); - - // Load a letter - { - const idx = (try cache.indexForCodepoint(alloc, 'A', .regular, null)).?; - const face = try cache.group.faceFromIndex(idx); - const glyph_index = face.glyphIndex('A').?; - const glyph = try cache.renderGlyph( - alloc, - idx, - glyph_index, - .{}, - ); - - try testing.expectEqual(@as(u32, 11), glyph.height); - } - - // Resize - try cache.setSize(.{ .points = 24, .xdpi = 96, .ydpi = 96 }); - { - const idx = (try cache.indexForCodepoint(alloc, 'A', .regular, null)).?; - const face = try cache.group.faceFromIndex(idx); - const glyph_index = face.glyphIndex('A').?; - const glyph = try cache.renderGlyph( - alloc, - idx, - glyph_index, - .{}, - ); - - try testing.expectEqual(@as(u32, 21), glyph.height); - } -} diff --git a/src/font/GroupCacheSet.zig b/src/font/GroupCacheSet.zig deleted file mode 100644 index 818e13e91..000000000 --- a/src/font/GroupCacheSet.zig +++ /dev/null @@ -1,518 +0,0 @@ -//! This structure contains a set of GroupCache instances keyed by -//! unique font configuration. -//! -//! Most terminals (surfaces) will share the same font configuration. -//! This structure allows expensive font information such as -//! the font atlas, glyph cache, font faces, etc. to be shared. -//! -//! The Ghostty renderers which use this information run on their -//! own threads so this structure is thread-safe. It expects that -//! the case where all glyphs are cached is the common case and -//! optimizes for that. When a glyph is not cached, all renderers -//! that share the same font configuration will be blocked until -//! the glyph is cached. -const GroupCacheSet = @This(); - -const std = @import("std"); -const builtin = @import("builtin"); -const assert = std.debug.assert; -const Allocator = std.mem.Allocator; -const ArenaAllocator = std.heap.ArenaAllocator; -const fontpkg = @import("main.zig"); -const Collection = fontpkg.Collection; -const Discover = fontpkg.Discover; -const Style = fontpkg.Style; -const Library = fontpkg.Library; -const Metrics = fontpkg.face.Metrics; -const CodepointMap = fontpkg.CodepointMap; -const DesiredSize = fontpkg.face.DesiredSize; -const Face = fontpkg.Face; -const Group = fontpkg.Group; -const GroupCache = fontpkg.GroupCache; -const discovery = @import("discovery.zig"); -const configpkg = @import("../config.zig"); -const Config = configpkg.Config; - -const log = std.log.scoped(.font_group_cache_set); - -/// The allocator to use for all heap allocations. -alloc: Allocator, - -/// The map of font configurations to GroupCache instances. -map: Map = .{}, - -/// The font library that is used for all font groups. -font_lib: Library, - -/// Font discovery mechanism. -font_discover: ?Discover = null, - -/// Initialize a new GroupCacheSet. -pub fn init(alloc: Allocator) !GroupCacheSet { - var font_lib = try Library.init(); - errdefer font_lib.deinit(); - - return .{ - .alloc = alloc, - .map = .{}, - .font_lib = font_lib, - }; -} - -pub fn deinit(self: *GroupCacheSet) void { - var it = self.map.iterator(); - while (it.next()) |entry| { - entry.key_ptr.deinit(); - const ref = entry.value_ptr.*; - ref.cache.deinit(self.alloc); - self.alloc.destroy(ref.cache); - } - self.map.deinit(self.alloc); - - if (comptime Discover != void) { - if (self.font_discover) |*v| v.deinit(); - } - - self.font_lib.deinit(); -} - -/// Initialize a GroupCache for the given font configuration. If the -/// GroupCache is not present it will be initialized with a ref count of -/// 1. If it is present, the ref count will be incremented. -/// -/// This is NOT thread-safe. -pub fn groupRef( - self: *GroupCacheSet, - config: *const Config, - font_size: DesiredSize, -) !struct { Key, *GroupCache } { - var key = try Key.init(self.alloc, config); - errdefer key.deinit(); - - const gop = try self.map.getOrPut(self.alloc, key); - if (gop.found_existing) { - log.debug("found cached GroupCache for font config", .{}); - - // We can deinit the key because we found a cached value. - key.deinit(); - - // Increment our ref count and return the cache - gop.value_ptr.ref += 1; - return .{ gop.key_ptr.*, gop.value_ptr.cache }; - } - errdefer self.map.removeByPtr(gop.key_ptr); - - log.debug("initializing new GroupCache for font config", .{}); - - // A new font config, initialize the cache. - const cache = try self.alloc.create(GroupCache); - errdefer self.alloc.destroy(cache); - gop.value_ptr.* = .{ - .cache = cache, - .ref = 1, - }; - - // Build our font face collection that we'll use to initialize - // the Group. - - cache.* = try GroupCache.init(self.alloc, group: { - var group = try Group.init( - self.alloc, - self.font_lib, - font_size, - try self.collection(&key, font_size), - ); - errdefer group.deinit(); - group.metric_modifiers = key.metric_modifiers; - group.codepoint_map = key.codepoint_map; - group.discover = try self.discover(); - - // Set our styles - group.styles.set(.bold, config.@"font-style-bold" != .false); - group.styles.set(.italic, config.@"font-style-italic" != .false); - group.styles.set(.bold_italic, config.@"font-style-bold-italic" != .false); - - // Auto-italicize if we have to. - try group.italicize(); - - log.info("font loading complete, any non-logged faces are using the built-in font", .{}); - break :group group; - }); - errdefer cache.deinit(self.alloc); - - return .{ gop.key_ptr.*, gop.value_ptr.cache }; -} - -/// Builds the Collection for the given configuration key and -/// initial font size. -fn collection( - self: *GroupCacheSet, - key: *const Key, - size: DesiredSize, -) !Collection { - var c = try Collection.init(self.alloc); - errdefer c.deinit(self.alloc); - - const opts: fontpkg.face.Options = .{ - .size = size, - .metric_modifiers = &key.metric_modifiers, - }; - - // Search for fonts - if (Discover != void) discover: { - const disco = try self.discover() orelse { - log.warn( - "font discovery not available, cannot search for fonts", - .{}, - ); - break :discover; - }; - - // A buffer we use to store the font names for logging. - var name_buf: [256]u8 = undefined; - - inline for (@typeInfo(Style).Enum.fields) |field| { - const style = @field(Style, field.name); - for (key.descriptorsForStyle(style)) |desc| { - var disco_it = try disco.discover(self.alloc, desc); - defer disco_it.deinit(); - if (try disco_it.next()) |face| { - log.info("font {s}: {s}", .{ - field.name, - try face.name(&name_buf), - }); - - _ = try c.add( - self.alloc, - style, - .{ .deferred = face }, - ); - } else log.warn("font-family {s} not found: {s}", .{ - field.name, - desc.family.?, - }); - } - } - } - - // Our built-in font will be used as a backup - _ = try c.add( - self.alloc, - .regular, - .{ .fallback_loaded = try Face.init( - self.font_lib, - face_ttf, - opts, - ) }, - ); - _ = try c.add( - self.alloc, - .bold, - .{ .fallback_loaded = try Face.init( - self.font_lib, - face_bold_ttf, - opts, - ) }, - ); - - // On macOS, always search for and add the Apple Emoji font - // as our preferred emoji font for fallback. We do this in case - // people add other emoji fonts to their system, we always want to - // prefer the official one. Users can override this by explicitly - // specifying a font-family for emoji. - if (comptime builtin.target.isDarwin()) apple_emoji: { - const disco = try self.discover() orelse break :apple_emoji; - var disco_it = try disco.discover(self.alloc, .{ - .family = "Apple Color Emoji", - }); - defer disco_it.deinit(); - if (try disco_it.next()) |face| { - _ = try c.add( - self.alloc, - .regular, - .{ .fallback_deferred = face }, - ); - } - } - - // Emoji fallback. We don't include this on Mac since Mac is expected - // to always have the Apple Emoji available on the system. - if (comptime !builtin.target.isDarwin() or Discover == void) { - _ = try c.add( - self.alloc, - .regular, - .{ .fallback_loaded = try Face.init( - self.font_lib, - face_emoji_ttf, - opts, - ) }, - ); - _ = try c.add( - self.alloc, - .regular, - .{ .fallback_loaded = try Face.init( - self.font_lib, - face_emoji_text_ttf, - opts, - ) }, - ); - } - - return c; -} - -/// Decrement the ref count for the given key. If the ref count is zero, -/// the GroupCache will be deinitialized and removed from the map.j:w -pub fn groupDeref(self: *GroupCacheSet, key: Key) void { - const entry = self.map.getEntry(key) orelse return; - assert(entry.value_ptr.ref >= 1); - - // If we have more than one reference, decrement and return. - if (entry.value_ptr.ref > 1) { - entry.value_ptr.ref -= 1; - return; - } - - // We are at a zero ref count so deinit the group and remove. - entry.key_ptr.deinit(); - entry.value_ptr.cache.deinit(self.alloc); - self.alloc.destroy(entry.value_ptr.cache); - self.map.removeByPtr(entry.key_ptr); -} - -/// Map of font configurations to GroupCache instances. The GroupCache -/// instances are pointers that are heap allocated so that they're -/// stable pointers across hash map resizes. -pub const Map = std.HashMapUnmanaged( - Key, - RefGroupCache, - struct { - const KeyType = Key; - - pub fn hash(ctx: @This(), k: KeyType) u64 { - _ = ctx; - return k.hashcode(); - } - - pub fn eql(ctx: @This(), a: KeyType, b: KeyType) bool { - return ctx.hash(a) == ctx.hash(b); - } - }, - std.hash_map.default_max_load_percentage, -); - -/// Initialize once and return the font discovery mechanism. This remains -/// initialized throughout the lifetime of the application because some -/// font discovery mechanisms (i.e. fontconfig) are unsafe to reinit. -fn discover(self: *GroupCacheSet) !?*Discover { - // If we're built without a font discovery mechanism, return null - if (comptime Discover == void) return null; - - // If we initialized, use it - if (self.font_discover) |*v| return v; - - self.font_discover = Discover.init(); - return &self.font_discover.?; -} - -/// Ref-counted GroupCache. -const RefGroupCache = struct { - cache: *GroupCache, - ref: u32 = 0, -}; - -/// The key used to uniquely identify a font configuration. -pub const Key = struct { - arena: ArenaAllocator, - - /// The descriptors used for all the fonts added to the - /// initial group, including all styles. This is hashed - /// in order so the order matters. All users of the struct - /// should ensure that the order is consistent. - descriptors: []const discovery.Descriptor, - - /// These are the offsets into the descriptors array for - /// each style. For example, bold is from - /// offsets[@intFromEnum(.bold) - 1] to - /// offsets[@intFromEnum(.bold)]. - style_offsets: StyleOffsets = .{0} ** style_offsets_len, - - /// The codepoint map configuration. - codepoint_map: CodepointMap, - - /// The metric modifier set configuration. - metric_modifiers: Metrics.ModifierSet, - - const style_offsets_len = std.enums.directEnumArrayLen(Style, 0); - const StyleOffsets = [style_offsets_len]usize; - - comptime { - // We assume this throughout this structure. If this changes - // we may need to change this structure. - assert(@intFromEnum(Style.regular) == 0); - assert(@intFromEnum(Style.bold) == 1); - assert(@intFromEnum(Style.italic) == 2); - assert(@intFromEnum(Style.bold_italic) == 3); - } - - pub fn init( - alloc_gpa: Allocator, - config: *const Config, - ) !Key { - var arena = ArenaAllocator.init(alloc_gpa); - errdefer arena.deinit(); - const alloc = arena.allocator(); - - var descriptors = std.ArrayList(discovery.Descriptor).init(alloc); - defer descriptors.deinit(); - for (config.@"font-family".list.items) |family| { - try descriptors.append(.{ - .family = family, - .style = config.@"font-style".nameValue(), - .size = config.@"font-size", - .variations = config.@"font-variation".list.items, - }); - } - - // In all the styled cases below, we prefer to specify an exact - // style via the `font-style` configuration. If a style is not - // specified, we use the discovery mechanism to search for a - // style category such as bold, italic, etc. We can't specify both - // because the latter will restrict the search to only that. If - // a user says `font-style = italic` for the bold face for example, - // no results would be found if we restrict to ALSO searching for - // italic. - for (config.@"font-family-bold".list.items) |family| { - const style = config.@"font-style-bold".nameValue(); - try descriptors.append(.{ - .family = family, - .style = style, - .size = config.@"font-size", - .bold = style == null, - .variations = config.@"font-variation".list.items, - }); - } - for (config.@"font-family-italic".list.items) |family| { - const style = config.@"font-style-italic".nameValue(); - try descriptors.append(.{ - .family = family, - .style = style, - .size = config.@"font-size", - .italic = style == null, - .variations = config.@"font-variation".list.items, - }); - } - for (config.@"font-family-bold-italic".list.items) |family| { - const style = config.@"font-style-bold-italic".nameValue(); - try descriptors.append(.{ - .family = family, - .style = style, - .size = config.@"font-size", - .bold = style == null, - .italic = style == null, - .variations = config.@"font-variation".list.items, - }); - } - - // Setup the codepoint map - const codepoint_map: CodepointMap = map: { - const map = config.@"font-codepoint-map"; - if (map.map.list.len == 0) break :map .{}; - const clone = try config.@"font-codepoint-map".clone(alloc); - break :map clone.map; - }; - - // Metric modifiers - const metric_modifiers: Metrics.ModifierSet = set: { - var set: Metrics.ModifierSet = .{}; - if (config.@"adjust-cell-width") |m| try set.put(alloc, .cell_width, m); - if (config.@"adjust-cell-height") |m| try set.put(alloc, .cell_height, m); - if (config.@"adjust-font-baseline") |m| try set.put(alloc, .cell_baseline, m); - if (config.@"adjust-underline-position") |m| try set.put(alloc, .underline_position, m); - if (config.@"adjust-underline-thickness") |m| try set.put(alloc, .underline_thickness, m); - if (config.@"adjust-strikethrough-position") |m| try set.put(alloc, .strikethrough_position, m); - if (config.@"adjust-strikethrough-thickness") |m| try set.put(alloc, .strikethrough_thickness, m); - break :set set; - }; - - return .{ - .arena = arena, - .descriptors = try descriptors.toOwnedSlice(), - .style_offsets = .{ - config.@"font-family".list.items.len, - config.@"font-family-bold".list.items.len, - config.@"font-family-italic".list.items.len, - config.@"font-family-bold-italic".list.items.len, - }, - .codepoint_map = codepoint_map, - .metric_modifiers = metric_modifiers, - }; - } - - pub fn deinit(self: *Key) void { - self.arena.deinit(); - } - - /// Get the descriptors for the given font style that can be - /// used with discovery. - pub fn descriptorsForStyle( - self: Key, - style: Style, - ) []const discovery.Descriptor { - const idx = @intFromEnum(style); - const start: usize = if (idx == 0) 0 else self.style_offsets[idx - 1]; - const end = self.style_offsets[idx]; - return self.descriptors[start..end]; - } - - /// Hash the key with the given hasher. - pub fn hash(self: Key, hasher: anytype) void { - const autoHash = std.hash.autoHash; - autoHash(hasher, self.descriptors.len); - for (self.descriptors) |d| d.hash(hasher); - autoHash(hasher, self.codepoint_map); - autoHash(hasher, self.metric_modifiers.count()); - if (self.metric_modifiers.count() > 0) { - inline for (@typeInfo(Metrics.Key).Enum.fields) |field| { - const key = @field(Metrics.Key, field.name); - if (self.metric_modifiers.get(key)) |value| { - autoHash(hasher, key); - value.hash(hasher); - } - } - } - } - - /// Returns a hash code that can be used to uniquely identify this - /// action. - pub fn hashcode(self: Key) u64 { - var hasher = std.hash.Wyhash.init(0); - self.hash(&hasher); - return hasher.final(); - } -}; - -const face_ttf = @embedFile("res/JetBrainsMono-Regular.ttf"); -const face_bold_ttf = @embedFile("res/JetBrainsMono-Bold.ttf"); -const face_emoji_ttf = @embedFile("res/NotoColorEmoji.ttf"); -const face_emoji_text_ttf = @embedFile("res/NotoEmoji-Regular.ttf"); - -test "Key" { - const testing = std.testing; - const alloc = testing.allocator; - var cfg = try Config.default(alloc); - defer cfg.deinit(); - - var k = try Key.init(alloc, &cfg); - defer k.deinit(); - - try testing.expect(k.hashcode() > 0); -} - -test "basics" { - const testing = std.testing; - const alloc = testing.allocator; - - var set = try GroupCacheSet.init(alloc); - defer set.deinit(); -} diff --git a/src/font/main.zig b/src/font/main.zig index 0a0d376e9..dffdac5c0 100644 --- a/src/font/main.zig +++ b/src/font/main.zig @@ -10,9 +10,6 @@ pub const CodepointResolver = @import("CodepointResolver.zig"); pub const Collection = @import("Collection.zig"); pub const DeferredFace = @import("DeferredFace.zig"); pub const Face = face.Face; -pub const Group = @import("Group.zig"); -pub const GroupCache = @import("GroupCache.zig"); -pub const GroupCacheSet = @import("GroupCacheSet.zig"); pub const Glyph = @import("Glyph.zig"); pub const Metrics = face.Metrics; pub const shape = @import("shape.zig"); @@ -30,8 +27,6 @@ pub usingnamespace @import("library.zig"); pub usingnamespace if (builtin.target.isWasm()) struct { pub usingnamespace Atlas.Wasm; pub usingnamespace DeferredFace.Wasm; - pub usingnamespace Group.Wasm; - pub usingnamespace GroupCache.Wasm; pub usingnamespace face.web_canvas.Wasm; pub usingnamespace shape.web_canvas.Wasm; } else struct {}; diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index a5e87b59b..f525f986a 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -160,7 +160,7 @@ pub const DerivedConfig = struct { font_thicken: bool, font_features: std.ArrayListUnmanaged([:0]const u8), - font_styles: font.Group.StyleStatus, + font_styles: font.CodepointResolver.StyleStatus, cursor_color: ?terminal.color.RGB, cursor_opacity: f64, cursor_text: ?terminal.color.RGB, @@ -189,7 +189,7 @@ pub const DerivedConfig = struct { const font_features = try config.@"font-feature".list.clone(alloc); // Get our font styles - var font_styles = font.Group.StyleStatus.initFill(true); + var font_styles = font.CodepointResolver.StyleStatus.initFill(true); font_styles.set(.bold, config.@"font-style-bold" != .false); font_styles.set(.italic, config.@"font-style-italic" != .false); font_styles.set(.bold_italic, config.@"font-style-bold-italic" != .false); diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index d6669ccfa..2056a236d 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -243,7 +243,7 @@ pub const DerivedConfig = struct { font_thicken: bool, font_features: std.ArrayListUnmanaged([:0]const u8), - font_styles: font.Group.StyleStatus, + font_styles: font.CodepointResolver.StyleStatus, cursor_color: ?terminal.color.RGB, cursor_text: ?terminal.color.RGB, cursor_opacity: f64, @@ -272,7 +272,7 @@ pub const DerivedConfig = struct { const font_features = try config.@"font-feature".list.clone(alloc); // Get our font styles - var font_styles = font.Group.StyleStatus.initFill(true); + var font_styles = font.CodepointResolver.StyleStatus.initFill(true); font_styles.set(.bold, config.@"font-style-bold" != .false); font_styles.set(.italic, config.@"font-style-italic" != .false); font_styles.set(.bold_italic, config.@"font-style-bold-italic" != .false); From 21a648748dbc28282f7e0ea54292cebae6118c4b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 7 Apr 2024 10:54:59 -0700 Subject: [PATCH 53/53] font: CodepointMap supports clone --- src/config/Config.zig | 6 +----- src/font/CodepointMap.zig | 12 ++++++++++++ src/font/discovery.zig | 15 +++++++++++++++ 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 03b6183c1..09491567a 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2965,11 +2965,7 @@ pub const RepeatableCodepointMap = struct { /// Deep copy of the struct. Required by Config. pub fn clone(self: *const Self, alloc: Allocator) !Self { - // TODO(fontmem): clone the codemap descriptors - - return .{ - .map = .{ .list = try self.map.list.clone(alloc) }, - }; + return .{ .map = try self.map.clone(alloc) }; } /// Compare if two of our value are requal. Required by Config. diff --git a/src/font/CodepointMap.zig b/src/font/CodepointMap.zig index c5b5b1ffb..8c9ded402 100644 --- a/src/font/CodepointMap.zig +++ b/src/font/CodepointMap.zig @@ -30,6 +30,18 @@ pub fn deinit(self: *CodepointMap, alloc: Allocator) void { self.list.deinit(alloc); } +/// Deep copy of the struct. The given allocator is expected to +/// be an arena allocator of some sort since the struct itself +/// doesn't support fine-grained deallocation of fields. +pub fn clone(self: *const CodepointMap, alloc: Allocator) !CodepointMap { + var list = try self.list.clone(alloc); + for (list.items(.descriptor)) |*d| { + d.* = try d.clone(alloc); + } + + return .{ .list = list }; +} + /// Add an entry to the map. /// /// For conflicting codepoints, entries added later take priority over diff --git a/src/font/discovery.zig b/src/font/discovery.zig index 13a994f76..4371909ba 100644 --- a/src/font/discovery.zig +++ b/src/font/discovery.zig @@ -86,6 +86,21 @@ pub const Descriptor = struct { return hasher.final(); } + /// Deep copy of the struct. The given allocator is expected to + /// be an arena allocator of some sort since the descriptor + /// itself doesn't support fine-grained deallocation of fields. + pub fn clone(self: *const Descriptor, alloc: Allocator) !Descriptor { + // We can't do any errdefer cleanup in here. As documented we + // expect the allocator to be an arena so any errors should be + // cleaned up somewhere else. + + var copy = self.*; + copy.family = if (self.family) |src| try alloc.dupeZ(u8, src) else null; + copy.style = if (self.style) |src| try alloc.dupeZ(u8, src) else null; + copy.variations = try alloc.dupe(Variation, self.variations); + return copy; + } + /// Convert to Fontconfig pattern to use for lookup. The pattern does /// not have defaults filled/substituted (Fontconfig thing) so callers /// must still do this.