//! 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 is thread-safe when the operations are documented //! as thread-safe. 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, /// Lock to protect multi-threaded access to the map. lock: std.Thread.Mutex = .{}, pub const InitError = Library.InitError; /// Initialize a new SharedGridSet. pub fn init(alloc: Allocator) InitError!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(); } /// Returns the number of cached grids. pub fn count(self: *SharedGridSet) usize { self.lock.lock(); defer self.lock.unlock(); 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 DerivedConfig, font_size: DesiredSize, ) !struct { Key, *SharedGrid } { var key = try Key.init(self.alloc, config, font_size); 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", .{}); // 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, config); 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, }; }); 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, config: *const DerivedConfig, ) !Collection { // 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, .freetype_load_flags = config.@"freetype-load-flags", }; var c = Collection.init(); errdefer c.deinit(self.alloc); c.load_options = load_options; // 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 }, ); continue; } } // If there are variation configurations and we didn't find // the font, then we retry the discovery with all stylistic // bits set to false. This is because some fonts may not // set the stylistic bit in their table but still support // axes to mimic the style. At the time of writing, Berkeley // Mono Variable is like this. See #2140. if (style != .regular and desc.variations.len > 0) { var disco_it = try disco.discover(self.alloc, desc: { var copy = desc; copy.bold = false; copy.italic = false; break :desc copy; }); 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 }, ); continue; } } log.warn("font-family {s} not found: {s}", .{ field.name, desc.family.?, }); } } } // Complete our styles to ensure we have something to satisfy every // possible style request. We do this before adding our built-in font // because we want to ensure our built-in styles are fallbacks to // the configured styles. try c.completeStyles(self.alloc, config.@"font-synthetic-style"); // Our built-in font will be used as a backup _ = try c.add( self.alloc, .regular, .{ .fallback_loaded = try Face.init( self.font_lib, font.embedded.regular, load_options.faceOptions(), ) }, ); _ = try c.add( self.alloc, .bold, .{ .fallback_loaded = try Face.init( self.font_lib, font.embedded.bold, load_options.faceOptions(), ) }, ); _ = try c.add( self.alloc, .italic, .{ .fallback_loaded = try Face.init( self.font_lib, font.embedded.italic, load_options.faceOptions(), ) }, ); _ = try c.add( self.alloc, .bold_italic, .{ .fallback_loaded = try Face.init( self.font_lib, font.embedded.bold_italic, load_options.faceOptions(), ) }, ); // 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() and Discover != void) 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, font.embedded.emoji, load_options.faceOptions(), ) }, ); _ = try c.add( self.alloc, .regular, .{ .fallback_loaded = try Face.init( self.font_lib, font.embedded.emoji_text, load_options.faceOptions(), ) }, ); } 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 { self.lock.lock(); defer self.lock.unlock(); 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, }; /// 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-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-synthetic-style": configpkg.FontSyntheticStyle, @"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, @"adjust-cursor-thickness": ?Metrics.Modifier, @"freetype-load-flags": font.face.FreetypeLoadFlags, /// Initialize a DerivedConfig. The config should be either a /// config.Config or another DerivedConfig to clone from. pub fn init( alloc_gpa: Allocator, config: anytype, ) Allocator.Error!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-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-synthetic-style" = config.@"font-synthetic-style", .@"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", .@"adjust-cursor-thickness" = config.@"adjust-cursor-thickness", .@"freetype-load-flags" = if (comptime font.options.backend.hasFreetype()) config.@"freetype-load-flags" else {}, // 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, /// 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 = .{}, /// The configured font size for this key. We don't use this /// directly but it is used as part of the hash for the /// font grid. font_size: DesiredSize = .{ .points = 12 }, load_flags: font.face.FreetypeLoadFlags = font.face.freetype_load_flags_default, 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_src: *const DerivedConfig, font_size: DesiredSize, ) Allocator.Error!Key { var arena = ArenaAllocator.init(alloc_gpa); errdefer arena.deinit(); const alloc = arena.allocator(); // Clone our configuration. We need to do this because the lifetime // of the derived config is usually shorter than that of a key // and we use pointers into the derived config for the key. We // can remove this if we wanted by dupe-ing the memory we use // from DerivedConfig below. var config = try DerivedConfig.init(alloc, config_src); 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 = font_size.points, .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 = font_size.points, .bold = style == null, .variations = config.@"font-variation-bold".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 = font_size.points, .italic = style == null, .variations = config.@"font-variation-italic".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 = font_size.points, .bold = style == null, .italic = style == null, .variations = config.@"font-variation-bold-italic".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); if (config.@"adjust-cursor-thickness") |m| try set.put(alloc, .cursor_thickness, m); break :set set; }; const regular_offset = config.@"font-family".list.items.len; const bold_offset = regular_offset + config.@"font-family-bold".list.items.len; const italic_offset = bold_offset + config.@"font-family-italic".list.items.len; const bold_italic_offset = italic_offset + config.@"font-family-bold-italic".list.items.len; return .{ .arena = arena, .descriptors = try descriptors.toOwnedSlice(), .style_offsets = .{ regular_offset, bold_offset, italic_offset, bold_italic_offset, }, .codepoint_map = codepoint_map, .metric_modifiers = metric_modifiers, .font_size = font_size, .load_flags = config.@"freetype-load-flags", }; } 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, @as(u32, @bitCast(self.font_size.points))); autoHash(hasher, self.font_size.xdpi); autoHash(hasher, self.font_size.ydpi); autoHash(hasher, self.descriptors.len); for (self.descriptors) |d| d.hash(hasher); self.codepoint_map.hash(hasher); autoHash(hasher, self.metric_modifiers.count()); autoHash(hasher, self.load_flags); 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(); } }; test "Key" { const testing = std.testing; const alloc = testing.allocator; 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, .{ .points = 12 }); defer k.deinit(); var k2 = try Key.init(alloc, &keycfg, .{ .points = 12 }); defer k2.deinit(); try testing.expect(k.hashcode() > 0); try testing.expectEqual(k.hashcode(), k2.hashcode()); } test "Key different font points" { const testing = std.testing; const alloc = testing.allocator; 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, .{ .points = 12 }); defer k.deinit(); var k2 = try Key.init(alloc, &keycfg, .{ .points = 16 }); defer k2.deinit(); try testing.expect(k.hashcode() != k2.hashcode()); } test "Key different font DPI" { const testing = std.testing; const alloc = testing.allocator; 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, .{ .points = 12, .xdpi = 1 }); defer k.deinit(); var k2 = try Key.init(alloc, &keycfg, .{ .points = 12, .xdpi = 2 }); defer k2.deinit(); try testing.expect(k.hashcode() != k2.hashcode()); } test SharedGridSet { const testing = std.testing; const alloc = testing.allocator; var set = try SharedGridSet.init(alloc); defer set.deinit(); var cfg = try Config.default(alloc); defer cfg.deinit(); var keycfg = try DerivedConfig.init(alloc, &cfg); defer keycfg.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()); // Get another const key2, const grid2 = try set.ref(&keycfg, .{ .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()); }