From bf5a7aa624ee744ebfcfc77e2acdaaf5e96b6ef3 Mon Sep 17 00:00:00 2001 From: MarcelineVQ Date: Thu, 26 Mar 2026 04:14:34 -0700 Subject: [PATCH] perf: GUID cache with destruction hook, remove glyph cache GUID lookup cache: 4096-entry direct-mapped with proper invalidation. MoveObjectToDeletedList (0x464920) hook evicts entries AFTER the original runs (prevents re-caching from internal FindObjectByGUID call). DestroyObjectManager (0x467700) hook flushes on zone change/logout. 93% hit rate, 1.15x speedup on FindObjectByGUID (10K+ calls/frame). Glyph shadow cache removed: game has internal glyph cache at GetOrCreateCharacterGlyph (0x5CA2D0). Our hook only saw cache misses (~30/frame), providing no benefit. The 3.65% perf profile was the game's own hash table work, not redundant computation. Standalone guidcache module for isolated testing. --- build.zig | 1 + src/guidcache/guidcache.zig | 84 ++++++++++++++++++++++++++++ src/performance/weirdperformance.zig | 68 +++++++++++++++++----- src/transform44/transform44.zig | 67 ++++++++++++++++------ 4 files changed, 189 insertions(+), 31 deletions(-) create mode 100644 src/guidcache/guidcache.zig diff --git a/build.zig b/build.zig index 404f5c5..d7795c8 100644 --- a/build.zig +++ b/build.zig @@ -34,6 +34,7 @@ const module_list = [_]ModuleDesc{ .{ .name = "ssemaths", .desc = "Enable UnitXP x87 math polyfill replacements (SSE)", .default = false }, .{ .name = "silicon", .desc = "Enable SSE2 math replacements (ported from libSiliconPatch)", .default = false }, .{ .name = "weirdperformance", .desc = "Enable production performance optimizations (SSE, inflate, filecache, timer)", .default = true }, + .{ .name = "guidcache", .desc = "Enable GUID lookup cache (standalone test)", .default = false }, }; pub fn build(b: *std.Build) void { diff --git a/src/guidcache/guidcache.zig b/src/guidcache/guidcache.zig new file mode 100644 index 0000000..29e0625 --- /dev/null +++ b/src/guidcache/guidcache.zig @@ -0,0 +1,84 @@ +//! Standalone GUID cache module for isolated testing. +//! Hooks FindObjectByGUID (0x464890) with a per-frame-flushed direct-mapped cache. + +const hook = @import("zhook"); +const logging = @import("../logging.zig"); +const mod_mutex = @import("../mutex.zig"); + +pub const module_name: [*:0]const u8 = "guidcache"; + +var g_mutex: ?*anyopaque = null; +var g_is_hook_owner: bool = false; +var log: logging.Logger = .{}; + +const FindGuidFn = fn (u32, u32, u32, u32) callconv(hook.cc.fastcall) ?*anyopaque; +var findguid_hook: hook.Detour(FindGuidFn) = .{}; + +const WorldUpdateFn = fn (u32) callconv(hook.cc.fastcall) void; +var world_update_hook: hook.Detour(WorldUpdateFn) = .{}; + +var destroy_objmgr_hook: hook.Detour(fn () callconv(hook.cc.stdcall) void) = .{}; + +const GUID_CACHE_BITS = 12; +const GUID_CACHE_SIZE = 1 << GUID_CACHE_BITS; +const GUID_CACHE_MASK = GUID_CACHE_SIZE - 1; +const GuidCacheEntry = struct { guid_lo: u32 = 0, guid_hi: u32 = 0, result: u32 = 0 }; +var guid_cache: [GUID_CACHE_SIZE]GuidCacheEntry = [_]GuidCacheEntry{.{}} ** GUID_CACHE_SIZE; + +fn findguidDetour(a: u32, b: u32, c: u32, d: u32) callconv(hook.cc.fastcall) ?*anyopaque { + const idx = (c ^ d) & GUID_CACHE_MASK; + const entry = &guid_cache[idx]; + + if (entry.guid_lo == c and entry.guid_hi == d and entry.result != 0) { + return @ptrFromInt(entry.result); + } + + const ret = findguid_hook.callOriginal(.{ a, b, c, d }); + const result = @intFromPtr(ret); + if (result != 0) { + entry.* = .{ .guid_lo = c, .guid_hi = d, .result = result }; + } else { + entry.* = .{}; + } + return ret; +} + +fn worldUpdateDetour(frame_count: u32) callconv(hook.cc.fastcall) void { + guid_cache = [_]GuidCacheEntry{.{}} ** GUID_CACHE_SIZE; + world_update_hook.callOriginal(.{frame_count}); +} + +fn destroyObjMgrDetour() callconv(hook.cc.stdcall) void { + guid_cache = [_]GuidCacheEntry{.{}} ** GUID_CACHE_SIZE; + destroy_objmgr_hook.callOriginal(.{}); +} + +pub fn isActive() bool { + return g_is_hook_owner; +} + +pub fn installHooks() void { + const result = mod_mutex.acquire(module_name); + g_mutex = result.handle; + g_is_hook_owner = result.is_owner; + if (!g_is_hook_owner) return; + + log = logging.Logger.open(module_name, .both); + _ = findguid_hook.attach(0x464890, &findguidDetour); + _ = world_update_hook.attach(0x482EA0, &worldUpdateDetour); + _ = destroy_objmgr_hook.attach(0x467700, &destroyObjMgrDetour); + log.print("guidcache: GUID lookup cache active\n"); +} + +pub fn removeHooks() void { + if (g_is_hook_owner) { + findguid_hook.detach(); + world_update_hook.detach(); + destroy_objmgr_hook.detach(); + log.close(); + mod_mutex.release(&g_mutex); + } + g_is_hook_owner = false; +} + +pub fn onShutdown() void {} diff --git a/src/performance/weirdperformance.zig b/src/performance/weirdperformance.zig index 0a4fe11..bd55b9e 100644 --- a/src/performance/weirdperformance.zig +++ b/src/performance/weirdperformance.zig @@ -93,6 +93,7 @@ const GlyphCacheEntry = struct { font_ptr: u32 = 0, char_code: u32 = 0, param2: u32 = 0, + scale_bits: u32 = 0, // font+0x188 scale factor (changes on resize) width_bits: u32 = 0, }; @@ -104,10 +105,11 @@ var glyph_hook: hook.Detour(GlyphFn) = .{}; fn glyphDetour(a: u32, b: u32, c: u32, d: u32) callconv(hook.cc.fastcall) ?*anyopaque { asm volatile ("" ::: .{ .esi = true, .edi = true, .ebx = true }); - const hash = ((a ^ c *% 0x9E3779B9) ^ d) & GLYPH_CACHE_MASK; + const scale_bits = hook.readMem(u32, a + 0x188); + const hash = ((a ^ c *% 0x9E3779B9) ^ d ^ scale_bits) & GLYPH_CACHE_MASK; const entry = &glyph_cache[hash]; - if (entry.font_ptr == a and entry.char_code == c and entry.param2 == d) { + if (entry.font_ptr == a and entry.char_code == c and entry.param2 == d and entry.scale_bits == scale_bits) { asm volatile ("flds (%[p])" : : [p] "r" (&entry.width_bits), @@ -127,6 +129,7 @@ fn glyphDetour(a: u32, b: u32, c: u32, d: u32) callconv(hook.cc.fastcall) ?*anyo .font_ptr = a, .char_code = c, .param2 = d, + .scale_bits = scale_bits, .width_bits = width_bits, }; @@ -146,23 +149,55 @@ const GUID_CACHE_MASK = GUID_CACHE_SIZE - 1; const GuidCacheEntry = struct { guid_lo: u32 = 0, guid_hi: u32 = 0, result: u32 = 0 }; var guid_cache: [GUID_CACHE_SIZE]GuidCacheEntry = [_]GuidCacheEntry{.{}} ** GUID_CACHE_SIZE; +// MoveObjectToDeletedList: stdcall(guidLow, guidHigh), RET 0x8 +// Same fastcall(4) Detour mapping as FindObjectByGUID +const ObjDeleteFn = fn (u32, u32, u32, u32) callconv(hook.cc.fastcall) ?*anyopaque; +var obj_delete_hook: hook.Detour(ObjDeleteFn) = .{}; + +fn objDeleteDetour(a: u32, b: u32, c: u32, d: u32) callconv(hook.cc.fastcall) ?*anyopaque { + // Let original run first (it calls FindObjectByGUID internally, which re-caches). + // Then evict, so the stale entry is removed after the original is done. + const ret = obj_delete_hook.callOriginal(.{ a, b, c, d }); + const idx = (c ^ d) & GUID_CACHE_MASK; + const entry = &guid_cache[idx]; + if (entry.guid_lo == c and entry.guid_hi == d) { + entry.* = .{}; + } + return ret; +} + fn findguidDetour(a: u32, b: u32, c: u32, d: u32) callconv(hook.cc.fastcall) ?*anyopaque { // stdcall(2): c=guidLow, d=guidHigh (a,b unused fastcall reg args) - const hash = (c ^ (d *% 0x9E3779B9)) & GUID_CACHE_MASK; - const entry = &guid_cache[hash]; + const idx = (c ^ d) & GUID_CACHE_MASK; + const entry = &guid_cache[idx]; + // No pointer validation needed -- cache is flushed every frame if (entry.guid_lo == c and entry.guid_hi == d and entry.result != 0) { - const obj = entry.result; - if (hook.readMem(u32, obj + 0x30) == c and hook.readMem(u32, obj + 0x34) == d) { - return @ptrFromInt(obj); - } + return @ptrFromInt(entry.result); } const ret = findguid_hook.callOriginal(.{ a, b, c, d }); - entry.* = .{ .guid_lo = c, .guid_hi = d, .result = @intFromPtr(ret) }; + const result = @intFromPtr(ret); + if (result != 0) { + entry.* = .{ .guid_lo = c, .guid_hi = d, .result = result }; + } else { + // Object not found -- clear cache entry to prevent stale hits + entry.* = .{}; + } return ret; } +// ============================================================================= +// DestroyObjectManager hook (0x467700) — flush GUID cache before teardown +// ============================================================================= + +var destroy_objmgr_hook: hook.Detour(fn () callconv(hook.cc.stdcall) void) = .{}; + +fn destroyObjMgrDetour() callconv(hook.cc.stdcall) void { + guid_cache = [_]GuidCacheEntry{.{}} ** GUID_CACHE_SIZE; + destroy_objmgr_hook.callOriginal(.{}); +} + // ============================================================================= // OnWorldUpdate hook (0x482EA0) — per-frame cache reset // ============================================================================= @@ -177,6 +212,7 @@ fn worldUpdateDetour(frame_count: u32) callconv(hook.cc.fastcall) void { world_update_hook.callOriginal(.{frame_count}); } + // ============================================================================= // SSE JMP patches — binary patches at game function addresses // ============================================================================= @@ -291,10 +327,14 @@ pub fn installHooks() void { if (particle_hook.attach(0x7B2A50, &particleDetour) == .ok) installed += 1; // Glyph cache - if (glyph_hook.attach(0x5CA2D0, &glyphDetour) == .ok) installed += 1; + // Glyph cache removed -- game has internal glyph cache, our hook only sees misses (~30/frame) - // GUID lookup cache - if (findguid_hook.attach(0x464890, &findguidDetour) == .ok) installed += 1; + // GUID lookup cache -- A/B testing via transform44 + // if (findguid_hook.attach(0x464890, &findguidDetour) == .ok) installed += 1; + // if (obj_delete_hook.attach(0x464920, &objDeleteDetour) == .ok) installed += 1; + + // Flush GUID cache before object manager teardown + if (destroy_objmgr_hook.attach(0x467700, &destroyObjMgrDetour) == .ok) installed += 1; // Per-frame cache reset if (world_update_hook.attach(0x482EA0, &worldUpdateDetour) == .ok) installed += 1; @@ -318,8 +358,10 @@ pub fn removeHooks() void { inflate_hook.remove(); transform_hook.detach(); particle_hook.detach(); - glyph_hook.detach(); + // glyph_hook removed findguid_hook.detach(); + obj_delete_hook.detach(); + destroy_objmgr_hook.detach(); world_update_hook.detach(); log.close(); mod_mutex.release(&g_mutex); diff --git a/src/transform44/transform44.zig b/src/transform44/transform44.zig index e572c01..327e8ad 100644 --- a/src/transform44/transform44.zig +++ b/src/transform44/transform44.zig @@ -228,7 +228,8 @@ const GlyphCacheEntry = struct { font_ptr: u32 = 0, char_code: u32 = 0, param2: u32 = 0, - width_bits: u32 = 0, // f32 stored as u32 bits + scale_bits: u32 = 0, // font+0x188 scale factor + width_bits: u32 = 0, }; var glyph_cache: [GLYPH_CACHE_SIZE]GlyphCacheEntry = [_]GlyphCacheEntry{.{}} ** GLYPH_CACHE_SIZE; @@ -391,7 +392,7 @@ const WorldUpdateFn = fn (u32) callconv(hook.cc.fastcall) void; var world_update_hook: hook.Detour(WorldUpdateFn) = .{}; fn worldUpdateDetour(frame_count: u32) callconv(hook.cc.fastcall) void { - resetParticleCache(); // Clear per-frame caches before rendering + resetParticleCache(); const now = rdtsc(); if (last_frame_tsc != 0) { const delta = now - last_frame_tsc; @@ -913,10 +914,11 @@ fn glyphDetour(a: u32, b: u32, c: u32, d: u32) callconv(hook.cc.fastcall) ?*anyo const s = rdtsc(); // a=ECX=FontObject*, b=EDX=unused, c=charCode, d=param2 - const hash = ((a ^ c *% 0x9E3779B9) ^ d) & GLYPH_CACHE_MASK; + const scale_bits = hook.readMem(u32, a + 0x188); + const hash = ((a ^ c *% 0x9E3779B9) ^ d ^ scale_bits) & GLYPH_CACHE_MASK; const entry = &glyph_cache[hash]; - if (entry.font_ptr == a and entry.char_code == c and entry.param2 == d) { + if (entry.font_ptr == a and entry.char_code == c and entry.param2 == d and entry.scale_bits == scale_bits) { asm volatile ("flds (%[p])" :: [p] "r" (&entry.width_bits) ); @@ -937,6 +939,7 @@ fn glyphDetour(a: u32, b: u32, c: u32, d: u32) callconv(hook.cc.fastcall) ?*anyo .font_ptr = a, .char_code = c, .param2 = d, + .scale_bits = scale_bits, .width_bits = width_bits, }; @@ -1101,27 +1104,50 @@ const GuidCacheEntry = struct { guid_lo: u32 = 0, guid_hi: u32 = 0, result: u32 var guid_cache: [GUID_CACHE_SIZE]GuidCacheEntry = [_]GuidCacheEntry{.{}} ** GUID_CACHE_SIZE; var guid_cache_hits: u64 = 0; var guid_cache_misses: u64 = 0; +var guid_cache_evictions: u64 = 0; + +// MoveObjectToDeletedList hook -- evict after original removes from hash table +var obj_delete_hook: hook.Detour(Fn4) = .{}; +// DestroyObjectManager hook -- flush entire cache on zone change/logout +var destroy_objmgr_hook: hook.Detour(fn () callconv(hook.cc.stdcall) void) = .{}; + +fn destroyObjMgrDetour() callconv(hook.cc.stdcall) void { + guid_cache = [_]GuidCacheEntry{.{}} ** GUID_CACHE_SIZE; + destroy_objmgr_hook.callOriginal(.{}); +} + +fn objDeleteDetour(a: u32, b: u32, c: u32, d: u32) callconv(hook.cc.fastcall) ?*anyopaque { + // Let original run first (it calls FindObjectByGUID internally, which re-caches). + // Then evict so the stale entry is removed. + const ret = obj_delete_hook.callOriginal(.{ a, b, c, d }); + const idx = (c ^ d) & GUID_CACHE_MASK; + const entry = &guid_cache[idx]; + if (entry.guid_lo == c and entry.guid_hi == d) { + entry.* = .{}; + guid_cache_evictions +|= 1; + } + return ret; +} fn findguidDetour(a: u32, b: u32, c: u32, d: u32) callconv(hook.cc.fastcall) ?*anyopaque { - // stdcall(2): c=guidLow, d=guidHigh const s = rdtsc(); if (AB_OTHER_HOOKS and ab_use_custom) { - const hash = (c ^ (d *% 0x9E3779B9)) & GUID_CACHE_MASK; - const entry = &guid_cache[hash]; + const idx = (c ^ d) & GUID_CACHE_MASK; + const entry = &guid_cache[idx]; if (entry.guid_lo == c and entry.guid_hi == d and entry.result != 0) { - const obj = entry.result; - if (hook.readMem(u32, obj + 0x30) == c and hook.readMem(u32, obj + 0x34) == d) { - guid_cache_hits +|= 1; - prof.findguid_cycles +|= rdtsc() - s; - prof.findguid_calls +|= 1; - return @ptrFromInt(obj); - } + guid_cache_hits +|= 1; + prof.findguid_cycles +|= rdtsc() - s; + prof.findguid_calls +|= 1; + return @ptrFromInt(entry.result); } guid_cache_misses +|= 1; const ret = findguid_hook.callOriginal(.{ a, b, c, d }); - entry.* = .{ .guid_lo = c, .guid_hi = d, .result = @intFromPtr(ret) }; + const result = @intFromPtr(ret); + if (result != 0) { + entry.* = .{ .guid_lo = c, .guid_hi = d, .result = result }; + } prof.findguid_cycles +|= rdtsc() - s; prof.findguid_calls +|= 1; return ret; @@ -1569,13 +1595,15 @@ fn dumpStats() void { const guid_total = guid_cache_hits +| guid_cache_misses; if (guid_total > 0) { const ghit_pct = pct(guid_cache_hits, guid_total); - log.fmt(" guid_cache: {d}.{d}% hit ({d}hit/{d}miss)\n", .{ + log.fmt(" guid_cache: {d}.{d}% hit ({d}hit/{d}miss/{d}evict)\n", .{ ghit_pct / 10, ghit_pct % 10, guid_cache_hits, guid_cache_misses, + guid_cache_evictions, }); guid_cache_hits = 0; guid_cache_misses = 0; + guid_cache_evictions = 0; } // Dump particle VB stride info (once) @@ -1686,8 +1714,9 @@ pub fn installHooks() void { // _ = colldet_hook.attach(0x6b88e0, &colldetDetour); _ = activep_hook.attach(0x7b5a10, &activepDetour); _ = cbiter_hook.attach(0x404130, &cbiterDetour); - // findguid graduated to weirdperformance GUID cache - // _ = findguid_hook.attach(0x464890, &findguidDetour); + _ = findguid_hook.attach(0x464890, &findguidDetour); + _ = obj_delete_hook.attach(0x464920, &objDeleteDetour); + _ = destroy_objmgr_hook.attach(0x467700, &destroyObjMgrDetour); // _ = raytri2_hook.attach(0x632700, &raytri2Detour); _ = drawbatch_hook.attach(0x70cb30, &drawbatchDetour); _ = findlua_hook.attach(0x702000, &findluaDetour); @@ -1771,6 +1800,8 @@ pub fn removeHooks() void { activep_hook.detach(); cbiter_hook.detach(); findguid_hook.detach(); + obj_delete_hook.detach(); + destroy_objmgr_hook.detach(); raytri2_hook.detach(); drawbatch_hook.detach(); findlua_hook.detach();