Files
WeirdUtils/docs/outline-port-notes.md
T
MarcelineVQ 2aaa089c76 Add module control API, bigcursor with fractional scaling, shared mutex
Module control API:
- Three cdecl exports: WeirdUtils_IsModuleActive, WeirdUtils_DisableModule, WeirdUtils_DisableAll
- C header include/weirdutils_api.h for runtime DLL discovery
- All modules gain pub module_name, isActive(), shared mutex via src/mutex.zig
- build.zig refactored to module_list array of ModuleDesc

Bigcursor:
- D3D9 vtable hooks on SetCursorProperties/ShowCursor
- Scale2x/Scale3x pixel-art upscalers in pure Zig (replaces 12K-line hqx C)
- Bilinear resampler for fractional scales (1.0-4.0, default 1.2)
- Win32 cursor via CreateIconIndirect bypasses D3D9 32x32 limit
- FNV-1a hash cache for ~16 cursor bitmaps
- Lua API: SetCursorScale(n) / GetCursorScale()
- CVar cursorScale for Config.wtf persistence (tenths)

Other:
- Add dpslog module stub
- Docs and README updates
2026-03-06 13:27:35 -08:00

84 lines
4.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.