Add glyph shadow cache: direct-mapped O(1) bypass for game's 4-bucket hash table
GetOrCreateCharacterGlyph (0x5ca2d0) is the #2 CPU hotspot at 3.65%. The game's glyph cache uses only 4 hash buckets for ~95 ASCII chars, causing ~24-entry chain walks with pointer chasing on every lookup. Text measurement (99.8% of calls) re-walks these chains per character, thousands of times per frame during UI updates. Shadow cache: 4096-entry direct-mapped array with Murmur2 hash, keyed on (FontObject*, charCode, param2). Cache hit returns the cached float width via FPU ST(0) inline asm, skipping the chain walk entirely. Gated behind ab_use_custom for A/B benchmarking.
This commit is contained in:
@@ -91,6 +91,8 @@ const ProfState = struct {
|
||||
clip_cycles: u64 = 0,
|
||||
glyph_calls: u64 = 0, // GetOrCreateCharacterGlyph (0x5ca2d0) 3.65%
|
||||
glyph_cycles: u64 = 0,
|
||||
glyph_hits: u64 = 0, // shadow cache hits (custom mode only)
|
||||
glyph_misses: u64 = 0, // shadow cache misses (custom mode only)
|
||||
particle_calls: u64 = 0, // RenderParticleSprites (0x7b2a50) 1.73%
|
||||
particle_cycles: u64 = 0,
|
||||
collision_calls: u64 = 0, // processLinkedListCollision (0x6abc40) 1.57%
|
||||
@@ -155,6 +157,24 @@ const ProfState = struct {
|
||||
textline_cycles: u64 = 0,
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Glyph shadow cache — direct-mapped, bypasses game's 4-bucket hash table.
|
||||
// Key: (font_ptr, char_code, param2). O(1) lookup, no pointer chasing.
|
||||
// =============================================================================
|
||||
|
||||
const GLYPH_CACHE_SHIFT = 12;
|
||||
const GLYPH_CACHE_SIZE = 1 << GLYPH_CACHE_SHIFT; // 4096 entries
|
||||
const GLYPH_CACHE_MASK = GLYPH_CACHE_SIZE - 1;
|
||||
|
||||
const GlyphCacheEntry = struct {
|
||||
font_ptr: u32 = 0,
|
||||
char_code: u32 = 0,
|
||||
param2: u32 = 0,
|
||||
width_bits: u32 = 0, // f32 stored as u32 bits
|
||||
};
|
||||
|
||||
var glyph_cache: [GLYPH_CACHE_SIZE]GlyphCacheEntry = [_]GlyphCacheEntry{.{}} ** GLYPH_CACHE_SIZE;
|
||||
|
||||
inline fn rdtsc() u64 {
|
||||
var lo: u32 = undefined;
|
||||
var hi: u32 = undefined;
|
||||
@@ -749,7 +769,48 @@ fn clipDetour(a: u32, b: u32, c: u32) callconv(hook.cc.fastcall) ?*anyopaque {
|
||||
return ret;
|
||||
}
|
||||
fn glyphDetour(a: u32, b: u32, c: u32, d: u32) callconv(hook.cc.fastcall) ?*anyopaque {
|
||||
asm volatile ("" ::: .{ .esi = true, .edi = true, .ebx = true });
|
||||
|
||||
const s = rdtsc();
|
||||
|
||||
if (ab_use_custom) {
|
||||
// a=ECX=FontObject*, b=EDX=unused, c=charCode, d=param2
|
||||
const hash = std.hash.Murmur2_32.hashUint32WithSeed(c, a ^ d) & GLYPH_CACHE_MASK;
|
||||
const entry = &glyph_cache[hash];
|
||||
|
||||
if (entry.font_ptr == a and entry.char_code == c and entry.param2 == d) {
|
||||
// Cache hit — load cached width into ST(0) for caller
|
||||
asm volatile ("flds (%[p])"
|
||||
:: [p] "r" (&entry.width_bits)
|
||||
);
|
||||
prof.glyph_hits +|= 1;
|
||||
prof.glyph_cycles +|= rdtsc() - s;
|
||||
prof.glyph_calls +|= 1;
|
||||
return null; // EAX unused by callers, they read ST(0)
|
||||
}
|
||||
|
||||
// Cache miss — call original (sets ST(0)), then capture the result
|
||||
const ret = glyph_hook.callOriginal(.{ a, b, c, d });
|
||||
|
||||
// Read ST(0) without popping — original's float return is still on FPU stack
|
||||
var width_bits: u32 = undefined;
|
||||
asm volatile ("fsts (%[p])"
|
||||
:: [p] "r" (&width_bits)
|
||||
);
|
||||
|
||||
entry.* = .{
|
||||
.font_ptr = a,
|
||||
.char_code = c,
|
||||
.param2 = d,
|
||||
.width_bits = width_bits,
|
||||
};
|
||||
|
||||
prof.glyph_misses +|= 1;
|
||||
prof.glyph_cycles +|= rdtsc() - s;
|
||||
prof.glyph_calls +|= 1;
|
||||
return ret;
|
||||
}
|
||||
|
||||
const ret = glyph_hook.callOriginal(.{ a, b, c, d });
|
||||
prof.glyph_cycles +|= rdtsc() - s;
|
||||
prof.glyph_calls +|= 1;
|
||||
@@ -1190,6 +1251,17 @@ fn dumpStats() void {
|
||||
}
|
||||
}
|
||||
|
||||
// Glyph cache stats (custom mode only)
|
||||
const glyph_total = prof.glyph_hits +| prof.glyph_misses;
|
||||
if (glyph_total > 0) {
|
||||
const hit_pct = pct(prof.glyph_hits, glyph_total);
|
||||
log.fmt(" glyph_cache: {d}.{d}% hit ({d}hit/{d}miss)\n", .{
|
||||
hit_pct / 10, hit_pct % 10,
|
||||
prof.glyph_hits,
|
||||
prof.glyph_misses,
|
||||
});
|
||||
}
|
||||
|
||||
// Flip A/B mode for next period
|
||||
ab_use_custom = !ab_use_custom;
|
||||
prof = ProfState{};
|
||||
|
||||
Reference in New Issue
Block a user