mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 00:06:09 +03:00
font: GroupCache is like Group, but with caching...
This commit is contained in:
@ -15,9 +15,8 @@ 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 codepoint = @import("main.zig").codepoint;
|
||||
|
||||
const log = std.log.scoped(.font_fallback);
|
||||
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
|
||||
@ -65,7 +64,7 @@ pub fn addFace(self: *Group, alloc: Allocator, style: Style, face: Face) !void {
|
||||
pub const FontIndex = packed struct {
|
||||
/// The number of bits we use for the index.
|
||||
const idx_bits = 8 - StyleArray.len;
|
||||
const IndexInt = @Type(.{ .Int = .{ .signedness = .unsigned, .bits = idx_bits } });
|
||||
pub const IndexInt = @Type(.{ .Int = .{ .signedness = .unsigned, .bits = idx_bits } });
|
||||
|
||||
style: Style,
|
||||
idx: IndexInt,
|
||||
@ -107,13 +106,6 @@ fn indexForCodepointExact(self: Group, style: Style, cp: u32) ?FontIndex {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns true if the glyph pointed to by the index requires color.
|
||||
/// This is used to determine the proper atlas to pass in for rendering
|
||||
/// the glyph.
|
||||
pub fn indexRequiresColor(self: Group, index: FontIndex) bool {
|
||||
return self.faces.get(index.style).items[@intCast(usize, index.idx)].hasColor();
|
||||
}
|
||||
|
||||
/// Return the Face represented by a given FontIndex.
|
||||
pub fn faceFromIndex(self: Group, index: FontIndex) Face {
|
||||
return self.faces.get(index.style).items[@intCast(usize, index.idx)];
|
||||
@ -165,7 +157,6 @@ test {
|
||||
const idx = group.indexForCodepoint(.regular, i).?;
|
||||
try testing.expectEqual(Style.regular, idx.style);
|
||||
try testing.expectEqual(@as(FontIndex.IndexInt, 0), idx.idx);
|
||||
try testing.expect(!group.indexRequiresColor(idx));
|
||||
|
||||
// Render it
|
||||
const face = group.faceFromIndex(idx);
|
||||
@ -183,6 +174,5 @@ test {
|
||||
const idx = group.indexForCodepoint(.regular, '🥸').?;
|
||||
try testing.expectEqual(Style.regular, idx.style);
|
||||
try testing.expectEqual(@as(FontIndex.IndexInt, 1), idx.idx);
|
||||
try testing.expect(group.indexRequiresColor(idx));
|
||||
}
|
||||
}
|
||||
|
194
src/font/GroupCache.zig
Normal file
194
src/font/GroupCache.zig
Normal file
@ -0,0 +1,194 @@
|
||||
//! 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 Atlas = @import("../Atlas.zig");
|
||||
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 Group = @import("main.zig").Group;
|
||||
|
||||
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: Atlas,
|
||||
atlas_color: Atlas,
|
||||
|
||||
const CodepointKey = struct {
|
||||
style: Style,
|
||||
codepoint: u32,
|
||||
};
|
||||
|
||||
const GlyphKey = struct {
|
||||
index: Group.FontIndex,
|
||||
glyph: u32,
|
||||
};
|
||||
|
||||
pub fn init(alloc: Allocator, group: Group) !GroupCache {
|
||||
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: 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);
|
||||
}
|
||||
|
||||
/// 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();
|
||||
}
|
||||
|
||||
/// Get the font index for a given codepoint. This is cached.
|
||||
pub fn indexForCodepoint(self: *GroupCache, alloc: Allocator, style: Style, cp: u32) !?Group.FontIndex {
|
||||
const key: CodepointKey = .{ .style = style, .codepoint = cp };
|
||||
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(style, cp);
|
||||
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,
|
||||
) !Glyph {
|
||||
const key: GlyphKey = .{ .index = index, .glyph = glyph_index };
|
||||
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 face = self.group.faceFromIndex(index);
|
||||
const atlas: *Atlas = if (face.hasColor()) &self.atlas_color else &self.atlas_greyscale;
|
||||
const glyph = self.group.renderGlyph(
|
||||
alloc,
|
||||
atlas,
|
||||
index,
|
||||
glyph_index,
|
||||
) 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,
|
||||
);
|
||||
},
|
||||
|
||||
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 Atlas.init(alloc, 512, .greyscale);
|
||||
defer atlas_greyscale.deinit(alloc);
|
||||
|
||||
var lib = try Library.init();
|
||||
defer lib.deinit();
|
||||
|
||||
var group = try Group.init(alloc);
|
||||
defer group.deinit(alloc);
|
||||
try group.addFace(alloc, .regular, try Face.init(lib, testFont, .{ .points = 12 }));
|
||||
|
||||
var cache = try init(alloc, group);
|
||||
defer cache.deinit(alloc);
|
||||
|
||||
// Visible ASCII. Do it twice to verify cache.
|
||||
var i: u32 = 32;
|
||||
while (i < 127) : (i += 1) {
|
||||
const idx = (try cache.indexForCodepoint(alloc, .regular, i)).?;
|
||||
try testing.expectEqual(Style.regular, idx.style);
|
||||
try testing.expectEqual(@as(Group.FontIndex.IndexInt, 0), idx.idx);
|
||||
|
||||
// Render
|
||||
const face = 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, .regular, i)).?;
|
||||
try testing.expectEqual(Style.regular, idx.style);
|
||||
try testing.expectEqual(@as(Group.FontIndex.IndexInt, 0), idx.idx);
|
||||
|
||||
// Render
|
||||
const face = group.faceFromIndex(idx);
|
||||
const glyph_index = face.glyphIndex(i).?;
|
||||
_ = try cache.renderGlyph(
|
||||
alloc,
|
||||
idx,
|
||||
glyph_index,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ const std = @import("std");
|
||||
pub const Face = @import("Face.zig");
|
||||
pub const Family = @import("Family.zig");
|
||||
pub const Group = @import("Group.zig");
|
||||
pub const GroupCache = @import("GroupCache.zig");
|
||||
pub const Glyph = @import("Glyph.zig");
|
||||
pub const FallbackSet = @import("FallbackSet.zig");
|
||||
pub const Library = @import("Library.zig");
|
||||
|
Reference in New Issue
Block a user