perf(shaper/coretext): cache fonts between shape calls

This commit is contained in:
Qwerasd
2024-06-14 00:14:25 -04:00
parent 9741b3a18c
commit 04896a14b4

View File

@ -31,7 +31,7 @@ const log = std.log.scoped(.font_shaper);
/// See: https://github.com/mitchellh/ghostty/issues/1643 /// See: https://github.com/mitchellh/ghostty/issues/1643
/// ///
pub const Shaper = struct { pub const Shaper = struct {
/// The allocated used for the feature list and cell buf. /// The allocated used for the feature list, font cache, and cell buf.
alloc: Allocator, alloc: Allocator,
/// The string used for shaping the current run. /// The string used for shaping the current run.
@ -49,6 +49,13 @@ pub const Shaper = struct {
/// and releasing many objects when shaping. /// and releasing many objects when shaping.
writing_direction: *macos.foundation.Array, writing_direction: *macos.foundation.Array,
/// List where we cache fonts, so we don't have to
/// remake them for every single shaping operation.
///
/// Fonts are cached as attribute dictionaries to
/// be applied directly to attributed strings.
cached_fonts: std.ArrayList(?*macos.foundation.Dictionary),
const CellBuf = std.ArrayListUnmanaged(font.shape.Cell); const CellBuf = std.ArrayListUnmanaged(font.shape.Cell);
const CodepointList = std.ArrayListUnmanaged(Codepoint); const CodepointList = std.ArrayListUnmanaged(Codepoint);
const Codepoint = struct { const Codepoint = struct {
@ -202,12 +209,16 @@ pub const Shaper = struct {
}; };
errdefer writing_direction.release(); errdefer writing_direction.release();
const cached_fonts = std.ArrayList(?*macos.foundation.Dictionary).init(alloc);
errdefer cached_fonts.deinit();
return Shaper{ return Shaper{
.alloc = alloc, .alloc = alloc,
.cell_buf = .{}, .cell_buf = .{},
.run_state = run_state, .run_state = run_state,
.features = feats, .features = feats,
.writing_direction = writing_direction, .writing_direction = writing_direction,
.cached_fonts = cached_fonts,
}; };
} }
@ -216,6 +227,18 @@ pub const Shaper = struct {
self.run_state.deinit(self.alloc); self.run_state.deinit(self.alloc);
self.features.deinit(); self.features.deinit();
self.writing_direction.release(); self.writing_direction.release();
self.releaseCachedFonts();
self.cached_fonts.deinit();
}
/// Release all cached fonts.
pub fn releaseCachedFonts(self: *Shaper) void {
for (self.cached_fonts.items) |ft| {
if (ft) |f| {
f.release();
}
}
} }
pub fn runIterator( pub fn runIterator(
@ -267,55 +290,7 @@ pub const Shaper = struct {
defer arena.deinit(); defer arena.deinit();
const alloc = arena.allocator(); const alloc = arena.allocator();
// Get our font. We have to apply the font features we want for const attr_dict: *macos.foundation.Dictionary = try self.getFont(run.grid, run.font_index);
// the font here.
const run_font: *macos.text.Font = font: {
// 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);
defer attrs.release();
const desc = try macos.text.FontDescriptor.createWithAttributes(attrs);
defer desc.release();
const copied = try original.copyWithAttributes(0, null, desc);
errdefer copied.release();
break :font copied;
};
defer run_font.release();
// Get our font and use that get the attributes to set for the
// attributed string so the whole string uses the same font.
const attr_dict = dict: {
var keys = [_]?*const anyopaque{
macos.text.StringAttribute.font.key(),
macos.text.StringAttribute.writing_direction.key(),
};
var values = [_]?*const anyopaque{
run_font,
self.writing_direction,
};
break :dict try macos.foundation.Dictionary.create(&keys, &values);
};
defer attr_dict.release();
// Create an attributed string from our string // Create an attributed string from our string
const attr_str = try macos.foundation.AttributedString.create( const attr_str = try macos.foundation.AttributedString.create(
@ -416,6 +391,75 @@ pub const Shaper = struct {
return self.cell_buf.items; return self.cell_buf.items;
} }
/// Get an attr dict for a font from a specific index.
/// These items are cached, do not retain or release them.
fn getFont(self: *Shaper, grid: *font.SharedGrid, index: font.Collection.Index) !*macos.foundation.Dictionary {
const index_int = index.int();
if (self.cached_fonts.items.len <= index_int) {
try self.cached_fonts.ensureTotalCapacity(index_int + 1);
while (self.cached_fonts.items.len <= index_int) {
self.cached_fonts.appendAssumeCapacity(null);
}
}
if (self.cached_fonts.items[index_int]) |cached| {
return cached;
}
const run_font = font: {
// 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.
grid.lock.lockShared();
defer grid.lock.unlockShared();
const face = try grid.resolver.collection.getFace(index);
const original = face.font;
const attrs = try self.features.attrsDict(face.quirks_disable_default_font_features);
defer attrs.release();
const desc = try macos.text.FontDescriptor.createWithAttributes(attrs);
defer desc.release();
const copied = try original.copyWithAttributes(0, null, desc);
errdefer copied.release();
break :font copied;
};
defer run_font.release();
// Get our font and use that get the attributes to set for the
// attributed string so the whole string uses the same font.
const attr_dict = dict: {
var keys = [_]?*const anyopaque{
macos.text.StringAttribute.font.key(),
macos.text.StringAttribute.writing_direction.key(),
};
var values = [_]?*const anyopaque{
run_font,
self.writing_direction,
};
break :dict try macos.foundation.Dictionary.create(&keys, &values);
};
self.cached_fonts.items[index_int] = attr_dict;
return attr_dict;
}
/// The hooks for RunIterator. /// The hooks for RunIterator.
pub const RunIteratorHook = struct { pub const RunIteratorHook = struct {
shaper: *Shaper, shaper: *Shaper,