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.
84 lines
4.7 KiB
Markdown
84 lines
4.7 KiB
Markdown
# 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. The `SUB ESP,0x80` is 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:
|
||
|
||
1. **Pass 1 — Mark body**: Draw original geometry to stencil buffer (bit 0), no colour write.
|
||
2. **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.
|
||
3. **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
|
||
|
||
1. **Target** (golden amber `#FFC800`) — current target, 2.25px outline
|
||
2. **Raid-marked** (per-icon colour) — units with raid icons 1-8, 1.5px
|
||
3. **Dead player** (cyan `#00FFFF`) — deceased friendly players, 2.5px, through walls
|
||
|
||
## Per-frame Flow
|
||
|
||
1. `EndScene` fires → `tracker.scanObjects()` rebuilds GUID tracking → stencil cleared
|
||
2. Next frame: `ManageRenderListNode` classifies models → `DrawBatchProj` sets flags → `DIP` does three-pass rendering
|
||
3. 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/hook` inline hook library.
|
||
- Object scanning is frame-based (EndScene), not event-driven (no separate Idris runtime thread).
|
||
- Shader loading uses dynamic `d3dx9_43.dll` lookup; gracefully disabled if absent.
|