From ecf3e2ad7d29b5597953388148fb15036e82d814 Mon Sep 17 00:00:00 2001 From: Jesse Miller Date: Sat, 6 Sep 2025 11:26:23 -0600 Subject: [PATCH 1/2] Position-independent font shaper caching Use relative cluster positioning to allow identical texts runs in different row positions to share the same cache entry. --- src/font/shaper/run.zig | 10 +++++++++- src/renderer/generic.zig | 6 +++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/font/shaper/run.zig b/src/font/shaper/run.zig index 90917f657..0b6ebeb4c 100644 --- a/src/font/shaper/run.zig +++ b/src/font/shaper/run.zig @@ -17,6 +17,10 @@ pub const TextRun = struct { /// 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. + /// + /// The hash is position-independent within the row by using relative + /// cluster positions. This allows identical runs in different positions + /// to share the same cache entry, improving cache efficiency. hash: u64, /// The offset in the row where this run started @@ -77,7 +81,11 @@ pub const RunIterator = struct { // Go through cell by cell and accumulate while we build our run. var j: usize = self.i; while (j < max) : (j += 1) { - const cluster = j; + // Use relative cluster positions (offset from run start) to make + // the shaping cache position-independent. This ensures that runs + // with identical content but different starting positions in the + // row produce the same hash, enabling cache reuse. + const cluster = j - self.i; const cell = &cells[j]; // If we have a selection and we're at a boundary point, then diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 8726f2951..dd0ed43bc 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -2530,7 +2530,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // Advance our index until we reach or pass // our current x position in the shaper cells. - while (shaper_cells.?[shaper_cells_i].x < x) { + while (shaper_cells.?[shaper_cells_i].x + run.offset < x) { shaper_cells_i += 1; } } @@ -2769,13 +2769,13 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // If we encounter a shaper cell to the left of the current // cell then we have some problems. This logic relies on x // position monotonically increasing. - assert(cells[shaper_cells_i].x >= x); + assert(cells[shaper_cells_i].x + run.offset >= x); // NOTE: An assumption is made here that a single cell will never // be present in more than one shaper run. If that assumption is // violated, this logic breaks. - while (shaper_cells_i < cells.len and cells[shaper_cells_i].x == x) : ({ + while (shaper_cells_i < cells.len and cells[shaper_cells_i].x + run.offset == x) : ({ shaper_cells_i += 1; }) { self.addGlyph( From 31e54ff44abae6c410de933f5867ee7c1d537369 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 8 Sep 2025 10:40:56 -0600 Subject: [PATCH 2/2] comment + style changes --- src/font/shape.zig | 8 +++++--- src/font/shaper/run.zig | 4 +++- src/renderer/generic.zig | 8 +++++--- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/font/shape.zig b/src/font/shape.zig index 5e1e30eec..dd0f3dcc5 100644 --- a/src/font/shape.zig +++ b/src/font/shape.zig @@ -38,9 +38,11 @@ pub const Shaper = switch (options.backend) { /// for a shaping call. Note all terminal cells may be present; only /// cells that have a glyph that needs to be rendered. pub const Cell = struct { - /// The column that this cell occupies. Since a set of shaper cells is - /// always on the same line, only the X is stored. It is expected the - /// caller has access to the original screen cell. + /// The X position of this shaper cell relative to the offset of the + /// run. Because runs are always within a single row, it is expected + /// that the caller can reconstruct the full position of the cell by + /// using the known Y position of the cell and adding the X position + /// to the run offset. x: u16, /// An additional offset to apply to the rendering. diff --git a/src/font/shaper/run.zig b/src/font/shaper/run.zig index 0b6ebeb4c..7bd019fd7 100644 --- a/src/font/shaper/run.zig +++ b/src/font/shaper/run.zig @@ -23,7 +23,9 @@ pub const TextRun = struct { /// to share the same cache entry, improving cache efficiency. hash: u64, - /// The offset in the row where this run started + /// The offset in the row where this run started. This is added to the + /// X position of the final shaped cells to get the absolute position + /// in the row where they belong. offset: u16, /// The total number of cells produced by this run. diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index dd0ed43bc..a72acf5c2 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -2528,9 +2528,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type { break :cache cells; }; + const cells = shaper_cells.?; + // Advance our index until we reach or pass // our current x position in the shaper cells. - while (shaper_cells.?[shaper_cells_i].x + run.offset < x) { + while (run.offset + cells[shaper_cells_i].x < x) { shaper_cells_i += 1; } } @@ -2769,13 +2771,13 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // If we encounter a shaper cell to the left of the current // cell then we have some problems. This logic relies on x // position monotonically increasing. - assert(cells[shaper_cells_i].x + run.offset >= x); + assert(run.offset + cells[shaper_cells_i].x >= x); // NOTE: An assumption is made here that a single cell will never // be present in more than one shaper run. If that assumption is // violated, this logic breaks. - while (shaper_cells_i < cells.len and cells[shaper_cells_i].x + run.offset == x) : ({ + while (shaper_cells_i < cells.len and run.offset + cells[shaper_cells_i].x == x) : ({ shaper_cells_i += 1; }) { self.addGlyph(