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.
Probe infrastructure using comptime probeDetour() that generates
per-function detours: atomic counter increment + callOriginal passthrough.
Hit counts reported on shutdown. Hooks installed at lateInit (engine init),
not DLL load time.
Key findings during decompilation:
- 0x40CF81 is GetFPUControlWord, not ftol (silicon mislabel)
- Real __ftol at 0x40A2B0 (51+ callers, hot path)
- 0x7B7A80/7B7B10 use normal stack floats, not FPU register params
- 0x686640/686820/6868E0 are bounding volume ops, not vector ops
- 0x7786A0 is a UI model constructor, not SetModelLighting
Complete catalog of libSiliconPatch240.dll hook targets with verified
calling conventions, parameter layouts, RET stack cleanup, and functional
descriptions from Ghidra analysis. Zero unknown stubs remain.
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.
Reads quest giver status from unit+0xCB8 (populated by client's
SMSG_QUESTGIVER_STATUS packet handler) to show yellow ! icons on the
minimap for NPCs with available quests. Uses the vanilla gossip frame
AvailableQuestIcon texture. Enabled by default, togglable from the
tracking dropdown like other NPC categories.
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.
Inlined benchmarks use x87 inline asm vs direct Zig @Vector code with
no CALL/RET on either side. Shows the true instruction-level comparison
that in-place patching would achieve:
dotProduct: x87=3 SSE=1 -> 3.0x (was 0.3x when called)
evalPoly: x87=4 SSE=1 -> 4.0x (was 0.7x when called)
vec3MulScal: x87=3 SSE=2 -> 1.5x (was 0.8x when called)
Every "loser" from the called benchmarks flips to a winner when inlined.
The entire performance gap was function call overhead (~5 cycles), not
instruction quality. Confirms in-place patching as the right strategy.
Also added RADV_TEX_ANISO env var exploration item to release notes and
updated math polyfill section with full benchmark breakdown.
Added V4 vector helpers (loadV3, storeV3, dot3) and rewrote several
functions to use @Vector(4, f32) operations instead of scalar f64:
- planeNormal: 0.7x -> 1.7x (79 -> 33 cyc) -- V4 cross + normalize
- transformAABox: 0.7x -> 1.2x (118 -> 69 cyc) -- f32 + @min/@max
- evaluatePolynomial: 0.4x -> 0.6x (24 -> 16 cyc) -- drop f64 promotion
- vec3MulScalar: 0.7x -> 0.8x -- V4 splat multiply
- dotProduct/squaredMagnitude: rewritten with dot3 helper
crossProduct reverted from shuffle-SIMD back to scalar -- shuffles added
latency that the x87 pipeline doesn't have (16 vs 13 cyc).
Remaining losers (dotProduct 0.4x, evalPoly 0.6x) are at the function
call overhead floor -- the original x87 is 5-11 cycles, which is close
to bare call/ret cost.
In-place functions (vec3MulAssign, scaleByVec, etc.) were showing fake
speedups (121x, 10x) because repeated application on the same data
caused values to overflow to Inf/NaN. x87 FPU traps on denormals while
SSE handles them in hardware, making the comparison meaningless.
Now each iteration resets from a template copy. Real results show these
functions are ~1.0x (neutral), not 100x wins. The actual winners are
rotMat3x3/4x4 at 2.1x, and several functions where our scalar-through-
pointers approach is slower than the original x87 pipeline (dotProduct
0.3x, evaluatePolynomial 0.4x) — candidates for real SIMD optimization.
Extracts original x87 FPU bytes from WoW.exe via Ghidra, mmaps them
executable, and benchmarks against our SSE replacements. Covers all 17
UnitXP polyfill functions with correctness validation and cycle counts.
Maps a page at 0x7ff000 for the float 1.0 constant referenced by
rotMat3x3/rotMat4x4/planeNormal via absolute address 0x7ff9d8.
Build: zig build bench / zig build run-bench
Was __stdcall with stack param — would corrupt stack for every visible
particle emitter. Fixed to __fastcall(ECX=ptr) matching assembly at
0x717EB8: MOV ECX,[EBP-0x10]; CALL 0x7B5F60. Plain RET, no stack cleanup.
Bug #14 found during final verification pass.
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.
Lootable unit corpses (UNIT_DYNFLAG_LOOTABLE) now click-through over
blocking players, matching the existing behavior for interactable NPCs
and game objects. Adds isLootable() check to the interactable NPC test
and UNIT_DYNAMIC_FLAGS descriptor offset to shared offsets.
Also adds RELEASE_NOTES.md for tracking changes between releases, and
adds a checklist item in RELEASING.md to review it before publishing.
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.
- Eliminate hardcoded module_names in addons.zig — now derived from
build.zig via all_module_names build option
- Add module_active.zig runtime registry: main.zig registers isActive
pointers during install(), addons.zig queries them without importing
each module directly
- Prune embedded file prefixes at startup: after all modules claim
mutexes, build a runtime active_prefixes table excluding modules we
don't own. findEmbeddedFile searches only active entries — no
per-lookup isActive check on the hot path
- Tag both addon and asset prefixes with module_name so pruning applies
to all embedded files for a module
- Rename src/markers/ → src/worldmarkers/, src/outline/api.zig →
src/outline/outline.zig to follow {name}/{name}.zig convention
Registers GetAddOnMemoryUsage, UpdateAddOnMemoryUsage, GetAddOnCPUUsage,
UpdateAddOnCPUUsage, ResetAddOnCPUUsage, and GetScriptCPUUsage as global
Lua functions. All stubs for now — implementation will hook lua_Alloc
and FrameScript dispatch for per-addon tracking.
@hasField only works on struct fields, not module declarations.
build_options is a module, so @hasField always returned false,
silently skipping all addon file embedding and registration.
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.
Build options now provide addon_name, addon_hidden, and file lists per
module. addons.zig derives everything from these — no hardcoded module
list. Added addon_hidden flag (WorldMarkers uses it to stay unlisted).
Changed dpslog default to enabled.
Resize hook now expands event array to 801 slots. Dynamic slot search
scans from slot 650 for first empty entry (compatible with SuperWoW).
Fixed ProcessEnvironmentalDamage parameter order — Ghidra mislabeled
damageSource as damage. Lua chat output now fires only on first subevent.
Hook 23 packet handlers / internal functions to fire a single
COMBAT_LOG_EVENT with WotLK-style subevent strings and structured
args (spellId, amount, school, etc.) for addon consumption.
Event registration writes directly into the internal FrameScript
event table at slot 650, with a resize_lua_event_array hook that
expands capacity to 700 when needed (compatible with SuperWoW).
Includes Lua tracker addon (WeirdUtils_DPSLog) with popup checklist
UI, per-subevent chat output, and /dpslog slash command.
When cursor hits an object (hitType=2), re-call WorldIntersectionTest
with flags=0 to get terrain position behind it. Replaces the GUID-based
unit position lookup which failed for GOs (no movement struct).
Extract duplicated WoW 1.12.1 addresses and game accessor functions
into shared modules (src/offsets.zig, src/wow.zig), replacing 5+
copies of getObjectByGUID, isInBattleground, isValidPtr, etc.
- src/offsets.zig: shared address constants (object manager, descriptor
fields, map/zone, core function addresses, D3D9/GX)
- src/wow.zig: shared accessor functions (pointer validation, object
manager traversal, field reads, unit helpers, battleground detection,
game function wrappers, raid target cache)
- Update 10 modules to import from shared instead of inline constants
- Remove outline/wow.zig (promoted to src/wow.zig)
- Trim outline/offsets.zig and markers/offsets.zig to module-specific only
Replace console.zig with logging.zig: per-module Logger with auto
[name] prefix, optional file output, and destination routing. Convert
all modules from manual [name] prefixes and global con.print to Logger
instances. Remove redundant "Module loaded" lines. Replace
OutputDebugStringA in outline/tracker with Logger. Add dpslog module
with structured combat log events (SPELL_DMG, PERIODIC, HEAL, MELEE).
Add clickthrough module and bigcursor D3D9 cursor scaling.