From 93dcb1954dfc059ea50d00fee3b7333ab057732a Mon Sep 17 00:00:00 2001 From: David Rubin Date: Tue, 24 Jun 2025 15:43:48 -0700 Subject: [PATCH] faster glyph hashing There are two main improvements being made here. First, we move away from using autohash and instead use a one-shot strategy similar to the Style hashing. Since the GlyphKey includes the Metrics struct, which contains quite a few fields, autohash was performing expensive and unnecessary repeated updates. The second improvement is actually just, not hashing Metrics. By ignoring the Metrics field, we can fit the rest of the GlyphKey into a 64-bit packed struct and just return that as the hash! It ends up being unique for each GlyphKey in renderGlyph, and is nearly a zero-cost operation. This ends up boosting the performance (on my machine at least), from around 560fps to 590fps on the DOOM-fire benchmark. --- src/font/SharedGrid.zig | 35 ++++++++++++++++++++++++++++++++++- src/terminal/style.zig | 8 ++++---- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/src/font/SharedGrid.zig b/src/font/SharedGrid.zig index ad385abb5..dcfa0a551 100644 --- a/src/font/SharedGrid.zig +++ b/src/font/SharedGrid.zig @@ -40,7 +40,7 @@ const log = std.log.scoped(.font_shared_grid); codepoints: std.AutoHashMapUnmanaged(CodepointKey, ?Collection.Index) = .{}, /// Cache for glyph renders into the atlas. -glyphs: std.AutoHashMapUnmanaged(GlyphKey, Render) = .{}, +glyphs: std.HashMapUnmanaged(GlyphKey, Render, GlyphKey.Context, 80) = .{}, /// The texture atlas to store renders in. The Glyph data in the glyphs /// cache is dependent on the atlas matching. @@ -307,6 +307,39 @@ const GlyphKey = struct { index: Collection.Index, glyph: u32, opts: RenderOptions, + + const Context = struct { + pub fn hash(_: Context, key: GlyphKey) u64 { + return @bitCast(Packed.from(key)); + } + + pub fn eql(_: Context, a: GlyphKey, b: GlyphKey) bool { + return Packed.from(a) == Packed.from(b); + } + }; + + const Packed = packed struct(u64) { + index: Collection.Index, + glyph: u32, + opts: packed struct(u16) { + cell_width: u2, + thicken: bool, + thicken_strength: u8, + _padding: u5 = 0, + }, + + inline fn from(key: GlyphKey) Packed { + return .{ + .index = key.index, + .glyph = key.glyph, + .opts = .{ + .cell_width = key.opts.cell_width orelse 0, + .thicken = key.opts.thicken, + .thicken_strength = key.opts.thicken_strength, + }, + }; + } + }; }; const TestMode = enum { normal }; diff --git a/src/terminal/style.zig b/src/terminal/style.zig index f35a4e1f7..865e15f64 100644 --- a/src/terminal/style.zig +++ b/src/terminal/style.zig @@ -8,9 +8,6 @@ const Offset = size.Offset; const OffsetBuf = size.OffsetBuf; const RefCountedSet = @import("ref_counted_set.zig").RefCountedSet; -const XxHash3 = std.hash.XxHash3; -const autoHash = std.hash.autoHash; - /// The unique identifier for a style. This is at most the number of cells /// that can fit into a terminal page. pub const Id = size.CellCountInt; @@ -313,12 +310,15 @@ pub const Style = struct { pub fn hash(self: *const Style) u64 { const packed_style = PackedStyle.fromStyle(self.*); - return XxHash3.hash(0, std.mem.asBytes(&packed_style)); + return std.hash.XxHash3.hash(0, std.mem.asBytes(&packed_style)); } comptime { assert(@sizeOf(PackedStyle) == 16); assert(std.meta.hasUniqueRepresentation(PackedStyle)); + for (@typeInfo(PackedStyle.Data).@"union".fields) |field| { + assert(@bitSizeOf(field.type) == @bitSizeOf(PackedStyle.Data)); + } } };