diff --git a/src/App.zig b/src/App.zig index 99949d9e1..41a7887b2 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_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 @@ -55,11 +54,15 @@ pub fn create( var app = try alloc.create(App); errdefer alloc.destroy(app); + var font_grid_set = try font.SharedGridSet.init(alloc); + errdefer font_grid_set.deinit(); + app.* = .{ .alloc = alloc, .surfaces = .{}, .mailbox = .{}, .quit = false, + .font_grid_set = font_grid_set, }; errdefer app.surfaces.deinit(alloc); @@ -71,9 +74,12 @@ 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 + // 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); } @@ -166,20 +172,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 39f899d1b..f142515c5 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -54,8 +54,7 @@ rt_app: *apprt.runtime.App, rt_surface: *apprt.runtime.Surface, /// The font structures -font_lib: font.Library, -font_group: *font.GroupCache, +font_grid_key: font.SharedGridSet.Key, font_size: font.face.DesiredSize, /// The renderer for this surface. @@ -206,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, @@ -263,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", @@ -298,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); @@ -320,174 +325,15 @@ pub fn init( .ydpi = @intFromFloat(y_dpi), }; - // 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(); - - // 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 (config.@"font-codepoint-map".map.list.len > 0) { - group.codepoint_map = config.@"font-codepoint-map".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; - - 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}); - } - } - - // 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_grid_key, const font_grid = try app.font_grid_set.ref( + &derived_config.font, + 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: { @@ -509,7 +355,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", @@ -570,8 +416,7 @@ pub fn init( .app = app, .rt_app = rt_app, .rt_surface = rt_surface, - .font_lib = font_lib, - .font_group = font_group, + .font_grid_key = font_grid_key, .font_size = font_size, .renderer = renderer_impl, .renderer_thread = render_thread, @@ -588,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 @@ -686,15 +531,14 @@ 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); } + // 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); self.alloc.destroy(self.renderer_state.mutex); @@ -798,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", .{}); @@ -1122,15 +964,37 @@ 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; - // 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 = try self.app.font_grid_set.ref( + &self.config.font, + self.font_size, + ); + errdefer self.app.font_grid_set.deref(font_grid_key); + + // 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_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; } @@ -1839,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: { @@ -3138,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| { @@ -3146,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 => { @@ -3154,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/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); } } 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/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 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/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 6ca64ca01..09491567a 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 @@ -2960,9 +2965,7 @@ pub const RepeatableCodepointMap = struct { /// Deep copy of the struct. Required by Config. pub fn clone(self: *const Self, alloc: Allocator) !Self { - 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. @@ -3243,6 +3246,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) { 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/CodepointMap.zig b/src/font/CodepointMap.zig index 58a8a7c43..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 @@ -53,6 +65,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/CodepointResolver.zig b/src/font/CodepointResolver.zig new file mode 100644 index 000000000..370d933cb --- /dev/null +++ b/src/font/CodepointResolver.zig @@ -0,0 +1,522 @@ +//! 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 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; + +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. +/// +/// 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. +/// +/// 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.getIndexCodepointOverride(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 getIndexCodepointOverride( + 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, .{ .any = {} })) { + log.debug("codepoint override based on config codepoint={} family={s}", .{ + cp, + desc.family orelse "", + }); + + return idx; + } + + 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); + +/// 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); + } +} + +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); + } +} + +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/Collection.zig b/src/font/Collection.zig new file mode 100644 index 000000000..38f4b92f8 --- /dev/null +++ b/src/font/Collection.zig @@ -0,0 +1,649 @@ +//! 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. +//! +//! 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. 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 +//! 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"); +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; +const Library = font.Library; +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, + +/// 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 { + // Initialize our styles array, preallocating some space that is + // likely to be used. + var faces = StyleArray.initFill(.{}); + for (&faces.values) |*v| try v.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); + } + + if (self.load_options) |*v| v.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 +/// 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; + + // 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, + }; +} + +/// 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; +} + +/// 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_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, p_mode); +} + +/// 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 |_| {} +} + +/// 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 +/// 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)); + +/// 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. 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; + _ = alloc; + } + + /// The options to use for loading faces. + pub 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, +/// 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 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. + 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 + .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 } }, + ) }, + )); +} + +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)); + } +} + +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); + } +} + +test autoItalicize { + if (comptime !@hasDecl(Face, "italicize")) return error.SkipZigTest; + + 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); +} + +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); +} + +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 +} 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 {}, diff --git a/src/font/Group.zig b/src/font/Group.zig deleted file mode 100644 index c08f7bd34..000000000 --- a/src/font/Group.zig +++ /dev/null @@ -1,1173 +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.hash(); - } - - 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, - }, - }; - } -}; - -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; - } -}; - -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); - } -} - -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); - } -} - -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) }, - )); -} - -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/SharedGrid.zig b/src/font/SharedGrid.zig new file mode 100644 index 000000000..03f364570 --- /dev/null +++ b/src/font/SharedGrid.zig @@ -0,0 +1,359 @@ +//! 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. +//! +//! 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(); + +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; +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; +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, Render) = .{}, + +/// 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, + +/// 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. 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. +/// +/// 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, + 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, + .lock = .{}, + .metrics = undefined, // Loaded below + }; + + // 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; +} + +/// 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); + 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, + }; +} + +/// 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, + }; +} + +/// 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; +} + +/// 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 = {} }, + ); +} + +pub const Render = struct { + glyph: Glyph, + presentation: Presentation, +}; + +/// 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, + 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, + presentation: ?Presentation, +}; + +const GlyphKey = struct { + index: Collection.Index, + 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 getIndex { + 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. + 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); + try testing.expect(grid.hasCodepoint(idx, @intCast(i), null)); + } + + // 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); + } +} diff --git a/src/font/SharedGridSet.zig b/src/font/SharedGridSet.zig new file mode 100644 index 000000000..8295993a3 --- /dev/null +++ b/src/font/SharedGridSet.zig @@ -0,0 +1,632 @@ +//! 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 = .{}, + +/// 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(); +} + +/// 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); + 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 { + // 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, + }; + + var c = try Collection.init(self.alloc); + 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 }, + ); + } 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, + load_options.faceOptions(), + ) }, + ); + _ = try c.add( + self.alloc, + .bold, + .{ .fallback_loaded = try Face.init( + self.font_lib, + face_bold_ttf, + 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()) 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, + load_options.faceOptions(), + ) }, + ); + _ = try c.add( + self.alloc, + .regular, + .{ .fallback_loaded = try Face.init( + self.font_lib, + face_emoji_text_ttf, + load_options.faceOptions(), + ) }, + ); + } + + // 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 { + 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-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-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, + + /// 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 DerivedConfig, + font_size: DesiredSize, + ) !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 = 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".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".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".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); + 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| { + 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 keycfg = try DerivedConfig.init(alloc, &cfg); + defer keycfg.deinit(); + + var k = try Key.init(alloc, &keycfg, .{ .points = 12 }); + 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 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()); +} diff --git a/src/font/discovery.zig b/src/font/discovery.zig index 8874c3436..4371909ba 100644 --- a/src/font/discovery.zig +++ b/src/font/discovery.zig @@ -57,30 +57,50 @@ 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); + 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); + 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(); } + /// 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. @@ -552,7 +572,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 +580,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" { diff --git a/src/font/face/Metrics.zig b/src/font/face/Metrics.zig index e8f318d48..df96d5a6d 100644 --- a/src/font/face/Metrics.zig +++ b/src/font/face/Metrics.zig @@ -169,6 +169,19 @@ 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 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), + } + } + test "formatConfig percent" { const configpkg = @import("../../config.zig"); const testing = std.testing; diff --git a/src/font/main.zig b/src/font/main.zig index 383f2da74..dffdac5c0 100644 --- a/src/font/main.zig +++ b/src/font/main.zig @@ -6,15 +6,19 @@ 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; -pub const Group = @import("Group.zig"); -pub const GroupCache = @import("GroupCache.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"); +pub const SharedGridSet = @import("SharedGridSet.zig"); 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"); @@ -23,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 {}; @@ -145,7 +147,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/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index e6c64811a..3e7824562 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -5,10 +5,12 @@ 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; const Library = font.Library; +const SharedGrid = font.SharedGrid; const Style = font.Style; const Presentation = font.Presentation; const terminal = @import("../../terminal/main.zig"); @@ -189,7 +191,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 +199,7 @@ pub const Shaper = struct { ) font.shape.RunIterator { return .{ .hooks = .{ .shaper = self }, - .group = group, + .grid = grid, .screen = screen, .row = row, .selection = selection, @@ -231,7 +233,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); @@ -400,7 +419,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, @@ -419,7 +438,7 @@ test "run iterator" { var shaper = &testdata.shaper; var it = shaper.runIterator( - testdata.cache, + testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, @@ -439,7 +458,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, @@ -486,7 +505,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, @@ -527,7 +546,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, @@ -562,7 +581,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, @@ -590,7 +609,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, @@ -614,7 +633,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, @@ -646,7 +665,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, @@ -712,7 +731,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, @@ -752,7 +771,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, @@ -789,7 +808,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, @@ -824,7 +843,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, @@ -856,7 +875,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, @@ -894,7 +913,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, @@ -921,13 +940,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 @@ -941,7 +953,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, @@ -977,7 +989,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( @@ -1000,7 +1012,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( @@ -1023,7 +1035,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( @@ -1046,7 +1058,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( @@ -1069,7 +1081,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( @@ -1105,7 +1117,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, @@ -1124,7 +1136,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, @@ -1143,7 +1155,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, @@ -1162,7 +1174,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, @@ -1194,7 +1206,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, @@ -1213,7 +1225,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, @@ -1230,7 +1242,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, @@ -1260,7 +1272,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, @@ -1284,7 +1296,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, @@ -1309,7 +1321,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, @@ -1334,7 +1346,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, @@ -1358,7 +1370,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, @@ -1376,13 +1388,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(); } }; @@ -1412,17 +1424,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 } }, @@ -1430,7 +1436,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 } }, @@ -1447,21 +1453,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, }; } 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, }; } diff --git a/src/font/shaper/run.zig b/src/font/shaper/run.zig index b5c29ec3f..36e7ef2ab 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 @@ -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, @@ -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,10 +248,10 @@ 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( + const primary = try self.grid.getIndex( alloc, primary_cp, style, @@ -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); @@ -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. diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index e89454f9d..f525f986a 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. @@ -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, @@ -158,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, @@ -187,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); @@ -343,25 +345,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 +410,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 +452,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 +471,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, }; } @@ -564,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 setFontSize(self: *Metal, size: font.face.DesiredSize) !void { - log.info("set font size={}", .{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; - // Set our new size, this will also reset our font atlas. - try self.font_group.setSize(size); - - // 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; - }; + // 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 = .{ @@ -588,27 +588,6 @@ pub fn setFontSize(self: *Metal, size: font.face.DesiredSize) !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; - - // 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 = .{ - .width = metrics.cell_width, - .height = metrics.cell_height, - }, - }, .{ .forever = {} }); } /// Update the frame data. @@ -773,13 +752,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) @@ -1376,16 +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. - 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. @@ -1643,7 +1620,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, @@ -1868,7 +1845,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, @@ -1879,9 +1856,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, @@ -1894,11 +1870,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, }, }); } @@ -1913,7 +1889,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), @@ -1931,9 +1907,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 }, }); } @@ -1982,7 +1958,7 @@ fn addCursor( .underline => .underline, }; - const glyph = self.font_group.renderGlyph( + const render = self.font_grid.renderGlyph( self.alloc, font.sprite_index, @intFromEnum(sprite), @@ -2004,9 +1980,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]; @@ -2022,33 +1998,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(.{ @@ -2066,9 +2030,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 }, }); } diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 47d660b34..2056a236d 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, @@ -239,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, @@ -268,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); @@ -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 = .{}; } @@ -584,65 +590,20 @@ 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 { +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(); - 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, - ); + // 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 = 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.deferred_font_size = .{ .metrics = grid.metrics }; } /// The primary render callback that is completely thread-safe. @@ -1056,7 +1017,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 +1131,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 +1175,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 +1224,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 +1250,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 +1404,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 +1416,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 +1429,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 +1456,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 +1473,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, @@ -1582,16 +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. - 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. @@ -1656,74 +1594,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, + ); +} - { - const atlas = &self.font_group.atlas_greyscale; - if (atlas.modified) { - atlas.modified = false; - var texbind = try gl_state.texture.bind(.@"2D"); - defer texbind.unbind(); +/// 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 (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 + lock.lockShared(); + defer lock.unlockShared(); + + var texbind = try texture.bind(.@"2D"); + defer texbind.unbind(); + + 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, + ); } - { - 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(); - - 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, - ); - } - } - } + // Update our modified tracker after successful update + modified.* = atlas.modified.load(.monotonic); } /// Render renders the current cell state. This will not modify any of @@ -1999,7 +1941,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 +1987,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 +2008,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, ); } diff --git a/src/renderer/Options.zig b/src/renderer/Options.zig index c951eacd1..8c68affe8 100644 --- a/src/renderer/Options.zig +++ b/src/renderer/Options.zig @@ -8,8 +8,8 @@ const Config = @import("../config.zig").Config; /// The derived configuration for this renderer implementation. config: renderer.Renderer.DerivedConfig, -/// The font group that should be used. -font_group: *font.GroupCache, +/// The font grid that should be used along with the key for deref-ing. +font_grid: *font.SharedGrid, /// Padding options for the viewport. padding: Padding, diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index 113f6761c..91a213132 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -321,8 +321,9 @@ fn drainMailbox(self: *Thread) !void { } }, - .font_size => |size| { - try self.renderer.setFontSize(size); + .font_grid => |grid| { + self.renderer.setFontGrid(grid.grid); + grid.set.deref(grid.old_key); }, .foreground_color => |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, 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. 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