From 0a696156703d40b9106ce8ecb9d931296eeb6356 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 1 May 2024 18:55:22 -0700 Subject: [PATCH] font/shaper: add Cache --- src/font/shape.zig | 6 +++ src/font/shaper/Cache.zig | 77 +++++++++++++++++++++++++++++++++++++++ src/lru.zig | 4 +- 3 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 src/font/shaper/Cache.zig diff --git a/src/font/shape.zig b/src/font/shape.zig index 469cf9aaf..1e5dd1a9f 100644 --- a/src/font/shape.zig +++ b/src/font/shape.zig @@ -4,6 +4,7 @@ pub const harfbuzz = @import("shaper/harfbuzz.zig"); pub const coretext = @import("shaper/coretext.zig"); pub const web_canvas = @import("shaper/web_canvas.zig"); pub usingnamespace @import("shaper/run.zig"); +pub const Cache = @import("shaper/Cache.zig"); /// Shaper implementation for our compile options. pub const Shaper = switch (options.backend) { @@ -56,3 +57,8 @@ pub const Options = struct { /// support applying features globally. features: []const []const u8 = &.{}, }; + +test { + _ = Cache; + _ = Shaper; +} diff --git a/src/font/shaper/Cache.zig b/src/font/shaper/Cache.zig new file mode 100644 index 000000000..0571be12d --- /dev/null +++ b/src/font/shaper/Cache.zig @@ -0,0 +1,77 @@ +//! 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"); + +/// 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| alloc.free(evicted.value); + gop.value_ptr.* = copy; +} + +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); +} diff --git a/src/lru.zig b/src/lru.zig index a334a783e..bb7977f00 100644 --- a/src/lru.zig +++ b/src/lru.zig @@ -150,7 +150,7 @@ pub fn HashMap( } /// 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) { @compileError("getContext must be used."); } @@ -158,7 +158,7 @@ pub fn HashMap( } /// 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; return node.data.value; }