Port the model outline hook system (CM2SceneRenderDraw, ManageRenderListNode, DrawBatchProjected) from the C++ idris DLL to pure Zig. Includes D3D9 vtable hooks for stencil-based outline rendering with per-category colors and thickness. Fix GetObjectByGUID calling convention: was using fastcall (ECX/EDX) but Ghidra confirms it's __stdcall with stack params and RET 8. Fix lua_pushcclosure address from 0x6F3B80 (mid-body of another function) to 0x6F3920. Guard resolveModelOwner in DrawBatchProj with hasTargets() check and cache unit model status during ManageRenderListNode to avoid raw pointer chasing at render time. Add IsBadReadPtr validation in resolveModelOwner to match C++ IsValidReadPtr pattern for page-level memory safety.
4.7 KiB
Outline Port Notes
Pure Zig reimplementation of the WoW 1.12.1 unit outline system, ported from the Idris C/C++ reference at /media/storage/projects/idris/dlls.
Hooked Functions
Model pipeline (inline hooks via libs/hook)
| Address | Function | Convention | Prologue | Fixups | Notes |
|---|---|---|---|---|---|
0x0070b360 |
CM2SceneRenderDraw |
__thiscall |
9 bytes | none | Batch reordering for outline priority |
0x00710b90 |
CM2Model_ManageRenderListNode |
__thiscall |
6 bytes | none | Classifies models on render-list add |
0x0070cb30 |
CM2Scene_DrawBatchProjected |
__fastcall |
6 bytes | none | Flags DIP hook for outline rendering |
Prologue verification (Ghidra, WoW.exe 1.12.1 build 5875):
0x0070b360:55 8B EC 81 EC 80 00 00 00—PUSH EBP; MOV EBP,ESP; SUB ESP,0x80. Boundaries at +1, +3, +9. TheSUB ESP,0x80is a 6-byte instruction (81 EC + imm32) spanning offset +3..+9, so 6-byte overwrite is unsafe — changed to 9.0x00710b90:55 8B EC 8B 45 08—PUSH EBP; MOV EBP,ESP; MOV EAX,[EBP+8]. Boundaries at +1, +3, +6. Clean 6-byte boundary.0x0070cb30:55 8B EC 83 EC 10—PUSH EBP; MOV EBP,ESP; SUB ESP,0x10. Boundaries at +1, +3, +6. Clean 6-byte boundary.
All three use buildFastcallToCdeclThunk to bridge to callconv(.c) detour functions (since __thiscall is __fastcall with unused EDX).
D3D9 (vtable patching via dummy device)
| VTable Index | Method | Purpose |
|---|---|---|
| 42 | EndScene |
Per-frame object scan, stencil clear |
| 82 | DrawIndexedPrimitive |
Three-pass stencil outline rendering |
| 16 | Reset |
Force D24S8 depth/stencil format |
Vtable obtained by creating a temporary IDirect3DDevice9 via Direct3DCreate9 → CreateDevice with a hidden window. All D3D9 devices share the same vtable, so patching affects the game's device.
Outline Rendering (DIP hook)
Three-pass stencil approach per outline model:
- Pass 1 — Mark body: Draw original geometry to stencil buffer (bit 0), no colour write.
- Pass 2 — Draw outline: Screen-space vertex shader expands vertices along normals. Stencil test rejects body pixels. Write outline bit 1. Dead players disable depth test (through-wall); targets/raid marks respect depth.
- Pass 3 — Normal draw: Restore all state, draw model normally on top.
Vertex Shader
Compiled at runtime via D3DXAssembleShader from d3dx9_43.dll (loaded dynamically). Falls back to no outlines if the DLL isn't present.
Format: vs_2_0, uses WoW's bone matrix constants (c[idx+31..33]), view-projection at c2-c5, custom constants at c251 (bone scale) and c252 (pixel thickness).
Pixel shader: ps_3_0, outputs solid colour from c0.
Key Offsets
| Address/Offset | Purpose |
|---|---|
0x00B41414 |
Object Manager pointer |
+0xAC |
First object in linked list |
+0xA4 |
Base for next-object traversal |
+0xC0 |
Local player GUID (from ObjMgr) |
0x00B71368 |
Raid target GUID array (8 × 8 bytes) |
0x515970 |
UnitGUID(__fastcall, string_ECX→EAX:EDX) |
0x464870 |
GetObjectByGUID(__stdcall, lo_stack, hi_stack→EAX) — NOT fastcall! |
0x6061E0 |
UnitReaction(__thiscall, player_ECX, unit_stack→int) |
model+0x28 |
Direct owner object pointer |
model+0x3C0 |
Callback owner object pointer |
ctx+0x3310 |
Model pointer in render context |
Category Priority
- Target (golden amber
#FFC800) — current target, 2.25px outline - Raid-marked (per-icon colour) — units with raid icons 1-8, 1.5px
- Dead player (cyan
#00FFFF) — deceased friendly players, 2.5px, through walls
Per-frame Flow
EndScenefires →tracker.scanObjects()rebuilds GUID tracking → stencil cleared- Next frame:
ManageRenderListNodeclassifies models →DrawBatchProjsets flags →DIPdoes three-pass rendering - One-frame latency for tracking updates (imperceptible)
Differences from Reference
- No
std::unordered_set/std::unordered_map— fixed arrays with linear search (max 64 dead GUIDs, 8 raid marks, 256 outline models). - No
CriticalSection— all hooks run on the main WoW thread; no synchronisation needed. - No MinHook — uses the project's existing
libs/hookinline hook library. - Object scanning is frame-based (EndScene), not event-driven (no separate Idris runtime thread).
- Shader loading uses dynamic
d3dx9_43.dlllookup; gracefully disabled if absent.