Faithful recreation of RenderSpriteQuads (0x5A0F50) with hoisted
invariant division and inlined DisplayMode_CalculateOffset. No
measurable improvement — cost is dominated by getAdapterInfo (7
sub-calls to D3D device per invocation × 3203 calls/frame) and
DrawPrimitive/DrawIndexedPrimitive virtual dispatch.
Added decompilation and assembly dumps for future reference.
Faithful recreation attempt of SetupParticleRendering (0x7B3D20).
All game function CCs verified from assembly. Vertex buffer setup
works (8 verts produced), but D3D draw submission doesn't produce
visible output. Disabled pending investigation of GfxDeviceMethod
param struct layout. The function remains as timing-only pass-through.
Fixes found during work:
- max_particle_sprites global: 0xCF58F4 → 0xCF5B60
- billboard_matrix global: 0xCF5898 → 0xCF5888
- index_buffer_6/12: 0xCF58D0/D4 → 0xCF5BAC/0xCF5AF4
- BuildIndexBuffer takes renders_count not field_28
- Identity matrices must be mutable (game writes to them)
- Detected interleaved 24-byte vertex layout: xyz(12)+color(4)+uv(8).
Fast path writes 6 sequential u32s instead of scattered stores.
- Normal stride=0 (shared global) — write once in writeback, not 4x.
- Added stride_info export + logging for vertex layout analysis.
- A/B: ~30% peak reduction, baseline also improved due to less overhead.
- VBState caching on all 5 vertex paths (was only 2D and sin/cos).
Eliminates pointer re-reads: load once, emit 4 vertices, writeback.
- Cache setupRender() result per-frame via static + resetParticleCache()
called from worldUpdateDetour. Saves ~7K function calls/frame.
- @mulAdd throughout for FMA codegen on vertex position and texcoord.
- A/B verified: BASELINE ~521ms → CUSTOM ~387ms (~25% reduction).
Full recreation of RenderParticleSprites (0x7B2A50, 2688 bytes) in
particle_sse.zig. All 5 code paths: 2D billboard, 3D billboard,
2D+rotation (sin/cos), 3D+rotation (axis-angle matrix), tail particles.
Key fixes during verification:
- colorCtx address: removed Ghidra's spurious -0x12 offset
- calcColor arg2: pass raw u32 from emitter+0x1A8, not truncated
- Texture coord lookups: +8 offset to match assembly's eax increment
between position and texcoord reads in the vertex loop
Verified in-game: particles render identically in CUSTOM vs BASELINE.
Next: optimize with SSE (inline calcColor, V4 vertex math).
- Ghidra C decompilation of RenderParticleSprites (422 lines) and 5
helper functions (calculateColorValues, matVec3Transform, etc.)
- particle_sse.zig with calcColorValues_SSE (10.7x bench but cache-miss
bound in-game — needs inlining into full function replacement)
- Bench harness for calcColorValues with correctness check
- build.zig: particle_sse as separate ReleaseFast compilation unit
- colorDetour reverted to pass-through (SSE has no in-game effect due
to L1 cache misses on scattered ColorCtx structs)
- glyph cache: add A/B toggle so BASELINE/CUSTOM periods alternate
between original function and shadow cache. Swap Murmur2 hash for
fast golden-ratio integer mix (3 insns vs multi-step).
- frustumCullBBox: fastRecip (vrcpss+NR) and cvtss2si replace vdivss
and @round bloat. 2.0x speedup (was 1.6x).
- Fix all Zig operator precedence bugs in silicon_sse: & and | bind
looser than == in Zig, so (flags & 0x8 == 0) was always false.
Affected frustumCullBBox and processLinkedListCollision.
- bone_sse: deduplicate findInterpIdx calls in bone loop — rotation's
search result reused for scale/translation when tracks share temporal
structure (canReuseInterp guard). Est. ~23% bone loop cycle reduction.
- silicon: SSE replacement for processLinkedListCollision (0x6ABC40,
1.57% CPU). V4 AABB overlap test replaces 6 x87 FCOMP/FNSTSW.
Benched at 3.2x speedup (378→115 cyc/call, 8 nodes).
- transform44: remove A/B toggle, always use SSE path (teardown guard
kept). A/B infrastructure remains for other hooks.
- bench: add processLinkedListCollision benchmark with fake linked list
test fixture and stubbed addGeometryToBuffer.
Root cause of billboard visual artifacts: callVec3SqMag used inline asm
to call game's x87 vec3SqMag (0x4549F0) with fstps to capture ST0.
With SSE2 codegen, the x87/SSE state interaction caused corrupted float
values in billboard bone matrices (mat[0][2] wildly wrong).
Replaced with pure Zig: x*x + y*y + z*z — no x87, no inline asm.
Also replaced:
- matMul (0x74A7C0): pure Zig f64 scalar matmul, no alignment needs
- callFtol (0x40A2B0): f64 intermediate + @intFromFloat (cvttsd2si)
Architecture: bone_sse.zig is REF code compiled with SSE2, called as
cdecl from thiscall wrapper in transform44.zig (cross-object to prevent
LLVM inlining AND ESP alignment into thiscall frame).
A/B: other hooks gated behind AB_OTHER_HOOKS=false for isolated testing.
Assembly-level comparison of compiled REF against original 0x714260 revealed:
1. Billboard cross product sign error (types 0x10/0x20): computed +cross
instead of -cross for components 0/1, corrupting billboard bone matrices
2. colorAnimLoop wrong count field: read model_hdr+0x6C instead of +0x64
3. colorAnimLoop wrong gate offset: checked anim_data+0x04 instead of +0x0C
4. Timestamp delta guard inverted: REF guarded on cur_ts!=0 and always
wrote to this+0x4C; original guards on this+0x4C!=0 first and never
seeds the field (something else initializes it)
5. Section 5 emitter_ctx cached instead of re-read after matMul call
Also: build REF with x87-only target (subtract SSE/SSE2 features) to
match original's FLD/FMUL/FSTP codegen, and use callVec3SqMag for all
magnitude computations instead of inline SSE math.
Remaining known issues (not yet fixed):
- texAnimLoop alpha track missing crossfade blend
- colorAnimLoop missing crossfade blend
- Missing word animation section (model_hdr+0x6C/0x70)
- Bisect infrastructure and diagnostic code still present (test scaffolding)
- Replace all reimplemented game functions with actual game calls:
vec3_sqmag (0x4549F0), __ftol (0x40A2B0), getIndexOffset (0x71AFF0),
setShortValue (0x71B010) — matching assembly exactly
- Fix 3 wrong hardcoded constants that differ at runtime from Ghidra static values:
SHORT_TO_FLOAT: 0x38000000→0x38000100 (1/32767 not 1/32768)
BILLBOARD_EPSILON: 0x3727c5ac→0x34800000
HERMITE_5: 5.0→6.0
All now read from game memory at runtime
- Fix timestamp delta guard (this+0x4C): was guarding on stored value,
assembly guards on anim_ctx pointer — prevents first-frame initialization
- Change REF calling convention to thiscall matching original
- Add comprehensive memory comparison diagnostic (original vs REF)
- Disable interpKfDetour hook (was pure passthrough)
Bug #18: Ribbon emitter track offsets completely wrong. Position was
entry+0x24, actual tracks from assembly (0x716402-0x716AA9):
Track 1 (Vec3): gate=+0x1C, AnimData=+0x10, output=+0x00
Track 2 (Vec3): gate=+0x38, AnimData=+0x2C, output=+0x30
Track 3 (float): gate=+0x70, AnimData=+0x64, output=+0x80
Track 4 (Vec3): gate=+0x54, AnimData=+0x48, output=+0x50
Bug #19: Track 4 output was +0xA0 (should be +0x50), and tracks 2/4
were float (should be Vec3). Wrong output offsets corrupted stack data,
causing bone_rt pointer to contain float bit patterns.
Teardown: hook World_HandleLogoutCleanup (0x491180) instead of
CleanupWorldAndEntities (0x66FC40). Fires at START of logout sequence
before any Lua callbacks trigger model processing on freed data.
Prologue: PUSH ESI/EDI, epilogue: POP EDI/ESI, JMP (tail call).
Also: added bone_sse_reference.zig as separate compilation unit for
A/B testing the proven-working version independently.
Bug #15: Short-value interpolation read AD+0x0C (nTimestamps count) as
pointer to short array. Should be AD+0x18 (ofsValues). Caused segfault
reading from address ~0x134 (a count value treated as pointer).
Bug #16: boneKeyframeLoop AnimData offsets wrong. Rotation was kf_data+0x10,
should be +0x1C. Scale was kf_data+0x28 with garbled gate, should be +0x38
with gate at +0x44. Translation was correct at +0x00. Entry structure is
3x28-byte AnimBlocks: trans(+0x00), rot(+0x1C), scale(+0x38) = stride 0x54.
Bug #17: particleEmitterLoop (model_hdr+0x124) position AnimData was
entry+0x04, should be entry+0x10 with gate at entry+0x1C. Second track
was entry+0x20, should be entry+0x38/gate +0x44. Third track at
entry+0x60/gate +0x6C was missing entirely.
Also fixed IsParticleBufferEmpty calling convention (bug #14):
was __stdcall with stack param, now __fastcall(ECX=ptr) plain RET.
SSE dispatch enabled for A/B testing.
Moved math_sse.zig and all 17 hook declarations + CriticalSection spin
count optimization from transform44 into new ssemaths module. Off by
default (-Dssemaths=true to enable).
transform44 retains its profiling hooks and blit_hub optimization.
ssemaths is a clean standalone module with its own mutex, install/remove
lifecycle, and lateInit for post-UnitXP hook clobbering.
Added performance note to math_sse.zig documenting that hook-based
replacement adds ~5-8 cycles overhead that makes small functions slower,
and that in-place patching is the path to realize the full 2-4x gains
shown in inlined benchmarks.
Section 12c (model_hdr+0x134): visibility byte animation pattern verified
from assembly at 0x7176C2-0x717774. Byte array indexing, crossfade output
at +0xCC (not +0xBC). Position track at entry+0x24 with 12-byte keyframes.
Section 12e (model_hdr+0x13C): all 10 tracks verified. Tracks 1-6 use
scalar float interpolation (findInterpIdx + 4-byte keyframes). Tracks 7-10
use getInterpolatedFloat (0x71AF20). Track offsets, gate checks, and output
positions all confirmed from assembly.
SSE dispatch remains disabled pending final testing.
File cache (filecache module):
- Moved from transform44 sub-module to standalone src/filecache/
- 2-way set-associative cache (32768 sets x 2 ways) with FNV-1a hash + finalizer
- Fixed negative cache hit crash: zero output params before returning 0
(FindFileInArchive reuses filename slot for out_outer_archive)
- Fixed path 2 crash: set out_outer_archive on all cache hit paths
- Fixed stale block_entry crash: cache block index instead of raw pointer,
recompute from archive+0x290 on each hit
- Fixed archive-freed crash: use game's FindAndIncrementResourceReference
(0x650780) instead of manual +0x38 increment -- validates archive is alive
- Periodic stats dump with projected time savings (hit=~1000cy vs miss=~30000cy)
Timer fix (transform44 sub-module, ported from VanillaFixes):
- TSC calibration via QPC reference over 500ms
- Enables TSC mode if game was using GetTickCount fallback
- NtSetTimerResolution for 0.5ms OS timer granularity
- SetProcessInformation to disable Windows 11 power throttling
- Always-on (no A/B toggle -- no measurable impact on Wine/Linux)
bone_sse.zig: Full standalone SSE reimplementation of the 17703-byte bone
transform engine (0x714260). All helper functions reimplemented inline
(findInterpolationIndices, interpolateAnimationKeyframes, scaleMatrix3x3,
ApplyTranslation, rotateByQuaternion). Currently disabled (A/B dispatch
commented out) due to NULL ofsValues crash under investigation.
transform44.zig: Add OnWorldUpdate (0x482EA0) hook for true per-frame
counting. Previous frame counter used executeSceneRenderPass which fires
multiple times per frame (shadows, reflections, spell effects), causing
A/B periods to be as short as 0.5s during combat instead of ~30s.
SCENEOBJECT_OFFSETS.md: Complete assembly-verified field offset map
(51 offsets) extracted from [EBX+N] patterns in transformMatrix4x4.
Corrects bone_runtime_base from +0x80 to +0x090.
Hook File_FindInArchive (0x6549a0) with a direct-mapped filename-verified
cache. First open does the full MPQ chain walk (~60K cycles), subsequent
opens hit the cache (~300 cycles). 80% hit rate in gameplay testing.
Cache design: 16384 entries, FNV-1a hash for slot index, raw filename
comparison (128 bytes) for collision safety. Stores outer_archive,
inner_archive, and block_entry per file. Negative cache for not-found files.
Refcount at +0x38 incremented on all output archives to match original
File_FindInArchive behavior (verified via Ghidra: FindAndIncrementResourceReference
at 0x650780, DecrementResourceReference at 0x6507e0).
Also: default build changed to ReleaseFast (works around Zig fastcall inreg
bug in Debug mode), logging gate changed to != ReleaseSmall, file cache
integrated as transform44 sub-module with A/B comparison timing in readout.
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.
Track frame-to-frame wall time via rdtsc delta at executeSceneRenderPass.
Stats dump now shows each function's cycles as % of total frame time,
plus rough ms estimate at 3GHz. Helps identify which functions dominate.