Files
WeirdUtils/docs/outline-port-notes.md
T
MarcelineVQ 25ed0b3780 Add outline rendering system ported from C++ to Zig
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.
2026-02-24 00:51:33 -08:00

4.7 KiB
Raw Blame History

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.