mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 08:46:08 +03:00
Merge pull request #1725 from mitchellh/shaper-cache
Cache font shaping results
This commit is contained in:
@ -14,6 +14,7 @@ pub const Glyph = @import("Glyph.zig");
|
|||||||
pub const Metrics = face.Metrics;
|
pub const Metrics = face.Metrics;
|
||||||
pub const shape = @import("shape.zig");
|
pub const shape = @import("shape.zig");
|
||||||
pub const Shaper = shape.Shaper;
|
pub const Shaper = shape.Shaper;
|
||||||
|
pub const ShaperCache = shape.Cache;
|
||||||
pub const SharedGrid = @import("SharedGrid.zig");
|
pub const SharedGrid = @import("SharedGrid.zig");
|
||||||
pub const SharedGridSet = @import("SharedGridSet.zig");
|
pub const SharedGridSet = @import("SharedGridSet.zig");
|
||||||
pub const sprite = @import("sprite.zig");
|
pub const sprite = @import("sprite.zig");
|
||||||
|
@ -4,6 +4,7 @@ pub const harfbuzz = @import("shaper/harfbuzz.zig");
|
|||||||
pub const coretext = @import("shaper/coretext.zig");
|
pub const coretext = @import("shaper/coretext.zig");
|
||||||
pub const web_canvas = @import("shaper/web_canvas.zig");
|
pub const web_canvas = @import("shaper/web_canvas.zig");
|
||||||
pub usingnamespace @import("shaper/run.zig");
|
pub usingnamespace @import("shaper/run.zig");
|
||||||
|
pub const Cache = @import("shaper/Cache.zig");
|
||||||
|
|
||||||
/// Shaper implementation for our compile options.
|
/// Shaper implementation for our compile options.
|
||||||
pub const Shaper = switch (options.backend) {
|
pub const Shaper = switch (options.backend) {
|
||||||
@ -56,3 +57,8 @@ pub const Options = struct {
|
|||||||
/// support applying features globally.
|
/// support applying features globally.
|
||||||
features: []const []const u8 = &.{},
|
features: []const []const u8 = &.{},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
test {
|
||||||
|
_ = Cache;
|
||||||
|
_ = Shaper;
|
||||||
|
}
|
||||||
|
86
src/font/shaper/Cache.zig
Normal file
86
src/font/shaper/Cache.zig
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
//! This structure caches the shaped cells for a given text run.
|
||||||
|
//!
|
||||||
|
//! At one point, shaping was the most expensive part of rendering text
|
||||||
|
//! (accounting for 96% of frame time on my machine). To speed it up, this
|
||||||
|
//! was introduced so that shaping results can be cached depending on the
|
||||||
|
//! run.
|
||||||
|
//!
|
||||||
|
//! The cache key is the text run. The text run builds its own hash value
|
||||||
|
//! based on the font, style, codepoint, etc. This just utilizes the hash that
|
||||||
|
//! the text run provides.
|
||||||
|
pub const Cache = @This();
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const font = @import("../main.zig");
|
||||||
|
const lru = @import("../../lru.zig");
|
||||||
|
|
||||||
|
const log = std.log.scoped(.font_shaper_cache);
|
||||||
|
|
||||||
|
/// Our LRU is the run hash to the shaped cells.
|
||||||
|
const LRU = lru.AutoHashMap(u64, []font.shape.Cell);
|
||||||
|
|
||||||
|
/// The cache of shaped cells.
|
||||||
|
map: LRU,
|
||||||
|
|
||||||
|
pub fn init() Cache {
|
||||||
|
// Note: this is very arbitrary. Increasing this number will increase
|
||||||
|
// the cache hit rate, but also increase the memory usage. We should do
|
||||||
|
// some more empirical testing to see what the best value is.
|
||||||
|
const capacity = 1024;
|
||||||
|
|
||||||
|
return .{ .map = LRU.init(capacity) };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Cache, alloc: Allocator) void {
|
||||||
|
var it = self.map.map.iterator();
|
||||||
|
while (it.next()) |entry| alloc.free(entry.value_ptr.*.data.value);
|
||||||
|
self.map.deinit(alloc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the shaped cells for the given text run or null if they are not
|
||||||
|
/// in the cache.
|
||||||
|
pub fn get(self: *const Cache, run: font.shape.TextRun) ?[]const font.shape.Cell {
|
||||||
|
return self.map.get(run.hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert the shaped cells for the given text run into the cache. The
|
||||||
|
/// cells will be duplicated.
|
||||||
|
pub fn put(
|
||||||
|
self: *Cache,
|
||||||
|
alloc: Allocator,
|
||||||
|
run: font.shape.TextRun,
|
||||||
|
cells: []const font.shape.Cell,
|
||||||
|
) Allocator.Error!void {
|
||||||
|
const copy = try alloc.dupe(font.shape.Cell, cells);
|
||||||
|
const gop = try self.map.getOrPut(alloc, run.hash);
|
||||||
|
if (gop.evicted) |evicted| {
|
||||||
|
log.debug("evicted shaped cells for text run hash={}", .{run.hash});
|
||||||
|
alloc.free(evicted.value);
|
||||||
|
}
|
||||||
|
gop.value_ptr.* = copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn count(self: *const Cache) usize {
|
||||||
|
return self.map.map.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
test Cache {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var c = Cache.init();
|
||||||
|
defer c.deinit(alloc);
|
||||||
|
|
||||||
|
var run: font.shape.TextRun = undefined;
|
||||||
|
run.hash = 1;
|
||||||
|
try testing.expect(c.get(run) == null);
|
||||||
|
try c.put(alloc, run, &.{
|
||||||
|
.{ .x = 0, .glyph_index = 0 },
|
||||||
|
.{ .x = 1, .glyph_index = 1 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const actual = c.get(run).?;
|
||||||
|
try testing.expect(actual.len == 2);
|
||||||
|
}
|
@ -4,11 +4,21 @@ const Allocator = std.mem.Allocator;
|
|||||||
const font = @import("../main.zig");
|
const font = @import("../main.zig");
|
||||||
const shape = @import("../shape.zig");
|
const shape = @import("../shape.zig");
|
||||||
const terminal = @import("../../terminal/main.zig");
|
const terminal = @import("../../terminal/main.zig");
|
||||||
|
const autoHash = std.hash.autoHash;
|
||||||
|
const Hasher = std.hash.Wyhash;
|
||||||
|
|
||||||
/// A single text run. A text run is only valid for one Shaper instance and
|
/// A single text run. A text run is only valid for one Shaper instance and
|
||||||
/// until the next run is created. A text run never goes across multiple
|
/// until the next run is created. A text run never goes across multiple
|
||||||
/// rows in a terminal, so it is guaranteed to always be one line.
|
/// rows in a terminal, so it is guaranteed to always be one line.
|
||||||
pub const TextRun = struct {
|
pub const TextRun = struct {
|
||||||
|
/// A unique hash for this run. This can be used to cache the shaping
|
||||||
|
/// results. We don't provide a means to compare actual values if the
|
||||||
|
/// hash is the same, so we should continue to improve this hash to
|
||||||
|
/// lower the chance of hash collisions if they become a problem. If
|
||||||
|
/// there are hash collisions, it would result in rendering issues but
|
||||||
|
/// the core data would be correct.
|
||||||
|
hash: u64,
|
||||||
|
|
||||||
/// The offset in the row where this run started
|
/// The offset in the row where this run started
|
||||||
offset: u16,
|
offset: u16,
|
||||||
|
|
||||||
@ -54,6 +64,9 @@ pub const RunIterator = struct {
|
|||||||
// Allow the hook to prepare
|
// Allow the hook to prepare
|
||||||
try self.hooks.prepare();
|
try self.hooks.prepare();
|
||||||
|
|
||||||
|
// Initialize our hash for this run.
|
||||||
|
var hasher = Hasher.init(0);
|
||||||
|
|
||||||
// Let's get our style that we'll expect for the run.
|
// Let's get our style that we'll expect for the run.
|
||||||
const style = self.row.style(&cells[self.i]);
|
const style = self.row.style(&cells[self.i]);
|
||||||
|
|
||||||
@ -211,12 +224,13 @@ pub const RunIterator = struct {
|
|||||||
// If we're a fallback character, add that and continue; we
|
// If we're a fallback character, add that and continue; we
|
||||||
// don't want to add the entire grapheme.
|
// don't want to add the entire grapheme.
|
||||||
if (font_info.fallback) |cp| {
|
if (font_info.fallback) |cp| {
|
||||||
try self.hooks.addCodepoint(cp, @intCast(cluster));
|
try self.addCodepoint(&hasher, cp, @intCast(cluster));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add all the codepoints for our grapheme
|
// Add all the codepoints for our grapheme
|
||||||
try self.hooks.addCodepoint(
|
try self.addCodepoint(
|
||||||
|
&hasher,
|
||||||
if (cell.codepoint() == 0) ' ' else cell.codepoint(),
|
if (cell.codepoint() == 0) ' ' else cell.codepoint(),
|
||||||
@intCast(cluster),
|
@intCast(cluster),
|
||||||
);
|
);
|
||||||
@ -225,7 +239,7 @@ pub const RunIterator = struct {
|
|||||||
for (cps) |cp| {
|
for (cps) |cp| {
|
||||||
// Do not send presentation modifiers
|
// Do not send presentation modifiers
|
||||||
if (cp == 0xFE0E or cp == 0xFE0F) continue;
|
if (cp == 0xFE0E or cp == 0xFE0F) continue;
|
||||||
try self.hooks.addCodepoint(cp, @intCast(cluster));
|
try self.addCodepoint(&hasher, cp, @intCast(cluster));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -233,10 +247,17 @@ pub const RunIterator = struct {
|
|||||||
// Finalize our buffer
|
// Finalize our buffer
|
||||||
try self.hooks.finalize();
|
try self.hooks.finalize();
|
||||||
|
|
||||||
|
// Add our length to the hash as an additional mechanism to avoid collisions
|
||||||
|
autoHash(&hasher, j - self.i);
|
||||||
|
|
||||||
|
// Add our font index
|
||||||
|
autoHash(&hasher, current_font);
|
||||||
|
|
||||||
// Move our cursor. Must defer since we use self.i below.
|
// Move our cursor. Must defer since we use self.i below.
|
||||||
defer self.i = j;
|
defer self.i = j;
|
||||||
|
|
||||||
return TextRun{
|
return TextRun{
|
||||||
|
.hash = hasher.final(),
|
||||||
.offset = @intCast(self.i),
|
.offset = @intCast(self.i),
|
||||||
.cells = @intCast(j - self.i),
|
.cells = @intCast(j - self.i),
|
||||||
.grid = self.grid,
|
.grid = self.grid,
|
||||||
@ -244,6 +265,12 @@ pub const RunIterator = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn addCodepoint(self: *RunIterator, hasher: anytype, cp: u32, cluster: u32) !void {
|
||||||
|
autoHash(hasher, cp);
|
||||||
|
autoHash(hasher, cluster);
|
||||||
|
try self.hooks.addCodepoint(cp, cluster);
|
||||||
|
}
|
||||||
|
|
||||||
/// Find a font index that supports the grapheme for the given cell,
|
/// Find a font index that supports the grapheme for the given cell,
|
||||||
/// or null if no such font exists.
|
/// or null if no such font exists.
|
||||||
///
|
///
|
||||||
|
@ -150,7 +150,7 @@ pub fn HashMap(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get a value for a key.
|
/// Get a value for a key.
|
||||||
pub fn get(self: *Self, key: K) ?V {
|
pub fn get(self: *const Self, key: K) ?V {
|
||||||
if (@sizeOf(Context) != 0) {
|
if (@sizeOf(Context) != 0) {
|
||||||
@compileError("getContext must be used.");
|
@compileError("getContext must be used.");
|
||||||
}
|
}
|
||||||
@ -158,7 +158,7 @@ pub fn HashMap(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// See get
|
/// See get
|
||||||
pub fn getContext(self: *Self, key: K, ctx: Context) ?V {
|
pub fn getContext(self: *const Self, key: K, ctx: Context) ?V {
|
||||||
const node = self.map.getContext(key, ctx) orelse return null;
|
const node = self.map.getContext(key, ctx) orelse return null;
|
||||||
return node.data.value;
|
return node.data.value;
|
||||||
}
|
}
|
||||||
|
@ -98,6 +98,7 @@ uniforms: mtl_shaders.Uniforms,
|
|||||||
/// The font structures.
|
/// The font structures.
|
||||||
font_grid: *font.SharedGrid,
|
font_grid: *font.SharedGrid,
|
||||||
font_shaper: font.Shaper,
|
font_shaper: font.Shaper,
|
||||||
|
font_shaper_cache: font.ShaperCache,
|
||||||
|
|
||||||
/// The images that we may render.
|
/// The images that we may render.
|
||||||
images: ImageMap = .{},
|
images: ImageMap = .{},
|
||||||
@ -571,6 +572,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
|
|||||||
// Fonts
|
// Fonts
|
||||||
.font_grid = options.font_grid,
|
.font_grid = options.font_grid,
|
||||||
.font_shaper = font_shaper,
|
.font_shaper = font_shaper,
|
||||||
|
.font_shaper_cache = font.ShaperCache.init(),
|
||||||
|
|
||||||
// Shaders
|
// Shaders
|
||||||
.shaders = shaders,
|
.shaders = shaders,
|
||||||
@ -588,6 +590,7 @@ pub fn deinit(self: *Metal) void {
|
|||||||
self.cells.deinit(self.alloc);
|
self.cells.deinit(self.alloc);
|
||||||
|
|
||||||
self.font_shaper.deinit();
|
self.font_shaper.deinit();
|
||||||
|
self.font_shaper_cache.deinit(self.alloc);
|
||||||
|
|
||||||
self.config.deinit();
|
self.config.deinit();
|
||||||
|
|
||||||
@ -1666,6 +1669,12 @@ fn rebuildCells(
|
|||||||
cursor_style_: ?renderer.CursorStyle,
|
cursor_style_: ?renderer.CursorStyle,
|
||||||
color_palette: *const terminal.color.Palette,
|
color_palette: *const terminal.color.Palette,
|
||||||
) !void {
|
) !void {
|
||||||
|
// const start = try std.time.Instant.now();
|
||||||
|
// defer {
|
||||||
|
// const end = std.time.Instant.now() catch unreachable;
|
||||||
|
// std.log.warn("rebuildCells time={}us", .{end.since(start) / std.time.ns_per_us});
|
||||||
|
// }
|
||||||
|
|
||||||
// Create an arena for all our temporary allocations while rebuilding
|
// Create an arena for all our temporary allocations while rebuilding
|
||||||
var arena = ArenaAllocator.init(self.alloc);
|
var arena = ArenaAllocator.init(self.alloc);
|
||||||
defer arena.deinit();
|
defer arena.deinit();
|
||||||
@ -1728,7 +1737,24 @@ fn rebuildCells(
|
|||||||
if (shape_cursor) screen.cursor.x else null,
|
if (shape_cursor) screen.cursor.x else null,
|
||||||
);
|
);
|
||||||
while (try iter.next(self.alloc)) |run| {
|
while (try iter.next(self.alloc)) |run| {
|
||||||
for (try self.font_shaper.shape(run)) |shaper_cell| {
|
// Try to read the cells from the shaping cache if we can.
|
||||||
|
const shaper_cells = self.font_shaper_cache.get(run) orelse cache: {
|
||||||
|
const cells = try self.font_shaper.shape(run);
|
||||||
|
|
||||||
|
// Try to cache them. If caching fails for any reason we continue
|
||||||
|
// because it is just a performance optimization, not a correctness
|
||||||
|
// issue.
|
||||||
|
self.font_shaper_cache.put(self.alloc, run, cells) catch |err| {
|
||||||
|
log.warn("error caching font shaping results err={}", .{err});
|
||||||
|
};
|
||||||
|
|
||||||
|
// The cells we get from direct shaping are always owned by
|
||||||
|
// the shaper and valid until the next shaping call so we can
|
||||||
|
// just return them.
|
||||||
|
break :cache cells;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (shaper_cells) |shaper_cell| {
|
||||||
const coord: terminal.Coordinate = .{
|
const coord: terminal.Coordinate = .{
|
||||||
.x = shaper_cell.x,
|
.x = shaper_cell.x,
|
||||||
.y = y,
|
.y = y,
|
||||||
@ -1829,6 +1855,11 @@ fn rebuildCells(
|
|||||||
x += if (cp.wide) 2 else 1;
|
x += if (cp.wide) 2 else 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log some things
|
||||||
|
// log.debug("rebuildCells complete cached_runs={}", .{
|
||||||
|
// self.font_shaper_cache.count(),
|
||||||
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
fn updateCell(
|
fn updateCell(
|
||||||
|
@ -74,6 +74,7 @@ gl_state: ?GLState = null,
|
|||||||
/// The font structures.
|
/// The font structures.
|
||||||
font_grid: *font.SharedGrid,
|
font_grid: *font.SharedGrid,
|
||||||
font_shaper: font.Shaper,
|
font_shaper: font.Shaper,
|
||||||
|
font_shaper_cache: font.ShaperCache,
|
||||||
texture_greyscale_modified: usize = 0,
|
texture_greyscale_modified: usize = 0,
|
||||||
texture_greyscale_resized: usize = 0,
|
texture_greyscale_resized: usize = 0,
|
||||||
texture_color_modified: usize = 0,
|
texture_color_modified: usize = 0,
|
||||||
@ -345,6 +346,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL {
|
|||||||
.gl_state = gl_state,
|
.gl_state = gl_state,
|
||||||
.font_grid = grid,
|
.font_grid = grid,
|
||||||
.font_shaper = shaper,
|
.font_shaper = shaper,
|
||||||
|
.font_shaper_cache = font.ShaperCache.init(),
|
||||||
.draw_background = options.config.background,
|
.draw_background = options.config.background,
|
||||||
.focused = true,
|
.focused = true,
|
||||||
.foreground_color = options.config.foreground,
|
.foreground_color = options.config.foreground,
|
||||||
@ -359,6 +361,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL {
|
|||||||
|
|
||||||
pub fn deinit(self: *OpenGL) void {
|
pub fn deinit(self: *OpenGL) void {
|
||||||
self.font_shaper.deinit();
|
self.font_shaper.deinit();
|
||||||
|
self.font_shaper_cache.deinit(self.alloc);
|
||||||
|
|
||||||
{
|
{
|
||||||
var it = self.images.iterator();
|
var it = self.images.iterator();
|
||||||
@ -1013,7 +1016,24 @@ pub fn rebuildCells(
|
|||||||
if (shape_cursor) screen.cursor.x else null,
|
if (shape_cursor) screen.cursor.x else null,
|
||||||
);
|
);
|
||||||
while (try iter.next(self.alloc)) |run| {
|
while (try iter.next(self.alloc)) |run| {
|
||||||
for (try self.font_shaper.shape(run)) |shaper_cell| {
|
// Try to read the cells from the shaping cache if we can.
|
||||||
|
const shaper_cells = self.font_shaper_cache.get(run) orelse cache: {
|
||||||
|
const cells = try self.font_shaper.shape(run);
|
||||||
|
|
||||||
|
// Try to cache them. If caching fails for any reason we continue
|
||||||
|
// because it is just a performance optimization, not a correctness
|
||||||
|
// issue.
|
||||||
|
self.font_shaper_cache.put(self.alloc, run, cells) catch |err| {
|
||||||
|
log.warn("error caching font shaping results err={}", .{err});
|
||||||
|
};
|
||||||
|
|
||||||
|
// The cells we get from direct shaping are always owned by
|
||||||
|
// the shaper and valid until the next shaping call so we can
|
||||||
|
// just return them.
|
||||||
|
break :cache cells;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (shaper_cells) |shaper_cell| {
|
||||||
// If this cell falls within our preedit range then we skip it.
|
// If this cell falls within our preedit range then we skip it.
|
||||||
// We do this so we don't have conflicting data on the same
|
// We do this so we don't have conflicting data on the same
|
||||||
// cell.
|
// cell.
|
||||||
|
Reference in New Issue
Block a user