font: faster glyph hashing (#7677)

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.
This commit is contained in:
Mitchell Hashimoto
2025-06-24 19:12:30 -04:00
committed by GitHub
2 changed files with 38 additions and 5 deletions

View File

@ -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 };

View File

@ -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));
}
}
};