diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-04-04 19:22:04 +0300 |
|---|---|---|
| committer | Can Celik <ogulcancelik@gmail.com> | 2026-04-05 01:23:06 +0300 |
| commit | 41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch) | |
| tree | 3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/font/SharedGrid.zig | |
| parent | 55c138263e190a1e79d4b997bfb9397306c0ec1f (diff) | |
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/font/SharedGrid.zig')
| -rw-r--r-- | vendor/libghostty-vt/src/font/SharedGrid.zig | 540 |
1 files changed, 540 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/font/SharedGrid.zig b/vendor/libghostty-vt/src/font/SharedGrid.zig new file mode 100644 index 00000000..5fd729b3 --- /dev/null +++ b/vendor/libghostty-vt/src/font/SharedGrid.zig @@ -0,0 +1,540 @@ +//! 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 = @import("../quirks.zig").inlineAssert; +const tripwire = @import("../tripwire.zig"); +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.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.HashMapUnmanaged(GlyphKey, Render, GlyphKey.Context, 80) = .{}, + +/// The texture atlas to store renders in. The Glyph data in the glyphs +/// cache is dependent on the atlas matching. +atlas_grayscale: 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, + +pub const init_tw = tripwire.module(enum { + codepoints_capacity, + glyphs_capacity, + reload_metrics, +}, init); + +/// 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, +) !SharedGrid { + const tw = init_tw; + + // We need to support loading options since we use the size data + assert(resolver.collection.load_options != null); + + var atlas_grayscale = try Atlas.init(alloc, 512, .grayscale); + errdefer atlas_grayscale.deinit(alloc); + var atlas_color = try Atlas.init(alloc, 512, .bgra); + errdefer atlas_color.deinit(alloc); + + var result: SharedGrid = .{ + .resolver = resolver, + .atlas_grayscale = atlas_grayscale, + .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 tw.check(.codepoints_capacity); + try result.codepoints.ensureTotalCapacity(alloc, 128); + errdefer result.codepoints.deinit(alloc); + try tw.check(.glyphs_capacity); + try result.glyphs.ensureTotalCapacity(alloc, 128); + errdefer result.glyphs.deinit(alloc); + + // Initialize our metrics. + try tw.check(.reload_metrics); + try result.reloadMetrics(); + + 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_grayscale.deinit(alloc); + self.atlas_color.deinit(alloc); + self.resolver.deinit(alloc); +} + +fn reloadMetrics(self: *SharedGrid) !void { + const collection = &self.resolver.collection; + try collection.updateMetrics(); + + self.metrics = collection.metrics.?; + + // Setup our sprite font. + self.resolver.sprite = .{ .metrics = self.metrics }; +} + +/// 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. +/// +/// This always forces loading any deferred fonts since we assume that if +/// you're looking up an index that the caller plans to use the font. By +/// loading the font in this function we can ensure thread-safety on the +/// load without complicating future calls. +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; + + if (value) |idx| preload: { + // If the font is a sprite font then we don't need to preload + // because getFace doesn't work with special fonts. + if (idx.special() != null) break :preload; + + // Load the face in case its deferred. If this fails then we would've + // failed to load it in the future anyways so we want to undo all + // the caching we did. + _ = try self.resolver.collection.getFace(idx); + } + + 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); +} + +pub const renderGlyph_tw = tripwire.module(enum { + get_presentation, +}, renderGlyph); + +/// 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 tw = renderGlyph_tw; + + 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.*; + errdefer self.glyphs.removeByPtr(gop.key_ptr); + + // Get the presentation to determine what atlas to use + try tw.check(.get_presentation); + const p = try self.resolver.getPresentation(index, glyph_index); + const atlas: *font.Atlas = switch (p) { + .text => &self.atlas_grayscale, + .emoji => &self.atlas_color, + }; + + var render_opts = opts; + + // Always use these constraints for emoji. + if (p == .emoji) { + render_opts.constraint = .{ + // Scale emoji to be as large as possible + // while preserving their aspect ratio. + .size = .cover, + + // Center the emoji in its cells. + .align_horizontal = .center, + .align_vertical = .center, + + // Add a small bit of padding so the emoji + // doesn't quite touch the edges of the cells. + .pad_left = 0.025, + .pad_right = 0.025, + }; + } + + // Render into the atlas + const glyph = self.resolver.renderGlyph( + alloc, + atlas, + index, + glyph_index, + render_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, + render_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 Context = struct { + pub fn hash(_: Context, key: GlyphKey) u64 { + // Packed is a u64 but std.hash.int improves uniformity and + // avoids collisions in our hashmap. + const packed_key = Packed.from(key); + return std.hash.int(@as(u64, @bitCast(packed_key))); + } + + pub fn eql(_: Context, a: GlyphKey, b: GlyphKey) bool { + // Packed checks glyphs but in most cases the glyphs are NOT + // equal so the first check leads to increased throughput. + return a.glyph == b.glyph and Packed.from(a) == Packed.from(b); + } + }; + + const Packed = packed struct(u64) { + index: Collection.Index, + glyph: u32, + opts: packed struct(u16) { + cell_width: u2, + thicken: bool, + thicken_strength: u8, + constraint_width: u2, + _padding: u3 = 0, + }, + + inline fn from(key: GlyphKey) Packed { + return .{ + .index = key.index, + .glyph = key.glyph, + .opts = .{ + .cell_width = key.opts.cell_width orelse 0, + .thicken = key.opts.thicken, + .thicken_strength = key.opts.thicken_strength, + .constraint_width = key.opts.constraint_width, + }, + }; + } + }; +}; + +const TestMode = enum { normal }; + +fn testGrid(mode: TestMode, alloc: Allocator, lib: Library) !SharedGrid { + const testFont = font.embedded.regular; + + var c = Collection.init(); + c.load_options = .{ .library = lib }; + + switch (mode) { + .normal => { + _ = try c.add(alloc, try .init( + lib, + testFont, + .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); + }, + } + + var r: CodepointResolver = .{ .collection = c }; + errdefer r.deinit(alloc); + + return try init(alloc, r); +} + +test getIndex { + const testing = std.testing; + const alloc = testing.allocator; + // const testEmoji = @import("test.zig").fontEmoji; + + var lib = try Library.init(alloc); + 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); + } +} + +test "renderGlyph error after cache insert rolls back cache entry" { + // This test verifies that when renderGlyph fails after inserting a cache + // entry (via getOrPut), the errdefer properly removes the entry, preventing + // corrupted/uninitialized data from remaining in the cache. + + const testing = std.testing; + const alloc = testing.allocator; + + var lib = try Library.init(alloc); + defer lib.deinit(); + + var grid = try testGrid(.normal, alloc, lib); + defer grid.deinit(alloc); + + // Get the font index for 'A' + const idx = (try grid.getIndex(alloc, 'A', .regular, null)).?; + + // Get the glyph index for 'A' + const glyph_index = glyph_index: { + grid.lock.lockShared(); + defer grid.lock.unlockShared(); + const face = try grid.resolver.collection.getFace(idx); + break :glyph_index face.glyphIndex('A').?; + }; + + const render_opts: RenderOptions = .{ .grid_metrics = grid.metrics }; + const key: GlyphKey = .{ .index = idx, .glyph = glyph_index, .opts = render_opts }; + + // Verify the cache is empty for this glyph + try testing.expect(grid.glyphs.get(key) == null); + + // Set up tripwire to fail after cache insert. + // We use OutOfMemory as it's a valid error in the renderGlyph error set. + const tw = renderGlyph_tw; + defer tw.end(.reset) catch {}; + tw.errorAlways(.get_presentation, error.OutOfMemory); + + // This should fail due to the tripwire + try testing.expectError( + error.OutOfMemory, + grid.renderGlyph(alloc, idx, glyph_index, render_opts), + ); + + // The errdefer should have removed the cache entry, leaving the cache clean. + // Without the errdefer fix, this would contain garbage/uninitialized data. + try testing.expect(grid.glyphs.get(key) == null); +} + +test "init error" { + // Test every failure point in `init` and ensure that we don't + // leak memory (testing.allocator verifies) since we're exiting early. + // + // BUG: Currently this test will fail because init() is missing errdefer + // cleanup for codepoints and glyphs when late operations fail + // (ensureTotalCapacity, reloadMetrics). + const testing = std.testing; + const alloc = testing.allocator; + + for (std.meta.tags(init_tw.FailPoint)) |tag| { + const tw = init_tw; + defer tw.end(.reset) catch unreachable; + tw.errorAlways(tag, error.OutOfMemory); + + // Create a resolver for testing - we need to set up a minimal one. + // The caller is responsible for cleaning up the resolver if init fails. + var lib = try Library.init(alloc); + defer lib.deinit(); + + var c = Collection.init(); + c.load_options = .{ .library = lib }; + _ = try c.add(alloc, try .init( + lib, + font.embedded.regular, + .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); + + var resolver: CodepointResolver = .{ .collection = c }; + defer resolver.deinit(alloc); // Caller cleans up on init failure + + try testing.expectError( + error.OutOfMemory, + init(alloc, resolver), + ); + } +} |
