Root cause of crashes was thread safety: libdeflate's decompressor struct
has mutable decode tables rebuilt per-block, so sharing between main thread
and FMOD audio thread caused wild writes. Fixed with a thread-local pool
keyed by Windows thread ID (FS:[0x24]).
Switched build ABI from msvc to gnu — eliminates stub headers, libdeflate
uses Zig's bundled MinGW libc. Relaxed zlib header filter from == 0x78 to
(CMF & 0x0F) == 0x08 to catch both 32K and 4K window sizes.
Benchmarked: stock=2681ms, per-call-alloc=1304ms (2.05x), tls=1194ms (2.2x)
on 84k calls. Zero errors, zero fallbacks in production.
DecompressData_WithOptions modifies the input buffer during overlap
handling. Our hook must save the input before calling the original,
then pass the saved copy to libdeflate. Also documents all compression
types, function signatures, and bugs found during integration.
libdeflate sanity test passes (hello → hello, cpu_features=0x8000001F).
All 93K decompression calls during loading are pure zlib (type 0x02).
Header bytes 78 9C confirm standard zlib-wrapped deflate. No PKWare,
bzip2, or ADPCM observed.
Volume: 170MB compressed → 380MB decompressed in 2.9s during loading,
~5K calls/period during gameplay.
Format: [0x02] [zlib_stream...] — straightforward for libdeflate.
Previous crashes were from reading out_size AFTER original modified it
and from writing to out_buf unsafely.
Hooks DecompressData_WithOptions (0x661A80) to run both the original
zlib inflate and libdeflate on the same data. Times both, logs the
comparison every ~450 frames, counts mismatches.
malloc/free provided via game's Storm memory manager (SMemAlloc/SMemFree)
since we link without libc on the msvc target.
libdeflate compiled with -g0 to avoid .debug_frame COFF section name
warning. AVX-512 disabled (32-bit), SSE2/AVX2 paths active.
Vendored libdeflate decompress-only C sources into src/performance/.
Compiled as static lib (x86-windows-gnu for libc headers), linked into
the DLL. AVX-512 disabled (not available on 32-bit x86), SSE2/AVX2
paths active.
Target: replace WoW's embedded zlib inflate (~3% CPU) with libdeflate's
~2.3x faster implementation. Hook integration next.
- performance module default=true, transform44 and silicon default=false
- Silicon JMP patches (22 functions) moved into performance.zig directly
- Single flag: 'zig build' gives all optimizations, zero profiling
- Dev mode: 'zig build -Dtransform44=true' for A/B testing
Clean module at src/performance/ with all verified permanent
optimizations and zero profiling overhead. No rdtsc, no A/B testing,
no probe counters, no dumpStats.
Hooks: transformMatrix4x4 bone SSE, RenderParticleSprites SSE,
GetOrCreateCharacterGlyph cache, OnWorldUpdate cache reset,
teardown guard.
SSE compilation units (bone_sse, particle_sse, clip_sse, silicon_sse)
moved to src/performance/ as canonical location.
Build: zig build -Dperformance=true -Dsilicon=true
(transform44 module remains available for profiling/development)
Color channel packing missing & 0xFF after >> 14 extraction — upper
bits bled into adjacent channels causing broken particle fading.
Same class of bug as the alpha output fix earlier.
Added particle_sse_reference.zig (faithful recreation from commit
574f96f) as a separate compilation unit for correctness comparison.
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)
10% of frame time, ~275 calls/frame, ~5800 cyc/call. Builds identity
matrices (32 stores of dead work), does 1-5 matmul calls, copies to
g_worldMatrix global. Translation matrix is identity+offset — matmul
chain can be simplified to direct computation.
- V4 store (vmovups) writes xyz+color as one 16-byte op instead of 4
scalar stores. Unaligned but still 1 μop on modern CPUs.
- inline for unrolls 4-vertex loops, letting LLVM schedule stores
across vertices and fill pipeline bubbles.
- Hoisted world_pos to locals to prevent array re-reads.
- A/B: BASELINE ~433ms → CUSTOM ~299ms (~31% per-period reduction).
Total from original: 520ms → 299ms = 42% reduction.
- 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).
- Inline calcColor: eliminates function call, allows OoO overlap of
cache misses on colorCtx with vertex math. Pow path falls back to
game function.
- Inline mat*vec3 transform: V4 FMA chain replaces call to 0x7BCA80.
- VBState: cache VB pointers/strides in locals, write back once after
4 vertices. Eliminates ~80 pointer re-reads per particle.
- @mulAdd throughout vertex loops for FMA codegen.
- A/B verified: BASELINE ~470ms → CUSTOM ~382ms (~20% 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)
Assembly dumps and research doc for RenderParticleSprites (1.73% CPU),
calculateColorValues (0.63%), SetupParticleRendering, and
ProcessActiveParticles. Identifies SSE opportunities: V4 color interp,
billboard vertex math, rotation block. Plan for particle_sse.zig.
- 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.
All bitwise & and | comparisons were missing parentheses — Zig's ==
and != bind tighter than & and |, so `flags & 0x8 == 0` parsed as
`flags & (0x8 == 0)` = `flags & 0` = always 0.
Affected: si_frustumCullBBox (behind-camera check never ran, occlusion
flag check always passed), si_processLinkedListCollision (early-out
never triggered, node skip broken, AABB hit test only checked X-axis).
Also adds fastRecip (vrcpss+NR) and cvtss2si helpers for frustumCull
perspective divide optimization (2.0x speedup, was 1.6x).
- 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.
Log file writer queues events and flushes after 3s delay, re-resolving
GUIDs at write time so names from later packets can fill in. Flush
triggers on each new event (for old entries) and on DestroyObjectManager
hook before the object manager is torn down.
- Fix raid roster access: 0xB712A8 is a flat array, not pointer-to-array
- Fix SPELL_GO hit targets: full u64 GUIDs, not packed
- Fix GetLocalizedText calling convention: __fastcall not __cdecl
- Fix isUnknownName filtering at dpslog use sites (not in shared wow.zig)
- Fix dropdown icon tCoords persisting across reload
- Add pfQuest-style quest available icon (gold exclamation mark BLP)
- Hook DestroyObjectManager for log flush instead of World_HandleLogoutCleanup
- Null GUID handling: nil name, 0x80000000 flags in both Lua and file paths
- Document combat log file writer in DLL_README
- Write WotLK-format CSV to Logs\WeirdCombatLog.txt when /combatlog is
active (checks COMBAT_LOG_HANDLE at 0xB50544). Resolves names, flags,
spell names eagerly per event via same CLEU buffer resolution as
CombatLogGetCurrentEventInfo. Uses Win32 WriteFile with OS caching.
- Null GUIDs output as 0x0000000000000000,nil,0x80000000,0x0 matching
WotLK format. Empty resolved names output as nil not "".
- DPSLog addon: every subevent has unique chat handler with correct name
via factory functions.
- DAMAGE_SHIELD scans victim's auras (not attacker's) — shield owner
is the victim in the packet.
- DAMAGE_SHIELD now resolves spell ID by scanning the victim's (shield
owner's) aura descriptors for SPELL_AURA_DAMAGE_SHIELD (effect 15)
matching the packet school. Uses new deferred cleuDamageShieldSpell
buffer entry resolved lazily at CombatLogGetCurrentEventInfo time.
- Fix: scan victim's auras, not attacker's — the shield aura (Thorns,
Retribution Aura) is on the unit being hit, not the one hitting.
- DPSLog addon: every unique subevent now has its own chat handler
with correct name. Factory functions replace shared handlers that
were printing wrong subevent names (e.g. "SPELL_DAMAGE" for all
damage types).
- Remove CombatLogGetCurrentEventInfo rdtsc profiling (measured ~12k
cycles/call = ~4us, negligible).
- Defer all name resolution, flag computation, and spell lookups to
CombatLogGetCurrentEventInfo() request time. Fire functions store
only raw GUIDs (cleuGuid) and spell IDs (cleuSpellId). Resolution
happens lazily when addons call the function, after packet/update
processing is complete and the object manager is stable.
- Add aura caster tracking: cast ring buffer (32 entries) records
(caster, target, spellId) from SPELL_GO hit targets. Aura applied
hook correlates with recent casts to infer caster. Persistent cache
(128 entries) maps (unitGUID, slot) -> casterGUID for aura removal.
- Fix creature name resolution: use CGUnit_C::GetNameFromCacheOrUnknown
(0x609210) via object pointer for non-player GUIDs. Safe during lazy
resolution since object manager is stable at that point.
- Fix isInRaid crash: validate raid roster entry pointers with
isValidPtr before dereferencing. Offline raid members have stale
entry pointers (value 4) that passed the != 0 check but crashed
on deref. Same pattern worldmarkers already guards against.
- Strip name/school parameters from all 14 fire functions — no longer
needed since resolution is deferred. Reduces per-event work to just
storing raw IDs in the CLEU buffer.
InitializeGameEngine re-registers all packet handlers on every login,
overwriting our swaps. The NetClient pointer may be reused so comparing
it doesn't detect re-registration. Fix: unconditionally reset swap_count
and re-install all swaps whenever InitializeGameEngine fires.
Both DPSMate and WSBT adapters now call CombatLogGetCurrentEventInfo()
exactly once per event, unpacking all args into p1..p12 locals.
Remove redundant relevance check in WSBT profiling wrapper.
Major architectural changes:
- Migrate 12 packet handlers from JMP-patching Detours to NetClient opcode
handler table pointer swaps. No code bytes modified — only heap pointers.
Invisible to Warden memory scans. Hook InitializeGameEngine (0x401570,
__thiscall) to install swaps after all handlers are registered.
- Implement CombatLogGetCurrentEventInfo() — WotLK-style lazy arg retrieval.
Event fires with no args; addons call the function to get all fields from
a C-side buffer. No arg count limit (bypasses ExecuteLuaCallback 19-arg cap).
Includes unit flags, raid flags, and all suffix fields.
- Compute COMBATLOG_OBJECT_* unit flags: affiliation (MINE/PARTY/RAID/OUTSIDER),
reaction (FRIENDLY/NEUTRAL/HOSTILE via UnitReaction), control (PLAYER/NPC),
type (PLAYER/NPC/PET/GUARDIAN/OBJECT via SUMMONEDBY check), special (TARGET),
and raid target markers.
- Fix boolean fields (critical/glancing/crushing): changed from %d (Lua number 0,
which is TRUTHY) to nil/"1" via boolToLua(). Details addon was counting every
hit as critical because it uses truthiness checks.
- SPELL_INSTAKILL now fires from SPELLLOGEXECUTE (has caster GUID).
Standalone SMSG_SPELLINSTAKILLLOG handler is pass-through only.
- SPELL_EXTRA_ATTACKS now uses target GUID from packet (was discarded).
- SPELL_CAST_START/SUCCESS now parse spell target from packet.
- SPELL_DRAIN subevent added for instant power drain effects.
- Rename event from COMBAT_LOG_EVENT to COMBAT_LOG_EVENT_UNFILTERED.
- Update DPSMate and WSBT CLEU adapters to use CombatLogGetCurrentEventInfo().
- Remove unnecessary `or 0` guards on numeric fields (always non-nil from C).
Adds DPSMate_CLEUAdapter.lua -- replaces DPSMate's string-parsing
CHAT_MSG_* system with structured COMBAT_LOG_EVENT data from our DLL.
Adapter maps CLEU subevents directly to DPSMate.DB API calls:
DamageDone, DamageTaken, EnemyDamage, Healing, HealingTaken,
DeathHistory, Kick, Dispels, BuildBuffs, CCBreaker, etc.
Eliminates all strfind pattern matching from the combat log path.
Toggle: /dpscleu (on/off/status)
Benchmark: /dpsbench -- always-on per-combat A/B profiling.
Resets at combat start, reports at combat end, flips mode for next
combat. Reports events, total ms, us/event, GC delta, and percentage
comparison between CLEU and original modes.
Starts in CLEU mode by default.
- DLL_README.md: full feature description, GetSpellInfo API, module name in dev API list
- RELEASE_NOTES.md: updated DPSLog entry with WotLK parity details, removed from gaps list
- README.md: updated module table