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:
@@ -0,0 +1,468 @@
|
||||
/*
|
||||
* GetOrCreateCharacterGlyph (0x005CA2D0)
|
||||
* WoW 1.12.1 (build 5875) -- Font/glyph caching function
|
||||
* #2 CPU hotspot at 3.65%
|
||||
*
|
||||
* Decompiled via Ghidra 11.4.2, with manual analysis annotations.
|
||||
*
|
||||
* ==========================================================================
|
||||
* CALLING CONVENTION (verified from assembly)
|
||||
* ==========================================================================
|
||||
*
|
||||
* Convention: __thiscall
|
||||
* - ECX = this pointer (FontObject*)
|
||||
* - [EBP+0x08] = param1: charCode (uint32 -- used as hash key, loaded into EBX)
|
||||
* - [EBP+0x0C] = param2: secondary key / flags (uint32 -- loaded into ESI initially)
|
||||
* - RET 0x8 => 2 stack params (8 bytes), caller does NOT clean up
|
||||
* - Returns: float in ST(0) (FPU register) -- the glyph width
|
||||
* - Also stores width at entry+0x1C in the glyph hash entry
|
||||
*
|
||||
* Evidence:
|
||||
* 0x005ca2d0 PUSH EBP
|
||||
* 0x005ca2d1 MOV EBP,ESP
|
||||
* 0x005ca2d7 MOV EBX,dword ptr [EBP + 0x8] ; param1 = charCode
|
||||
* 0x005ca2e0 MOV ESI,dword ptr [EBP + 0xc] ; param2
|
||||
* 0x005ca2ef MOV EAX,dword ptr [ECX + 0x60] ; this->hashMask
|
||||
* 0x005ca2f9 MOV dword ptr [EBP + -0x4],ECX ; save this
|
||||
* ...
|
||||
* 0x005ca431 RET 0x8 ; cleans 2 dword params
|
||||
* 0x005ca4a0 RET 0x8 ; early-out path also cleans 8
|
||||
*
|
||||
* The Ghidra decompiler shows float10 return types and __return_storage_ptr__
|
||||
* which is WRONG. The function returns a float via ST(0) and also returns
|
||||
* the glyph entry pointer via EDI (used by callers).
|
||||
*
|
||||
* ==========================================================================
|
||||
* STRUCTURE LAYOUT (inferred from assembly)
|
||||
* ==========================================================================
|
||||
*
|
||||
* FontObject (this):
|
||||
* +0x3C: GlyphAllocator (base of allocator struct, size ~0x28)
|
||||
* +0x3C+0x00 = +0x3C: vtable ptr (allocator vtable)
|
||||
* +0x3C+0x04 = +0x40: linked list struct (for KerningListSpliceOperation)
|
||||
* +0x3C+0x14 = +0x50: kerning hash table base
|
||||
* +0x3C+0x1C = +0x58: hash bucket array ptr
|
||||
* +0x3C+0x24 = +0x60: hash mask (0xFFFFFFFF = uninitialized, else power-of-2 minus 1)
|
||||
* +0x70: text string data index/ptr
|
||||
* +0x188: float -- texture scale factor (multiplied with texture size)
|
||||
*
|
||||
* GlyphHashEntry (returned in EDI, allocated at +0x3C):
|
||||
* +0x00: charCode (uint32 -- the hash key, matches param1)
|
||||
* +0x04: next ptr offset (for chaining in hash bucket)
|
||||
* +0x14: combined key (charCode<<16 ^ param2, masked)
|
||||
* +0x18: flags (bit 1 = "rendered/valid" flag)
|
||||
* +0x1C: glyph width (float, stored from ST(0))
|
||||
*
|
||||
* Hash bucket entry (0x0C bytes each):
|
||||
* +0x00: next-link offset
|
||||
* +0x04: first entry ptr (low bit set = sentinel/empty)
|
||||
* +0x08: (used by hash lookup)
|
||||
*
|
||||
* ==========================================================================
|
||||
* ALGORITHM SUMMARY
|
||||
* ==========================================================================
|
||||
*
|
||||
* 1. Compute combined hash key: EDX = (charCode << 16) ^ (param2 & 0xFFFF) ^ (charCode << 16)
|
||||
* This creates a hash combining the char code and secondary parameter.
|
||||
*
|
||||
* 2. If hash table is initialized (mask != -1), search the hash chain:
|
||||
* - bucket = hashBuckets[mask & charCode]
|
||||
* - Walk linked list, comparing entry->charCode == charCode AND entry->combinedKey == EDX
|
||||
* - If found AND flags bit 1 is set: early return with cached width from +0x1C (HOT PATH)
|
||||
* - If found but not rendered: fall through to rendering
|
||||
*
|
||||
* 3. If not found or hash uninitialized:
|
||||
* - Get font texture data via GetTextStringData(this->+0x70)
|
||||
* - Get driver function info (getCurrentDriverFunction x2)
|
||||
* - Optionally get texture size if flag 0x40 set
|
||||
* - Call FindCharacterInFont(this, charCode) to locate glyph in font
|
||||
* - Compute width: (float)textureSize * this->+0x188 + FindCharacterInFont_result
|
||||
*
|
||||
* 4. If glyph entry doesn't exist yet:
|
||||
* - Initialize hash table if needed (mask=-1 -> set mask=3, resize to 4 buckets)
|
||||
* - Allocate new entry via AllocateCharacterHashEntry
|
||||
* - Insert into hash chain via vtable call and KerningListSpliceOperation
|
||||
* - Store charCode and combined key in entry
|
||||
*
|
||||
* 5. Set rendered flag (|= 2), call roundFloatWithValidation on width,
|
||||
* store at entry+0x1C, return via ST(0).
|
||||
*
|
||||
* ==========================================================================
|
||||
* PERFORMANCE NOTES
|
||||
* ==========================================================================
|
||||
*
|
||||
* The hot path is the hash lookup (step 2). When the glyph is already cached
|
||||
* and rendered (flags & 2), it returns immediately at 0x5ca497-0x5ca4a0.
|
||||
* The hash uses a simple mask-and-chain approach with 0xC-byte bucket entries.
|
||||
*
|
||||
* At 3.65% CPU, the bottleneck is likely:
|
||||
* - Hash chain traversal (cache misses on linked list walk)
|
||||
* - Called extremely frequently (6 callers, all text measurement/rendering)
|
||||
* - The hash table starts small (4 buckets) and may have long chains
|
||||
*
|
||||
* ==========================================================================
|
||||
* GHIDRA DECOMPILATION (raw, for reference)
|
||||
* ==========================================================================
|
||||
*/
|
||||
|
||||
/* NOTE: Ghidra's decompiled output below is misleading in several ways:
|
||||
* 1. float10/__return_storage_ptr__ types are artifacts of FPU return
|
||||
* 2. The "param_1" and "param_2" naming doesn't match stack layout
|
||||
* 3. Some pointer arithmetic is obfuscated by Ghidra's type inference
|
||||
*
|
||||
* Corrected prototype:
|
||||
* float __thiscall GetOrCreateCharacterGlyph(FontObject* this, uint32 charCode, uint32 param2)
|
||||
* Returns: float in ST(0), glyph entry ptr in EDI
|
||||
*/
|
||||
|
||||
float10 * __thiscall
|
||||
GetOrCreateCharacterGlyph
|
||||
(void *this,float10 *__return_storage_ptr__,float10 *param_1,float10 *param_2)
|
||||
{
|
||||
float10 **ppfVar1;
|
||||
float10 *pfVar2;
|
||||
undefined *puVar3;
|
||||
undefined *puVar4;
|
||||
float10 *pfVar5;
|
||||
undefined4 unaff_ESI;
|
||||
uint unaff_EDI;
|
||||
uint uVar6;
|
||||
float10 extraout_ST0;
|
||||
float10 extraout_ST0_00;
|
||||
undefined *local_18;
|
||||
undefined *local_10;
|
||||
undefined *local_c;
|
||||
undefined *local_8;
|
||||
|
||||
pfVar2 = __return_storage_ptr__;
|
||||
local_10 = (undefined *)((uint)param_1 & 0xffff ^ (int)__return_storage_ptr__ << 0x10);
|
||||
if (*(uint *)((int)this + 0x60) != 0xffffffff) {
|
||||
pfVar5 = *(float10 **)
|
||||
(*(int *)((int)this + 0x58) +
|
||||
(*(uint *)((int)this + 0x60) & (uint)__return_storage_ptr__) * 0xc + 8);
|
||||
if ((((uint)pfVar5 & 1) != 0) || (pfVar5 == (float10 *)0x0)) {
|
||||
pfVar5 = (float10 *)0x0;
|
||||
}
|
||||
for (; (((uint)pfVar5 & 1) == 0 && (pfVar5 != (float10 *)0x0));
|
||||
pfVar5 = *(float10 **)
|
||||
((int)pfVar5 +
|
||||
*(int *)(*(int *)((int)this + 0x58) +
|
||||
(*(uint *)((int)this + 0x60) & (uint)__return_storage_ptr__) * 0xc) + 4))
|
||||
{
|
||||
if (*(float10 **)pfVar5 == __return_storage_ptr__) {
|
||||
ppfVar1 = (float10 **)((int)pfVar5 + 0x14);
|
||||
if ((ppfVar1 == &__return_storage_ptr__) || (*ppfVar1 == (float10 *)local_10)) {
|
||||
__return_storage_ptr__ = pfVar5;
|
||||
if ((*(byte *)((int)pfVar5 + 0x18) & 2) != 0) {
|
||||
return (float10 *)ppfVar1;
|
||||
}
|
||||
goto LAB_005ca30c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
__return_storage_ptr__ = (float10 *)0x0;
|
||||
LAB_005ca30c:
|
||||
local_8 = (undefined *)this;
|
||||
puVar3 = GetTextStringData(*(int *)((int)this + 0x70));
|
||||
local_c = getCurrentDriverFunction((int)puVar3);
|
||||
puVar4 = getCurrentDriverFunction((int)puVar3);
|
||||
local_18 = (undefined *)0x0;
|
||||
if ((puVar3[8] & 0x40) != 0) {
|
||||
getTextureSize(puVar3,puVar4,2,(uint *)&local_18);
|
||||
local_18 = (undefined *)((-1 < (int)local_18) - 1 & (uint)local_18);
|
||||
}
|
||||
puVar3 = local_8;
|
||||
FindCharacterInFont(local_8,pfVar2,unaff_EDI);
|
||||
pfVar5 = __return_storage_ptr__;
|
||||
local_c = (undefined *)
|
||||
(float)((float10)(int)local_18 * (float10)*(float *)(puVar3 + 0x188) + extraout_ST0);
|
||||
if (__return_storage_ptr__ == (float10 *)0x0) {
|
||||
if (*(int *)((int)this + 0x60) == -1) {
|
||||
*(undefined4 *)((int)this + 0x60) = 3;
|
||||
ResizeKerningHashTable((void *)((int)this + 0x50),4);
|
||||
param_1 = pfVar5;
|
||||
uVar6 = 0;
|
||||
do {
|
||||
ClearAndResizeKerningList((void *)(*(int *)((int)this + 0x58) + (int)param_1),4);
|
||||
uVar6 = uVar6 + 1;
|
||||
param_1 = (float10 *)((int)param_1 + 0xc);
|
||||
} while (uVar6 <= *(uint *)((int)this + 0x60));
|
||||
}
|
||||
uVar6 = *(uint *)((int)this + 0x60) & (uint)pfVar2;
|
||||
puVar3 = AllocateCharacterHashEntry((int *)((int)this + 0x3c),uVar6);
|
||||
if (puVar3 != (undefined *)0x0) {
|
||||
uVar6 = *(uint *)((int)this + 0x60) & (uint)pfVar2;
|
||||
}
|
||||
pfVar5 = (float10 *)
|
||||
(**(code **)(*(int *)((int)this + 0x3c) + 4))
|
||||
(*(int *)((int)this + 0x58) + uVar6 * 0xc,0);
|
||||
KerningListSpliceOperation((void *)((int)this + 0x40),(int *)pfVar5,2,0);
|
||||
*(float10 **)pfVar5 = pfVar2;
|
||||
__return_storage_ptr__ = pfVar5;
|
||||
if ((float10 **)((int)pfVar5 + 0x14) != ¶m_1) {
|
||||
*(float10 **)((int)pfVar5 + 0x14) = (float10 *)local_10;
|
||||
}
|
||||
}
|
||||
pfVar2 = __return_storage_ptr__;
|
||||
*(uint *)((int)__return_storage_ptr__ + 0x18) = *(uint *)((int)__return_storage_ptr__ + 0x18) | 2;
|
||||
pfVar5 = roundFloatWithValidation
|
||||
(SUB84((double)(float)local_c,0),(double)CONCAT44(unaff_ESI,unaff_EDI));
|
||||
*(float *)((int)pfVar2 + 0x1c) = (float)extraout_ST0_00;
|
||||
return pfVar5;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* ==========================================================================
|
||||
* FULL DISASSEMBLY
|
||||
* ==========================================================================
|
||||
*/
|
||||
|
||||
/*
|
||||
0x005ca2d0 PUSH EBP
|
||||
0x005ca2d1 MOV EBP,ESP
|
||||
0x005ca2d3 SUB ESP,0x14
|
||||
0x005ca2d6 PUSH EBX
|
||||
0x005ca2d7 MOV EBX,dword ptr [EBP + 0x8] ; EBX = charCode (param1)
|
||||
0x005ca2da MOV EAX,EBX
|
||||
0x005ca2dc SHL EAX,0x10 ; EAX = charCode << 16
|
||||
0x005ca2df PUSH ESI
|
||||
0x005ca2e0 MOV ESI,dword ptr [EBP + 0xc] ; ESI = param2
|
||||
0x005ca2e3 MOV EDX,EAX
|
||||
0x005ca2e5 XOR EDX,ESI ; EDX = (charCode<<16) ^ param2
|
||||
0x005ca2e7 AND EDX,0xffff ; EDX = low 16 bits
|
||||
0x005ca2ed XOR EDX,EAX ; EDX = combinedKey = (charCode<<16) | (param2 ^ charCode) & 0xFFFF
|
||||
0x005ca2ef MOV EAX,dword ptr [ECX + 0x60] ; EAX = this->hashMask
|
||||
0x005ca2f2 CMP EAX,-0x1 ; if hashMask == -1 (uninitialized)
|
||||
0x005ca2f5 LEA ESI,[ECX + 0x3c] ; ESI = &this->glyphAllocator
|
||||
0x005ca2f8 PUSH EDI
|
||||
0x005ca2f9 MOV dword ptr [EBP + -0x4],ECX ; local_this = ECX
|
||||
0x005ca2fc MOV dword ptr [EBP + -0xc],EDX ; local_combinedKey = EDX
|
||||
0x005ca2ff JNZ 0x005ca434 ; if initialized, jump to hash lookup
|
||||
|
||||
; --- Hash not initialized or entry not found: compute glyph ---
|
||||
0x005ca305 MOV dword ptr [EBP + 0x8],0x0 ; foundEntry = NULL
|
||||
0x005ca30c MOV ECX,dword ptr [EBP + -0x4] ; ECX = this
|
||||
0x005ca30f MOV ECX,dword ptr [ECX + 0x70] ; ECX = this->textStringDataIdx
|
||||
0x005ca312 CALL 0x005d0370 ; GetTextStringData(ECX) -> EAX = textData
|
||||
0x005ca317 MOV EDI,EAX ; EDI = textData
|
||||
0x005ca319 MOV EDX,EBX ; EDX = charCode (2nd param for fastcall)
|
||||
0x005ca31b MOV ECX,EDI ; ECX = textData (this for thiscall)
|
||||
0x005ca31d CALL 0x007ce960 ; getCurrentDriverFunction(textData, charCode) -> EAX
|
||||
0x005ca322 MOV EDX,dword ptr [EBP + 0xc] ; EDX = param2
|
||||
0x005ca325 MOV ECX,EDI ; ECX = textData
|
||||
0x005ca327 MOV dword ptr [EBP + -0x8],EAX ; local_driverResult1 = EAX
|
||||
0x005ca32a CALL 0x007ce960 ; getCurrentDriverFunction(textData, param2) -> EAX
|
||||
0x005ca32f MOV dword ptr [EBP + -0x14],0x0 ; textureSize = 0
|
||||
0x005ca336 TEST byte ptr [EDI + 0x8],0x40 ; if textData->flags & 0x40
|
||||
0x005ca33a JZ 0x005ca35d ; skip texture size query
|
||||
|
||||
; --- Get texture size (optional) ---
|
||||
0x005ca33c LEA EDX,[EBP + -0x14]
|
||||
0x005ca33f PUSH EDX ; &textureSize (out param)
|
||||
0x005ca340 MOV EDX,dword ptr [EBP + -0x8] ; driverResult1
|
||||
0x005ca343 PUSH 0x2 ; dimension=2
|
||||
0x005ca345 PUSH EAX ; driverResult2
|
||||
0x005ca346 MOV ECX,EDI ; textData
|
||||
0x005ca348 CALL 0x007ce830 ; getTextureSize(textData, dr2, 2, &textureSize)
|
||||
0x005ca34d MOV EAX,dword ptr [EBP + -0x14]
|
||||
0x005ca350 XOR ECX,ECX
|
||||
0x005ca352 TEST EAX,EAX
|
||||
0x005ca354 SETGE CL ; CL = (textureSize >= 0) ? 1 : 0
|
||||
0x005ca357 DEC ECX ; ECX = (>=0) ? 0 : -1
|
||||
0x005ca358 AND ECX,EAX ; ECX = (>=0) ? 0 : textureSize (clamp negative to 0... wait)
|
||||
; Actually: SETGE+DEC gives 0 if >=0, -1 if <0
|
||||
; AND with EAX: >=0 -> 0&val=0... NO:
|
||||
; SETGE CL: >=0 -> CL=1, DEC -> 0, AND -> 0
|
||||
; <0 -> CL=0, DEC -> -1(0xFFFFFFFF), AND -> EAX
|
||||
; So this CLAMPS POSITIVE TO ZERO, keeps negative?
|
||||
; That seems wrong. Let me re-read...
|
||||
; Actually SETGE = set if SF==OF. For TEST EAX,EAX:
|
||||
; EAX >= 0 -> SF=0,OF=0 -> GE true -> CL=1
|
||||
; EAX < 0 -> SF=1,OF=0 -> GE false -> CL=0
|
||||
; So: >=0: CL=1, DEC->0, AND EAX -> 0. WRONG.
|
||||
; <0: CL=0, DEC->0xFFFFFFFF, AND EAX -> EAX.
|
||||
; This clamps positive values to 0 and keeps negative??
|
||||
; More likely: the intent is max(0, textureSize).
|
||||
; Re-examining: TEST EAX,EAX / SETGE CL / DEC ECX / AND ECX,EAX
|
||||
; If EAX >= 0: ECX = (1-1) & EAX = 0 & EAX = 0 -- WRONG for max(0,x)
|
||||
; If EAX < 0: ECX = (0-1) & EAX = 0xFFFFFFFF & EAX = EAX -- keeps negative
|
||||
; This is actually: (EAX < 0) ? EAX : 0 i.e. min(0, EAX)
|
||||
; Which means textureSize = min(0, textureSize). Clamp positive to 0.
|
||||
; Likely the texture size is a signed delta/offset that should be <= 0.
|
||||
0x005ca35a MOV dword ptr [EBP + -0x14],ECX
|
||||
|
||||
; --- Find character in font ---
|
||||
0x005ca35d MOV EDI,dword ptr [EBP + -0x4] ; EDI = this
|
||||
0x005ca360 PUSH EBX ; charCode
|
||||
0x005ca361 MOV ECX,EDI ; this
|
||||
0x005ca363 CALL 0x005ca240 ; FindCharacterInFont(this, charCode) -> ST(0) = glyph advance
|
||||
0x005ca368 FILD dword ptr [EBP + -0x14] ; push (float)textureSize onto FPU
|
||||
0x005ca36b FMUL float ptr [EDI + 0x188] ; * this->textureScale
|
||||
0x005ca371 MOV EDI,dword ptr [EBP + 0x8] ; EDI = foundEntry (from hash lookup or NULL)
|
||||
0x005ca374 TEST EDI,EDI
|
||||
0x005ca376 FADDP ; ST(0) = FindCharacterInFont_result + textureSize * textureScale
|
||||
0x005ca378 FSTP float ptr [EBP + -0x8] ; local_width = ST(0)
|
||||
0x005ca37b JNZ 0x005ca40e ; if foundEntry != NULL, skip allocation
|
||||
|
||||
; --- Allocate new hash entry ---
|
||||
0x005ca381 CMP dword ptr [ESI + 0x24],-0x1 ; this->hashMask == -1?
|
||||
0x005ca385 JNZ 0x005ca3c0 ; if initialized, skip init
|
||||
0x005ca387 PUSH 0x4
|
||||
0x005ca389 LEA ECX,[ESI + 0x14] ; &this->kerningHashTable
|
||||
0x005ca38c MOV dword ptr [ESI + 0x24],0x3 ; hashMask = 3 (4 buckets)
|
||||
0x005ca393 CALL 0x005cc730 ; ResizeKerningHashTable(&hashTable, 4)
|
||||
0x005ca398 MOV dword ptr [EBP + 0xc],EDI ; counter = 0 (EDI was 0)
|
||||
0x005ca39b NOP
|
||||
0x005ca39c LEA ESP,[ESP] ; alignment padding
|
||||
|
||||
; --- Initialize each bucket ---
|
||||
0x005ca3a0 MOV ECX,dword ptr [ESI + 0x1c] ; bucketArray
|
||||
0x005ca3a3 MOV EAX,dword ptr [EBP + 0xc] ; offset
|
||||
0x005ca3a6 PUSH 0x4
|
||||
0x005ca3a8 ADD ECX,EAX ; &bucketArray[offset]
|
||||
0x005ca3aa CALL 0x005cbd50 ; ClearAndResizeKerningList(bucket, 4)
|
||||
0x005ca3af MOV ECX,dword ptr [EBP + 0xc]
|
||||
0x005ca3b2 MOV EAX,dword ptr [ESI + 0x24] ; hashMask
|
||||
0x005ca3b5 INC EDI ; counter++
|
||||
0x005ca3b6 ADD ECX,0xc ; offset += 0xC (bucket size)
|
||||
0x005ca3b9 CMP EDI,EAX ; counter <= hashMask
|
||||
0x005ca3bb MOV dword ptr [EBP + 0xc],ECX
|
||||
0x005ca3be JBE 0x005ca3a0 ; loop
|
||||
|
||||
; --- Allocate entry in hash ---
|
||||
0x005ca3c0 MOV EDI,dword ptr [ESI + 0x24] ; hashMask
|
||||
0x005ca3c3 AND EDI,EBX ; bucketIdx = hashMask & charCode
|
||||
0x005ca3c5 PUSH EDI
|
||||
0x005ca3c6 MOV ECX,ESI ; &glyphAllocator
|
||||
0x005ca3c8 CALL 0x005cc290 ; AllocateCharacterHashEntry(&allocator, bucketIdx)
|
||||
0x005ca3cd TEST EAX,EAX
|
||||
0x005ca3cf JZ 0x005ca3d6
|
||||
0x005ca3d1 MOV EDI,dword ptr [ESI + 0x24] ; re-read hashMask (may have changed due to resize)
|
||||
0x005ca3d4 AND EDI,EBX ; bucketIdx = hashMask & charCode
|
||||
|
||||
0x005ca3d6 MOV ECX,dword ptr [ESI + 0x1c] ; bucketArray
|
||||
0x005ca3d9 MOV EDX,dword ptr [ESI] ; vtable
|
||||
0x005ca3db PUSH 0x0
|
||||
0x005ca3dd LEA EAX,[EDI + EDI*0x2] ; EAX = bucketIdx * 3
|
||||
0x005ca3e0 LEA EAX,[ECX + EAX*0x4] ; EAX = &bucketArray[bucketIdx * 0xC]
|
||||
0x005ca3e3 PUSH 0x0
|
||||
0x005ca3e5 PUSH EAX ; bucket ptr
|
||||
0x005ca3e6 MOV ECX,ESI ; &glyphAllocator
|
||||
0x005ca3e8 CALL dword ptr [EDX + 0x4] ; vtable[1](allocator, bucket, 0, 0) -> new entry
|
||||
0x005ca3eb PUSH 0x0
|
||||
0x005ca3ed MOV EDI,EAX ; EDI = newEntry
|
||||
0x005ca3ef PUSH 0x2
|
||||
0x005ca3f1 PUSH EDI
|
||||
0x005ca3f2 LEA ECX,[ESI + 0x4] ; &this->linkedList
|
||||
0x005ca3f5 CALL 0x005cc4d0 ; KerningListSpliceOperation(&list, newEntry, 2, 0)
|
||||
0x005ca3fa LEA EAX,[EDI + 0x14] ; &newEntry->combinedKey
|
||||
0x005ca3fd LEA ECX,[EBP + 0xc] ; ¶m2 on stack (self-pointer check)
|
||||
0x005ca400 CMP EAX,ECX
|
||||
0x005ca402 MOV dword ptr [EDI],EBX ; newEntry->charCode = charCode
|
||||
0x005ca404 JZ 0x005ca40b ; skip if self-referencing
|
||||
0x005ca406 MOV EDX,dword ptr [EBP + -0xc] ; combinedKey
|
||||
0x005ca409 MOV dword ptr [EAX],EDX ; newEntry->combinedKey = combinedKey
|
||||
0x005ca40b MOV dword ptr [EBP + 0x8],EDI ; foundEntry = newEntry
|
||||
|
||||
; --- Store width and return ---
|
||||
0x005ca40e MOV EDX,dword ptr [EDI + 0x18] ; entry->flags
|
||||
0x005ca411 FLD float ptr [EBP + -0x8] ; load computed width
|
||||
0x005ca414 OR EDX,0x2 ; flags |= RENDERED
|
||||
0x005ca417 SUB ESP,0x8
|
||||
0x005ca41a FSTP double ptr [ESP] ; push as double (for roundFloatWithValidation)
|
||||
0x005ca41d MOV dword ptr [EDI + 0x18],EDX ; entry->flags = flags | RENDERED
|
||||
0x005ca420 CALL 0x0073fdf5 ; roundFloatWithValidation(width_as_double)
|
||||
0x005ca425 FST float ptr [EDI + 0x1c] ; entry->width = rounded result
|
||||
0x005ca428 ADD ESP,0x8 ; clean up double param
|
||||
0x005ca42b POP EDI
|
||||
0x005ca42c POP ESI
|
||||
0x005ca42d POP EBX
|
||||
0x005ca42e MOV ESP,EBP
|
||||
0x005ca430 POP EBP
|
||||
0x005ca431 RET 0x8 ; clean 2 dword params, return ST(0)
|
||||
|
||||
; --- Hash lookup (jumped to from 0x5ca2ff when hash is initialized) ---
|
||||
0x005ca434 MOV ECX,dword ptr [ESI + 0x1c] ; bucketArray
|
||||
0x005ca437 AND EAX,EBX ; bucketIdx = hashMask & charCode
|
||||
0x005ca439 LEA EAX,[EAX + EAX*0x2] ; * 3
|
||||
0x005ca43c LEA EAX,[ECX + EAX*0x4 + 0x4] ; &bucket[idx].firstEntry (skip +0 next-link offset)
|
||||
; Actually: bucket base + idx*0xC + 0x4
|
||||
0x005ca440 MOV ECX,dword ptr [EAX + 0x4] ; ECX = bucket[idx]+0x8 = first entry ptr
|
||||
0x005ca443 TEST CL,0x1 ; sentinel check (low bit)
|
||||
0x005ca446 JNZ 0x005ca44c
|
||||
0x005ca448 TEST ECX,ECX ; NULL check
|
||||
0x005ca44a JNZ 0x005ca450
|
||||
0x005ca44c XOR ECX,ECX ; entry = NULL
|
||||
0x005ca44e MOV EDI,EDI ; NOP (alignment)
|
||||
|
||||
; --- Walk hash chain ---
|
||||
0x005ca450 TEST CL,0x1 ; sentinel?
|
||||
0x005ca453 JNZ 0x005ca305 ; -> not found, go compute
|
||||
0x005ca459 TEST ECX,ECX ; NULL?
|
||||
0x005ca45b JZ 0x005ca305 ; -> not found, go compute
|
||||
0x005ca461 CMP dword ptr [ECX],EBX ; entry->charCode == charCode?
|
||||
0x005ca463 JNZ 0x005ca473 ; no match, next entry
|
||||
0x005ca465 LEA EAX,[ECX + 0x14] ; &entry->combinedKey
|
||||
0x005ca468 LEA EDI,[EBP + 0x8] ; ¶m1 on stack (self-ref check)
|
||||
0x005ca46b CMP EAX,EDI
|
||||
0x005ca46d JZ 0x005ca48a ; self-ref -> match
|
||||
0x005ca46f CMP dword ptr [EAX],EDX ; entry->combinedKey == combinedKey?
|
||||
0x005ca471 JZ 0x005ca48a ; match!
|
||||
|
||||
; --- Next entry in chain ---
|
||||
0x005ca473 MOV EAX,dword ptr [ESI + 0x24] ; hashMask
|
||||
0x005ca476 MOV EDI,dword ptr [ESI + 0x1c] ; bucketArray
|
||||
0x005ca479 AND EAX,EBX ; bucketIdx
|
||||
0x005ca47b LEA EAX,[EAX + EAX*0x2] ; * 3
|
||||
0x005ca47e LEA EAX,[EDI + EAX*0x4] ; &bucket[idx]
|
||||
0x005ca481 MOV EAX,dword ptr [EAX] ; bucket[idx].nextOffset
|
||||
0x005ca483 ADD EAX,ECX ; entry + nextOffset
|
||||
0x005ca485 MOV ECX,dword ptr [EAX + 0x4] ; next entry ptr
|
||||
0x005ca488 JMP 0x005ca450 ; continue chain walk
|
||||
|
||||
; --- Found cached entry ---
|
||||
0x005ca48a TEST byte ptr [ECX + 0x18],0x2 ; entry->flags & RENDERED?
|
||||
0x005ca48e MOV dword ptr [EBP + 0x8],ECX ; foundEntry = entry
|
||||
0x005ca491 JZ 0x005ca30c ; not rendered yet, go compute width
|
||||
0x005ca497 FLD float ptr [ECX + 0x1c] ; ST(0) = entry->width (CACHED -- HOT PATH)
|
||||
0x005ca49a POP EDI
|
||||
0x005ca49b POP ESI
|
||||
0x005ca49c POP EBX
|
||||
0x005ca49d MOV ESP,EBP
|
||||
0x005ca49f POP EBP
|
||||
0x005ca4a0 RET 0x8 ; return cached width
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
* ==========================================================================
|
||||
* XREFS TO 0x005CA2D0 (callers -- 6 call sites)
|
||||
* ==========================================================================
|
||||
*
|
||||
* 0x005c73b9 from MeasureTextLine [UNCONDITIONAL_CALL]
|
||||
* 0x005c6be6 from MeasureTextWithFormatting [UNCONDITIONAL_CALL]
|
||||
* 0x005c75cd from WrapTextWithLineBreaks [UNCONDITIONAL_CALL]
|
||||
* 0x005ccda4 from RenderTextToVertexBuffer [UNCONDITIONAL_CALL]
|
||||
* 0x005c6a74 from CalculateTextDimensions [UNCONDITIONAL_CALL]
|
||||
* 0x005c6e44 from MeasureWrappedText [UNCONDITIONAL_CALL]
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
* ==========================================================================
|
||||
* XREFS FROM 0x005CA2D0 (callees -- function calls only)
|
||||
* ==========================================================================
|
||||
*
|
||||
* 0x005ca312 -> GetTextStringData (0x005d0370) [CALL]
|
||||
* 0x005ca31d -> getCurrentDriverFunction (0x007ce960) [CALL] -- called twice (0x31d, 0x32a)
|
||||
* 0x005ca348 -> getTextureSize (0x007ce830) [CALL]
|
||||
* 0x005ca363 -> FindCharacterInFont (0x005ca240) [CALL]
|
||||
* 0x005ca393 -> ResizeKerningHashTable (0x005cc730) [CALL]
|
||||
* 0x005ca3aa -> ClearAndResizeKerningList (0x005cbd50) [CALL]
|
||||
* 0x005ca3c8 -> AllocateCharacterHashEntry (0x005cc290) [CALL]
|
||||
* 0x005ca3e8 -> vtable[1] indirect call (allocator virtual method)
|
||||
* 0x005ca3f5 -> KerningListSpliceOperation (0x005cc4d0) [CALL]
|
||||
* 0x005ca420 -> roundFloatWithValidation (0x0073fdf5) [CALL]
|
||||
*/
|
||||
@@ -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