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
This commit is contained in:
+17
-17
@@ -8,9 +8,9 @@ that runs entirely in EndScene, composited on top of the final backbuffer.
|
||||
|
||||
The outline system has three main phases per frame:
|
||||
|
||||
1. **Object scan** (EndScene start) — identify which game objects should be outlined
|
||||
2. **DIP hook** (during game rendering) — cache draw calls and write stencil marks
|
||||
3. **JFA pipeline** (EndScene, after game rendering) — produce and composite outlines
|
||||
1. **Object scan** (EndScene start) - identify which game objects should be outlined
|
||||
2. **DIP hook** (during game rendering) - cache draw calls and write stencil marks
|
||||
3. **JFA pipeline** (EndScene, after game rendering) - produce and composite outlines
|
||||
|
||||
## Files
|
||||
|
||||
@@ -44,17 +44,17 @@ state to determine outline visibility.
|
||||
`CM2SceneRenderDraw` receives a flat array of M2 batch indices. The hook partitions
|
||||
them into 3 groups before calling the original function:
|
||||
|
||||
- **Group 1 — depth-priority models**: game object M2s and the local player's M2s.
|
||||
- **Group 1 - depth-priority models**: game object M2s and the local player's M2s.
|
||||
These render first so their depth is in the buffer when stencil marks are written.
|
||||
The local player occludes outlines (they're the camera reference point).
|
||||
If the local player IS an outline target, their models go in group 2 instead
|
||||
(the outline check takes priority in the partition logic).
|
||||
|
||||
- **Group 2 — outline targets**: models belonging to tracked entities (current target,
|
||||
- **Group 2 - outline targets**: models belonging to tracked entities (current target,
|
||||
raid-marked units, dead friendly players). The DIP hook intercepts these draws to
|
||||
cache parameters and write stencil=1 where they pass the depth test.
|
||||
|
||||
- **Group 3 — everything else**: other players, their gear, NPCs, creatures.
|
||||
- **Group 3 - everything else**: other players, their gear, NPCs, creatures.
|
||||
These render last. Their depth is NOT in the buffer when stencil marks are written,
|
||||
so outlines show through them. The outline composites on top in EndScene regardless.
|
||||
|
||||
@@ -73,14 +73,14 @@ them into 3 groups before calling the original function:
|
||||
The DIP hook writes stencil marks during outline target rendering (group 2):
|
||||
|
||||
- `STENCILFUNC = ALWAYS`, `STENCILPASS = REPLACE`, `STENCILREF = 1`
|
||||
- `STENCILZFAIL = KEEP` — pixels behind depth-tested geometry keep stencil=0
|
||||
- `STENCILZFAIL = KEEP` - pixels behind depth-tested geometry keep stencil=0
|
||||
- After each outline DIP, `STENCILWRITEMASK` is set to 0 to protect marks from
|
||||
subsequent draws (group 3 models could otherwise overwrite them)
|
||||
- Exception: dead players skip stencil entirely (`STENCILENABLE = 0`) so their
|
||||
outlines are visible through walls for corpse finding
|
||||
|
||||
EndScene Phase 1 uses `STENCILFUNC = EQUAL`, `STENCILREF = 1` to gate the
|
||||
silhouette replay — only pixels marked as visible get silhouette color.
|
||||
silhouette replay - only pixels marked as visible get silhouette color.
|
||||
|
||||
Stencil is cleared to 0 after Phase 1 to avoid affecting the next frame.
|
||||
|
||||
@@ -89,16 +89,16 @@ Stencil is cleared to 0 after Phase 1 to avoid affecting the next frame.
|
||||
After Phase 1 produces the silhouette RT (A8R8G8B8), the JFA pipeline generates
|
||||
outlines via distance field:
|
||||
|
||||
1. **JFA Init** — seed the distance field from the silhouette. Pixels with
|
||||
1. **JFA Init** - seed the distance field from the silhouette. Pixels with
|
||||
silhouette content (alpha >= 0.002) output their own UV as a seed.
|
||||
Empty pixels output sentinel (-1, -1) which is outside UV space [0,1]
|
||||
so it never wins distance comparisons.
|
||||
|
||||
2. **JFA Propagation** — 4 passes at step sizes [8, 4, 2, 1], ping-ponging
|
||||
2. **JFA Propagation** - 4 passes at step sizes [8, 4, 2, 1], ping-ponging
|
||||
between two G16R16F render targets. Each pass does a 9-tap sample
|
||||
(self + 8 neighbors at step distance) and keeps the nearest seed UV.
|
||||
|
||||
3. **JFA Decode + Composite** — compute pixel-space distance from each pixel
|
||||
3. **JFA Decode + Composite** - compute pixel-space distance from each pixel
|
||||
to its nearest seed. If distance < outline width AND the pixel is outside
|
||||
the silhouette interior, output the outline color with alpha blending.
|
||||
|
||||
@@ -121,7 +121,7 @@ Width is encoded as `alpha = pixels / 4.0` in the silhouette, decoded as
|
||||
| `rt_jfa_a_tex` | G16R16F | JFA ping buffer (seed UV coordinates) |
|
||||
| `rt_jfa_b_tex` | G16R16F | JFA pong buffer |
|
||||
|
||||
## DIP Hook — Draw Caching
|
||||
## DIP Hook - Draw Caching
|
||||
|
||||
The DIP hook does NOT draw silhouettes inline (that corrupts WoW's GxDevice
|
||||
internal render state). Instead it:
|
||||
@@ -147,7 +147,7 @@ Each frame, `scanObjects()` iterates the WoW object manager and collects:
|
||||
When `CM2Model_ManageRenderListNode` fires for each model being added to the
|
||||
render list, `classifyModel()` reads the model's owner back-pointers
|
||||
(`model+0x28` direct, `model+0x3C0` callback) and matches them against the
|
||||
collected object pointers. No pointer dereferencing of unknown memory — just
|
||||
collected object pointers. No pointer dereferencing of unknown memory - just
|
||||
value comparison against the validated set from the object manager.
|
||||
|
||||
## Outline Categories
|
||||
@@ -162,7 +162,7 @@ value comparison against the validated set from the object manager.
|
||||
|
||||
1. `api.init()` installs model hooks immediately (ManageRenderListNode,
|
||||
DrawBatchProjected, CM2SceneRenderDraw)
|
||||
2. D3D9 hooks are **deferred** until the first model hook fires — creating a
|
||||
2. D3D9 hooks are **deferred** until the first model hook fires - creating a
|
||||
dummy D3D9 device during engine init corrupts the proxy's state
|
||||
3. `api.initD3D9Deferred()` patches the D3D9 vtable (EndScene, DIP, Reset)
|
||||
4. Reset hook forces D24S8 depth/stencil format (8 stencil bits required)
|
||||
@@ -189,7 +189,7 @@ OutlineCommand("off") -- disable outlines
|
||||
|
||||
- **Local player outline (planned)**: a Gaussian blur outline mode for the local
|
||||
player to improve visibility in combat when surrounded by mobs. Separate from
|
||||
the JFA pipeline — will use blur difference (blur silhouette, subtract original,
|
||||
the JFA pipeline - will use blur difference (blur silhouette, subtract original,
|
||||
threshold) for a softer glow effect and only apply to other players.
|
||||
|
||||
- **Death tracking needs improvement**: currently uses `UNIT_FLAG_DEAD` which is
|
||||
@@ -197,10 +197,10 @@ OutlineCommand("off") -- disable outlines
|
||||
- Dead players should be outlined (works now)
|
||||
- Released bodies (corpse objects) should also be outlined (partially works via `.corpse`
|
||||
type, but needs verification that released-but-not-skeleton corpses are caught)
|
||||
- Feign Death must NOT trigger the dead outline — feign death sets the dead flag
|
||||
- Feign Death must NOT trigger the dead outline - feign death sets the dead flag
|
||||
but the player is alive. Need to check for the feign death aura/buff or use a
|
||||
more specific death condition
|
||||
- The local player should never get a death outline on themselves — the purpose
|
||||
- The local player should never get a death outline on themselves - the purpose
|
||||
of the death outline is to help the player find and resurrect others, not to
|
||||
highlight their own corpse
|
||||
- Marker outlines should not persist on units after they die
|
||||
|
||||
+13
-31
@@ -11,16 +11,17 @@ const model_hook = @import("model_hook.zig");
|
||||
const d3d9_hook = @import("d3d9_hook.zig");
|
||||
|
||||
const WINAPI = std.builtin.CallingConvention.winapi;
|
||||
extern "kernel32" fn CreateMutexA(lpMutexAttributes: ?*anyopaque, bInitialOwner: i32, lpName: [*:0]const u8) callconv(WINAPI) ?*anyopaque;
|
||||
extern "kernel32" fn ReleaseMutex(hMutex: *anyopaque) callconv(WINAPI) i32;
|
||||
extern "kernel32" fn CloseHandle(hObject: *anyopaque) callconv(WINAPI) i32;
|
||||
extern "kernel32" fn GetLastError() callconv(WINAPI) u32;
|
||||
extern "kernel32" fn GetCurrentProcessId() callconv(WINAPI) u32;
|
||||
const ERROR_ALREADY_EXISTS: u32 = 183;
|
||||
const mod_mutex = @import("../mutex.zig");
|
||||
|
||||
pub const module_name: [*:0]const u8 = "outline";
|
||||
|
||||
var g_mutex: ?*anyopaque = null;
|
||||
var g_is_hook_owner: bool = false;
|
||||
|
||||
pub fn isActive() bool {
|
||||
return g_is_hook_owner;
|
||||
}
|
||||
|
||||
/// Install model hooks immediately. D3D9 hooks are deferred until the first
|
||||
/// model hook fires (i.e. the game is actively rendering), because creating a
|
||||
/// dummy D3D9 device during engine init corrupts the d3d9 proxy's state and
|
||||
@@ -28,29 +29,17 @@ var g_is_hook_owner: bool = false;
|
||||
pub fn init() bool {
|
||||
con.print("[outline] Module loaded\n");
|
||||
|
||||
// Multi-DLL safety: only one instance per process should hook
|
||||
var mutex_name_buf: [64]u8 = undefined;
|
||||
const mutex_name = std.fmt.bufPrint(&mutex_name_buf, "Local\\WeirdUtils_OutlineHook_{d}", .{GetCurrentProcessId()}) catch return false;
|
||||
mutex_name_buf[mutex_name.len] = 0;
|
||||
|
||||
g_mutex = CreateMutexA(null, 1, @ptrCast(mutex_name_buf[0..mutex_name.len :0]));
|
||||
if (g_mutex == null) return false;
|
||||
|
||||
if (GetLastError() == ERROR_ALREADY_EXISTS) {
|
||||
_ = CloseHandle(g_mutex.?);
|
||||
g_mutex = null;
|
||||
g_is_hook_owner = false;
|
||||
con.print("[outline] Another DLL owns hooks (mutex taken), skipping\n");
|
||||
return true;
|
||||
}
|
||||
g_is_hook_owner = true;
|
||||
const result = mod_mutex.acquire(module_name);
|
||||
g_mutex = result.handle;
|
||||
g_is_hook_owner = result.is_owner;
|
||||
if (!g_is_hook_owner) return true;
|
||||
|
||||
if (!model_hook.installHooks()) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Called from the first model hook callback, once rendering is active.
|
||||
/// Safe to create the dummy D3D9 device now — the game's real device is
|
||||
/// Safe to create the dummy D3D9 device now - the game's real device is
|
||||
/// fully initialised and the proxy's state is stable.
|
||||
pub fn initD3D9Deferred() void {
|
||||
_ = d3d9_hook.installHooks();
|
||||
@@ -61,14 +50,7 @@ pub fn cleanup() void {
|
||||
if (g_is_hook_owner) {
|
||||
d3d9_hook.removeHooks();
|
||||
model_hook.removeHooks();
|
||||
}
|
||||
|
||||
if (g_is_hook_owner) {
|
||||
if (g_mutex) |m| {
|
||||
_ = ReleaseMutex(m);
|
||||
_ = CloseHandle(m);
|
||||
g_mutex = null;
|
||||
}
|
||||
mod_mutex.release(&g_mutex);
|
||||
}
|
||||
g_is_hook_owner = false;
|
||||
}
|
||||
|
||||
+143
-114
@@ -126,7 +126,7 @@ const CachedDraw = struct {
|
||||
num_verts: u32 = 0,
|
||||
start_idx: u32 = 0,
|
||||
prim_count: u32 = 0,
|
||||
// GPU state (AddRef'd COM objects — released after replay)
|
||||
// GPU state (AddRef'd COM objects - released after replay)
|
||||
vb: ?*anyopaque = null,
|
||||
vb_offset: u32 = 0,
|
||||
vb_stride: u32 = 0,
|
||||
@@ -136,7 +136,7 @@ const CachedDraw = struct {
|
||||
// Per-model outline info
|
||||
color: u32 = 0,
|
||||
category: types.ModelCategory = .none,
|
||||
// VS constants (bone matrices, world/view/proj) — copied by value
|
||||
// VS constants (bone matrices, world/view/proj) - copied by value
|
||||
vs_consts: [MAX_VS_CONST_REGS][4]f32 = undefined,
|
||||
};
|
||||
|
||||
@@ -197,8 +197,7 @@ fn deviceSetPtrOrNull(dev: *anyopaque, idx: usize, ptr: ?*anyopaque) void {
|
||||
:
|
||||
: [self] "r" (@intFromPtr(dev)),
|
||||
[func] "r" (func_addr),
|
||||
: .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true }
|
||||
);
|
||||
: .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,8 +250,7 @@ fn deviceSetTexture(dev: *anyopaque, stage: u32, tex: ?*anyopaque) void {
|
||||
: [self] "r" (@intFromPtr(dev)),
|
||||
[stage] "r" (stage),
|
||||
[func] "r" (func_addr),
|
||||
: .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true }
|
||||
);
|
||||
: .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,8 +300,7 @@ fn deviceSetStreamSource(dev: *anyopaque, stream: u32, vb: ?*anyopaque, offset:
|
||||
: [self] "r" (@intFromPtr(dev)),
|
||||
[a] "r" (&args),
|
||||
[func] "r" (func_addr),
|
||||
: .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true }
|
||||
);
|
||||
: .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,8 +334,7 @@ fn deviceSetIndices(dev: *anyopaque, ib: ?*anyopaque) void {
|
||||
:
|
||||
: [self] "r" (@intFromPtr(dev)),
|
||||
[func] "r" (func_addr),
|
||||
: .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true }
|
||||
);
|
||||
: .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -397,7 +393,7 @@ fn argbToFloat4(argb: u32) [4]f32 {
|
||||
// Terrain depth snapshot
|
||||
// =============================================================================
|
||||
|
||||
// (Terrain DS snapshot removed — DXVK does not support StretchRect for
|
||||
// (Terrain DS snapshot removed - DXVK does not support StretchRect for
|
||||
// depth-stencil surfaces. Terrain occlusion is achieved via stencil marks
|
||||
// written during the DIP hook, when the game's DS already has terrain depth.)
|
||||
|
||||
@@ -420,38 +416,55 @@ fn ensureResources(device: *anyopaque) void {
|
||||
resource_height = vp.Height;
|
||||
|
||||
// Silhouette RT (A8R8G8B8)
|
||||
if (deviceCreateTexture(device, vp.Width, vp.Height, 1,
|
||||
types.D3DUSAGE_RENDERTARGET, types.D3DFMT_A8R8G8B8, types.D3DPOOL_DEFAULT,
|
||||
&rt_silhouette_tex) < 0) { releaseResources(); return; }
|
||||
if (deviceCreateTexture(device, vp.Width, vp.Height, 1, types.D3DUSAGE_RENDERTARGET, types.D3DFMT_A8R8G8B8, types.D3DPOOL_DEFAULT, &rt_silhouette_tex) < 0) {
|
||||
releaseResources();
|
||||
return;
|
||||
}
|
||||
rt_silhouette_surf = textureGetSurfaceLevel(rt_silhouette_tex.?);
|
||||
if (rt_silhouette_surf == null) { releaseResources(); return; }
|
||||
if (rt_silhouette_surf == null) {
|
||||
releaseResources();
|
||||
return;
|
||||
}
|
||||
|
||||
// JFA A RT (G16R16F)
|
||||
if (deviceCreateTexture(device, vp.Width, vp.Height, 1,
|
||||
types.D3DUSAGE_RENDERTARGET, types.D3DFMT_G16R16F, types.D3DPOOL_DEFAULT,
|
||||
&rt_jfa_a_tex) < 0) { releaseResources(); return; }
|
||||
if (deviceCreateTexture(device, vp.Width, vp.Height, 1, types.D3DUSAGE_RENDERTARGET, types.D3DFMT_G16R16F, types.D3DPOOL_DEFAULT, &rt_jfa_a_tex) < 0) {
|
||||
releaseResources();
|
||||
return;
|
||||
}
|
||||
rt_jfa_a_surf = textureGetSurfaceLevel(rt_jfa_a_tex.?);
|
||||
if (rt_jfa_a_surf == null) { releaseResources(); return; }
|
||||
if (rt_jfa_a_surf == null) {
|
||||
releaseResources();
|
||||
return;
|
||||
}
|
||||
|
||||
// JFA B RT (G16R16F)
|
||||
if (deviceCreateTexture(device, vp.Width, vp.Height, 1,
|
||||
types.D3DUSAGE_RENDERTARGET, types.D3DFMT_G16R16F, types.D3DPOOL_DEFAULT,
|
||||
&rt_jfa_b_tex) < 0) { releaseResources(); return; }
|
||||
if (deviceCreateTexture(device, vp.Width, vp.Height, 1, types.D3DUSAGE_RENDERTARGET, types.D3DFMT_G16R16F, types.D3DPOOL_DEFAULT, &rt_jfa_b_tex) < 0) {
|
||||
releaseResources();
|
||||
return;
|
||||
}
|
||||
rt_jfa_b_surf = textureGetSurfaceLevel(rt_jfa_b_tex.?);
|
||||
if (rt_jfa_b_surf == null) { releaseResources(); return; }
|
||||
|
||||
if (rt_jfa_b_surf == null) {
|
||||
releaseResources();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
fn releaseResources() void {
|
||||
inline for (.{
|
||||
&rt_silhouette_surf, &rt_jfa_a_surf, &rt_jfa_b_surf,
|
||||
}) |surf_ptr| {
|
||||
if (surf_ptr.*) |s| { comRelease(s); surf_ptr.* = null; }
|
||||
if (surf_ptr.*) |s| {
|
||||
comRelease(s);
|
||||
surf_ptr.* = null;
|
||||
}
|
||||
}
|
||||
inline for (.{
|
||||
&rt_silhouette_tex, &rt_jfa_a_tex, &rt_jfa_b_tex,
|
||||
}) |tex_ptr| {
|
||||
if (tex_ptr.*) |t| { comRelease(t); tex_ptr.* = null; }
|
||||
if (tex_ptr.*) |t| {
|
||||
comRelease(t);
|
||||
tex_ptr.* = null;
|
||||
}
|
||||
}
|
||||
resource_width = 0;
|
||||
resource_height = 0;
|
||||
@@ -461,7 +474,7 @@ fn releaseResources() void {
|
||||
// Shader source strings
|
||||
// =============================================================================
|
||||
|
||||
/// Flat colour pixel shader — outputs PS constant c0.
|
||||
/// Flat colour pixel shader - outputs PS constant c0.
|
||||
const ps_flat_src = "ps_3_0\nmov oC0, c0\n";
|
||||
|
||||
/// JFA init: sample silhouette, output own UV as seed or sentinel (-1,-1).
|
||||
@@ -491,7 +504,7 @@ const jfa_prop_src =
|
||||
"def c9, 1.0, 1.0, 0.0, 0.0\n" ++
|
||||
"dcl_2d s0\n" ++
|
||||
"dcl_texcoord0 v0\n" ++
|
||||
// Self sample — initialize best seed and distance
|
||||
// Self sample - initialize best seed and distance
|
||||
"texld r0, v0, s0\n" ++
|
||||
"sub r2.xy, v0.xy, r0.xy\n" ++
|
||||
"dp2add r9.x, r2, r2, c1.x\n" ++ // best dist²
|
||||
@@ -672,7 +685,10 @@ fn assemblePS(device: *anyopaque, assemble: D3DXAssembleShaderFn, src: [*]const
|
||||
|
||||
fn releaseShaders() void {
|
||||
inline for (.{ &outline_ps, &jfa_init_ps, &jfa_prop_ps, &jfa_decode_ps, &debug_sil_ps }) |ps| {
|
||||
if (ps.*) |p| { comRelease(p); ps.* = null; }
|
||||
if (ps.*) |p| {
|
||||
comRelease(p);
|
||||
ps.* = null;
|
||||
}
|
||||
}
|
||||
shaders_attempted = false;
|
||||
}
|
||||
@@ -731,7 +747,7 @@ fn hkEndScene(device: *anyopaque) callconv(sc) i32 {
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Reset hook — force D24S8 depth/stencil format, release resources
|
||||
// Reset hook - force D24S8 depth/stencil format, release resources
|
||||
// =============================================================================
|
||||
|
||||
fn hkReset(device: *anyopaque, pp: *types.D3DPRESENT_PARAMETERS) callconv(sc) i32 {
|
||||
@@ -754,7 +770,7 @@ fn hkReset(device: *anyopaque, pp: *types.D3DPRESENT_PARAMETERS) callconv(sc) i3
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DrawIndexedPrimitive hook — cache outline draws for EndScene replay
|
||||
// DrawIndexedPrimitive hook - cache outline draws for EndScene replay
|
||||
// =============================================================================
|
||||
// When an outline target is being drawn, we cache the draw parameters and
|
||||
// current GPU state (VB, IB, vertex decl, VS, VS constants) so they can be
|
||||
@@ -808,7 +824,7 @@ fn hkDIP(
|
||||
|
||||
// Capture current GPU state (AddRef COM objects to keep them alive)
|
||||
deviceGetStreamSource(device, 0, &draw.vb, &draw.vb_offset, &draw.vb_stride);
|
||||
// GetStreamSource AddRef's the VB — we keep the ref until replay
|
||||
// GetStreamSource AddRef's the VB - we keep the ref until replay
|
||||
draw.ib = deviceGetIndices(device);
|
||||
// GetIndices AddRef's the IB
|
||||
draw.vertex_decl = deviceGetPtr(device, types.VT.GetVertexDeclaration);
|
||||
@@ -832,7 +848,7 @@ fn hkDIP(
|
||||
const s_enable = deviceGetRS(device, types.D3DRS.STENCILENABLE);
|
||||
const s_func = deviceGetRS(device, types.D3DRS.STENCILFUNC);
|
||||
const s_ref = deviceGetRS(device, types.D3DRS.STENCILREF);
|
||||
// (STENCILWRITEMASK not saved — intentionally set to 0 on restore)
|
||||
// (STENCILWRITEMASK not saved - intentionally set to 0 on restore)
|
||||
const s_pass = deviceGetRS(device, types.D3DRS.STENCILPASS);
|
||||
const s_fail = deviceGetRS(device, types.D3DRS.STENCILFAIL);
|
||||
const s_zfail = deviceGetRS(device, types.D3DRS.STENCILZFAIL);
|
||||
@@ -856,7 +872,7 @@ fn hkDIP(
|
||||
deviceSetRS(device, types.D3DRS.STENCILPASS, s_pass);
|
||||
deviceSetRS(device, types.D3DRS.STENCILFAIL, s_fail);
|
||||
deviceSetRS(device, types.D3DRS.STENCILZFAIL, s_zfail);
|
||||
// Write mask 0 instead of restoring original — prevents any
|
||||
// Write mask 0 instead of restoring original - prevents any
|
||||
// subsequent DIP from overwriting our stencil=1 marks.
|
||||
// Restored properly in EndScene before the JFA pipeline.
|
||||
deviceSetRS(device, types.D3DRS.STENCILWRITEMASK, 0);
|
||||
@@ -871,10 +887,22 @@ fn hkDIP(
|
||||
fn clearCachedDraws() void {
|
||||
for (0..cached_draw_count) |i| {
|
||||
var draw = &cached_draws[i];
|
||||
if (draw.vb) |obj| { comRelease(obj); draw.vb = null; }
|
||||
if (draw.ib) |obj| { comRelease(obj); draw.ib = null; }
|
||||
if (draw.vertex_decl) |obj| { comRelease(obj); draw.vertex_decl = null; }
|
||||
if (draw.vertex_shader) |obj| { comRelease(obj); draw.vertex_shader = null; }
|
||||
if (draw.vb) |obj| {
|
||||
comRelease(obj);
|
||||
draw.vb = null;
|
||||
}
|
||||
if (draw.ib) |obj| {
|
||||
comRelease(obj);
|
||||
draw.ib = null;
|
||||
}
|
||||
if (draw.vertex_decl) |obj| {
|
||||
comRelease(obj);
|
||||
draw.vertex_decl = null;
|
||||
}
|
||||
if (draw.vertex_shader) |obj| {
|
||||
comRelease(obj);
|
||||
draw.vertex_shader = null;
|
||||
}
|
||||
}
|
||||
cached_draw_count = 0;
|
||||
}
|
||||
@@ -898,7 +926,7 @@ fn runJfaPipeline(device: *anyopaque) void {
|
||||
// Save ALL state that replay + JFA will modify (manual, no state blocks)
|
||||
// =====================================================================
|
||||
|
||||
// COM objects (Get* AddRefs — must Release after restore)
|
||||
// COM objects (Get* AddRefs - must Release after restore)
|
||||
const saved_rt0 = deviceGetRenderTarget(device, 0);
|
||||
const saved_ps = deviceGetPtr(device, types.VT.GetPixelShader);
|
||||
const saved_vs = deviceGetPtr(device, types.VT.GetVertexShader);
|
||||
@@ -969,7 +997,7 @@ fn runJfaPipeline(device: *anyopaque) void {
|
||||
deviceSetRenderTarget(device, 0, rt_silhouette_surf.?);
|
||||
clearRenderTarget(device, 0x00000000);
|
||||
|
||||
// Keep game's DS bound — it has stencil marks from DIP hook where
|
||||
// Keep game's DS bound - it has stencil marks from DIP hook where
|
||||
// outline targets passed the terrain depth test (stencil=1 = visible).
|
||||
// Don't write depth or stencil during replay.
|
||||
deviceSetRS(device, types.D3DRS.ZWRITEENABLE, 0);
|
||||
@@ -1006,8 +1034,7 @@ fn runJfaPipeline(device: *anyopaque) void {
|
||||
deviceSetRS(device, types.D3DRS.STENCILPASS, types.D3DSTENCILOP_KEEP);
|
||||
}
|
||||
|
||||
_ = origFn(device, draw.prim_type, draw.base_vtx, draw.min_vtx,
|
||||
draw.num_verts, draw.start_idx, draw.prim_count);
|
||||
_ = origFn(device, draw.prim_type, draw.base_vtx, draw.min_vtx, draw.num_verts, draw.start_idx, draw.prim_count);
|
||||
}
|
||||
|
||||
clearCachedDraws();
|
||||
@@ -1048,89 +1075,88 @@ fn runJfaPipeline(device: *anyopaque) void {
|
||||
const quad = buildFullscreenQuad(vp.Width, vp.Height);
|
||||
deviceDrawPrimitiveUP(device, types.D3DPT_TRIANGLESTRIP, 2, @ptrCast(&quad), @sizeOf(QuadVertex));
|
||||
}
|
||||
// Skip JFA — jump straight to state restore
|
||||
// Skip JFA - jump straight to state restore
|
||||
} else {
|
||||
|
||||
// =====================================================================
|
||||
// Phase 2: JFA pipeline (silhouette → outline composite)
|
||||
// =====================================================================
|
||||
// =====================================================================
|
||||
// Phase 2: JFA pipeline (silhouette → outline composite)
|
||||
// =====================================================================
|
||||
|
||||
deviceSetPtrOrNull(device, types.VT.SetDepthStencilSurface, null);
|
||||
deviceSetPtrOrNull(device, types.VT.SetVertexShader, null);
|
||||
deviceSetFVF(device, types.D3DFVF_XYZRHW | types.D3DFVF_TEX1);
|
||||
deviceSetRS(device, types.D3DRS.ZENABLE, types.D3DZB_FALSE);
|
||||
deviceSetRS(device, types.D3DRS.ZWRITEENABLE, 0);
|
||||
deviceSetRS(device, types.D3DRS.ALPHABLENDENABLE, 0);
|
||||
deviceSetRS(device, types.D3DRS.CULLMODE, types.D3DCULL_NONE);
|
||||
deviceSetRS(device, types.D3DRS.ALPHATESTENABLE, 0);
|
||||
deviceSetRS(device, types.D3DRS.COLORWRITEENABLE, 0x0F);
|
||||
deviceSetPtrOrNull(device, types.VT.SetDepthStencilSurface, null);
|
||||
deviceSetPtrOrNull(device, types.VT.SetVertexShader, null);
|
||||
deviceSetFVF(device, types.D3DFVF_XYZRHW | types.D3DFVF_TEX1);
|
||||
deviceSetRS(device, types.D3DRS.ZENABLE, types.D3DZB_FALSE);
|
||||
deviceSetRS(device, types.D3DRS.ZWRITEENABLE, 0);
|
||||
deviceSetRS(device, types.D3DRS.ALPHABLENDENABLE, 0);
|
||||
deviceSetRS(device, types.D3DRS.CULLMODE, types.D3DCULL_NONE);
|
||||
deviceSetRS(device, types.D3DRS.ALPHATESTENABLE, 0);
|
||||
deviceSetRS(device, types.D3DRS.COLORWRITEENABLE, 0x0F);
|
||||
|
||||
deviceSetSamplerState(device, 0, types.D3DSAMP.ADDRESSU, types.D3DTADDRESS_CLAMP);
|
||||
deviceSetSamplerState(device, 0, types.D3DSAMP.ADDRESSV, types.D3DTADDRESS_CLAMP);
|
||||
deviceSetSamplerState(device, 0, types.D3DSAMP.MAGFILTER, types.D3DTEXF_POINT);
|
||||
deviceSetSamplerState(device, 0, types.D3DSAMP.MINFILTER, types.D3DTEXF_POINT);
|
||||
deviceSetSamplerState(device, 0, types.D3DSAMP.MIPFILTER, types.D3DTEXF_NONE);
|
||||
deviceSetSamplerState(device, 1, types.D3DSAMP.ADDRESSU, types.D3DTADDRESS_CLAMP);
|
||||
deviceSetSamplerState(device, 1, types.D3DSAMP.ADDRESSV, types.D3DTADDRESS_CLAMP);
|
||||
deviceSetSamplerState(device, 1, types.D3DSAMP.MAGFILTER, types.D3DTEXF_POINT);
|
||||
deviceSetSamplerState(device, 1, types.D3DSAMP.MINFILTER, types.D3DTEXF_POINT);
|
||||
deviceSetSamplerState(device, 1, types.D3DSAMP.MIPFILTER, types.D3DTEXF_NONE);
|
||||
deviceSetSamplerState(device, 0, types.D3DSAMP.ADDRESSU, types.D3DTADDRESS_CLAMP);
|
||||
deviceSetSamplerState(device, 0, types.D3DSAMP.ADDRESSV, types.D3DTADDRESS_CLAMP);
|
||||
deviceSetSamplerState(device, 0, types.D3DSAMP.MAGFILTER, types.D3DTEXF_POINT);
|
||||
deviceSetSamplerState(device, 0, types.D3DSAMP.MINFILTER, types.D3DTEXF_POINT);
|
||||
deviceSetSamplerState(device, 0, types.D3DSAMP.MIPFILTER, types.D3DTEXF_NONE);
|
||||
deviceSetSamplerState(device, 1, types.D3DSAMP.ADDRESSU, types.D3DTADDRESS_CLAMP);
|
||||
deviceSetSamplerState(device, 1, types.D3DSAMP.ADDRESSV, types.D3DTADDRESS_CLAMP);
|
||||
deviceSetSamplerState(device, 1, types.D3DSAMP.MAGFILTER, types.D3DTEXF_POINT);
|
||||
deviceSetSamplerState(device, 1, types.D3DSAMP.MINFILTER, types.D3DTEXF_POINT);
|
||||
deviceSetSamplerState(device, 1, types.D3DSAMP.MIPFILTER, types.D3DTEXF_NONE);
|
||||
|
||||
const quad = buildFullscreenQuad(vp.Width, vp.Height);
|
||||
const qstride: u32 = @sizeOf(QuadVertex);
|
||||
const fw = @as(f32, @floatFromInt(@max(vp.Width, 1)));
|
||||
const fh = @as(f32, @floatFromInt(@max(vp.Height, 1)));
|
||||
const quad = buildFullscreenQuad(vp.Width, vp.Height);
|
||||
const qstride: u32 = @sizeOf(QuadVertex);
|
||||
const fw = @as(f32, @floatFromInt(@max(vp.Width, 1)));
|
||||
const fh = @as(f32, @floatFromInt(@max(vp.Height, 1)));
|
||||
|
||||
// Pass 1: JFA Init (silhouette → JFA_A)
|
||||
deviceSetRenderTarget(device, 0, rt_jfa_a_surf.?);
|
||||
deviceSetTexture(device, 0, rt_silhouette_tex);
|
||||
deviceSetPtr(device, types.VT.SetPixelShader, jfa_init_ps.?);
|
||||
deviceDrawPrimitiveUP(device, types.D3DPT_TRIANGLESTRIP, 2, @ptrCast(&quad), qstride);
|
||||
// Pass 1: JFA Init (silhouette → JFA_A)
|
||||
deviceSetRenderTarget(device, 0, rt_jfa_a_surf.?);
|
||||
deviceSetTexture(device, 0, rt_silhouette_tex);
|
||||
deviceSetPtr(device, types.VT.SetPixelShader, jfa_init_ps.?);
|
||||
deviceDrawPrimitiveUP(device, types.D3DPT_TRIANGLESTRIP, 2, @ptrCast(&quad), qstride);
|
||||
|
||||
// JFA Propagation: steps [8, 4, 2, 1] ping-ponging between A and B.
|
||||
deviceSetPtr(device, types.VT.SetPixelShader, jfa_prop_ps.?);
|
||||
var c0: [4]f32 = undefined;
|
||||
// JFA Propagation: steps [8, 4, 2, 1] ping-ponging between A and B.
|
||||
deviceSetPtr(device, types.VT.SetPixelShader, jfa_prop_ps.?);
|
||||
var c0: [4]f32 = undefined;
|
||||
|
||||
// step=8 (JFA_A → JFA_B)
|
||||
deviceSetRenderTarget(device, 0, rt_jfa_b_surf.?);
|
||||
deviceSetTexture(device, 0, rt_jfa_a_tex);
|
||||
c0 = .{ 8.0 / fw, 8.0 / fh, 0.0, 0.0 };
|
||||
deviceSetPSConstF(device, 0, &c0);
|
||||
deviceDrawPrimitiveUP(device, types.D3DPT_TRIANGLESTRIP, 2, @ptrCast(&quad), qstride);
|
||||
// step=8 (JFA_A → JFA_B)
|
||||
deviceSetRenderTarget(device, 0, rt_jfa_b_surf.?);
|
||||
deviceSetTexture(device, 0, rt_jfa_a_tex);
|
||||
c0 = .{ 8.0 / fw, 8.0 / fh, 0.0, 0.0 };
|
||||
deviceSetPSConstF(device, 0, &c0);
|
||||
deviceDrawPrimitiveUP(device, types.D3DPT_TRIANGLESTRIP, 2, @ptrCast(&quad), qstride);
|
||||
|
||||
// step=4 (JFA_B → JFA_A)
|
||||
deviceSetRenderTarget(device, 0, rt_jfa_a_surf.?);
|
||||
deviceSetTexture(device, 0, rt_jfa_b_tex);
|
||||
c0 = .{ 4.0 / fw, 4.0 / fh, 0.0, 0.0 };
|
||||
deviceSetPSConstF(device, 0, &c0);
|
||||
deviceDrawPrimitiveUP(device, types.D3DPT_TRIANGLESTRIP, 2, @ptrCast(&quad), qstride);
|
||||
// step=4 (JFA_B → JFA_A)
|
||||
deviceSetRenderTarget(device, 0, rt_jfa_a_surf.?);
|
||||
deviceSetTexture(device, 0, rt_jfa_b_tex);
|
||||
c0 = .{ 4.0 / fw, 4.0 / fh, 0.0, 0.0 };
|
||||
deviceSetPSConstF(device, 0, &c0);
|
||||
deviceDrawPrimitiveUP(device, types.D3DPT_TRIANGLESTRIP, 2, @ptrCast(&quad), qstride);
|
||||
|
||||
// step=2 (JFA_A → JFA_B)
|
||||
deviceSetRenderTarget(device, 0, rt_jfa_b_surf.?);
|
||||
deviceSetTexture(device, 0, rt_jfa_a_tex);
|
||||
c0 = .{ 2.0 / fw, 2.0 / fh, 0.0, 0.0 };
|
||||
deviceSetPSConstF(device, 0, &c0);
|
||||
deviceDrawPrimitiveUP(device, types.D3DPT_TRIANGLESTRIP, 2, @ptrCast(&quad), qstride);
|
||||
// step=2 (JFA_A → JFA_B)
|
||||
deviceSetRenderTarget(device, 0, rt_jfa_b_surf.?);
|
||||
deviceSetTexture(device, 0, rt_jfa_a_tex);
|
||||
c0 = .{ 2.0 / fw, 2.0 / fh, 0.0, 0.0 };
|
||||
deviceSetPSConstF(device, 0, &c0);
|
||||
deviceDrawPrimitiveUP(device, types.D3DPT_TRIANGLESTRIP, 2, @ptrCast(&quad), qstride);
|
||||
|
||||
// step=1 (JFA_B → JFA_A)
|
||||
deviceSetRenderTarget(device, 0, rt_jfa_a_surf.?);
|
||||
deviceSetTexture(device, 0, rt_jfa_b_tex);
|
||||
c0 = .{ 1.0 / fw, 1.0 / fh, 0.0, 0.0 };
|
||||
deviceSetPSConstF(device, 0, &c0);
|
||||
deviceDrawPrimitiveUP(device, types.D3DPT_TRIANGLESTRIP, 2, @ptrCast(&quad), qstride);
|
||||
|
||||
// Pass 4: Decode + Composite (JFA_A + silhouette → backbuffer)
|
||||
if (saved_rt0) |rt| deviceSetRenderTarget(device, 0, rt);
|
||||
deviceSetTexture(device, 0, rt_jfa_a_tex);
|
||||
deviceSetTexture(device, 1, rt_silhouette_tex);
|
||||
c0 = [4]f32{ fw, fh, 4.0, 0.0 };
|
||||
deviceSetPSConstF(device, 0, &c0);
|
||||
deviceSetPtr(device, types.VT.SetPixelShader, jfa_decode_ps.?);
|
||||
deviceSetRS(device, types.D3DRS.ALPHABLENDENABLE, 1);
|
||||
deviceSetRS(device, types.D3DRS.SRCBLEND, types.D3DBLEND_SRCALPHA);
|
||||
deviceSetRS(device, types.D3DRS.DESTBLEND, types.D3DBLEND_INVSRCALPHA);
|
||||
deviceDrawPrimitiveUP(device, types.D3DPT_TRIANGLESTRIP, 2, @ptrCast(&quad), qstride);
|
||||
// step=1 (JFA_B → JFA_A)
|
||||
deviceSetRenderTarget(device, 0, rt_jfa_a_surf.?);
|
||||
deviceSetTexture(device, 0, rt_jfa_b_tex);
|
||||
c0 = .{ 1.0 / fw, 1.0 / fh, 0.0, 0.0 };
|
||||
deviceSetPSConstF(device, 0, &c0);
|
||||
deviceDrawPrimitiveUP(device, types.D3DPT_TRIANGLESTRIP, 2, @ptrCast(&quad), qstride);
|
||||
|
||||
// Pass 4: Decode + Composite (JFA_A + silhouette → backbuffer)
|
||||
if (saved_rt0) |rt| deviceSetRenderTarget(device, 0, rt);
|
||||
deviceSetTexture(device, 0, rt_jfa_a_tex);
|
||||
deviceSetTexture(device, 1, rt_silhouette_tex);
|
||||
c0 = [4]f32{ fw, fh, 4.0, 0.0 };
|
||||
deviceSetPSConstF(device, 0, &c0);
|
||||
deviceSetPtr(device, types.VT.SetPixelShader, jfa_decode_ps.?);
|
||||
deviceSetRS(device, types.D3DRS.ALPHABLENDENABLE, 1);
|
||||
deviceSetRS(device, types.D3DRS.SRCBLEND, types.D3DBLEND_SRCALPHA);
|
||||
deviceSetRS(device, types.D3DRS.DESTBLEND, types.D3DBLEND_INVSRCALPHA);
|
||||
deviceDrawPrimitiveUP(device, types.D3DPT_TRIANGLESTRIP, 2, @ptrCast(&quad), qstride);
|
||||
} // end else (normal JFA path)
|
||||
|
||||
// =====================================================================
|
||||
@@ -1174,7 +1200,10 @@ fn runJfaPipeline(device: *anyopaque) void {
|
||||
deviceSetVSConstF(device, 0, &saved_vs_consts, MAX_VS_CONST_REGS);
|
||||
|
||||
// COM objects (restore binding then release our ref)
|
||||
if (saved_rt0) |rt| { deviceSetRenderTarget(device, 0, rt); comRelease(rt); } // RT0
|
||||
if (saved_rt0) |rt| {
|
||||
deviceSetRenderTarget(device, 0, rt);
|
||||
comRelease(rt);
|
||||
} // RT0
|
||||
deviceSetPtrOrNull(device, types.VT.SetDepthStencilSurface, saved_ds); // DS
|
||||
if (saved_ds) |ds| comRelease(ds);
|
||||
deviceSetPtrOrNull(device, types.VT.SetPixelShader, saved_ps); // PS
|
||||
|
||||
+10
-10
@@ -1,12 +1,12 @@
|
||||
//! WoW model rendering pipeline hooks.
|
||||
//!
|
||||
//! Hooks three WoW functions to integrate outline rendering:
|
||||
//! - CM2SceneRenderDraw — reorders batches so outline targets render last.
|
||||
//! - CM2Model_ManageRenderListNode — classifies models on render-list add.
|
||||
//! - CM2Scene_DrawBatchProjected — flags the DIP hook for outline rendering.
|
||||
//! - CM2SceneRenderDraw - reorders batches so outline targets render last.
|
||||
//! - CM2Model_ManageRenderListNode - classifies models on render-list add.
|
||||
//! - CM2Scene_DrawBatchProjected - flags the DIP hook for outline rendering.
|
||||
//!
|
||||
//! Calling conventions:
|
||||
//! - RenderDraw & ManageRender use callconv(.x86_thiscall) — direct native detours.
|
||||
//! - RenderDraw & ManageRender use callconv(.x86_thiscall) - direct native detours.
|
||||
//! - DrawBatchProj uses a callconv(.naked) entry point because Zig 0.15 has a
|
||||
//! codegen bug with callconv(.x86_fastcall) that generates wrong ret instructions
|
||||
//! for functions with ≤2 register params. The naked wrapper bridges to a cdecl
|
||||
@@ -64,12 +64,12 @@ var reordered_indices: [MAX_REORDER]i32 = undefined;
|
||||
// CM2SceneRenderDraw hook
|
||||
// =============================================================================
|
||||
// __thiscall(this, viewMatrix, batchData, batchIndices, batchCount)
|
||||
// Native thiscall detour — no thunk needed.
|
||||
// Native thiscall detour - no thunk needed.
|
||||
//
|
||||
// Reorders batch indices into 3 groups:
|
||||
// 1. Game objects/doodads — render first, write depth so outlines respect them
|
||||
// 2. Outline targets — render second, DIP hook writes stencil against depth
|
||||
// 3. Other players, gear, NPCs — render last, draw over targets normally
|
||||
// 1. Game objects/doodads - render first, write depth so outlines respect them
|
||||
// 2. Outline targets - render second, DIP hook writes stencil against depth
|
||||
// 3. Other players, gear, NPCs - render last, draw over targets normally
|
||||
//
|
||||
// This gives outlines that are occluded by world/WMO/game objects but show
|
||||
// through other players and gear (since those aren't in depth when stencil
|
||||
@@ -145,7 +145,7 @@ fn renderDrawDetour(this: u32, view_matrix: u32, batch_data: u32, batch_indices:
|
||||
// CM2Model_ManageRenderListNode hook
|
||||
// =============================================================================
|
||||
// __thiscall(model_ECX, addToList_stack)
|
||||
// Native thiscall detour — no thunk needed.
|
||||
// Native thiscall detour - no thunk needed.
|
||||
|
||||
fn manageRenderDetour(model: u32, add_to_list: u32) callconv(tc) void {
|
||||
// Classify the model when it's being ADDED to the render list
|
||||
@@ -180,7 +180,7 @@ fn drawBatchProjDetour(ctx: u32) callconv(tc) void {
|
||||
const entry = if (model_ptr != 0) tracker.findOutlineEntry(model_ptr) else null;
|
||||
|
||||
if (entry != null) {
|
||||
// This batch is an outline target — signal the DIP hook
|
||||
// This batch is an outline target - signal the DIP hook
|
||||
rendering_outline = true;
|
||||
current_model = model_ptr;
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ pub const IS_IN_WORLD: usize = 0xB4B424;
|
||||
// =============================================================================
|
||||
|
||||
/// __stdcall(guidLo_stack, guidHi_stack) → object pointer (EAX). Returns 0 on miss.
|
||||
/// Callee cleans 8 bytes (RET 8). NOT __fastcall — params on stack, not registers.
|
||||
/// Callee cleans 8 bytes (RET 8). NOT __fastcall - params on stack, not registers.
|
||||
pub const FN_GET_OBJECT_BY_GUID: usize = 0x464870;
|
||||
|
||||
/// __fastcall(unitIdStr_ECX) → GUID in EAX:EDX. Accepts "player", "target", etc.
|
||||
@@ -106,14 +106,14 @@ pub const FN_UNIT_REACTION: usize = 0x6061E0;
|
||||
// Hooked function addresses (model rendering pipeline)
|
||||
// =============================================================================
|
||||
|
||||
/// CM2SceneRenderDraw — main batch rendering entry point.
|
||||
/// CM2SceneRenderDraw - main batch rendering entry point.
|
||||
/// __thiscall(this, viewMatrix, batchData, batchIndices, batchCount)
|
||||
pub const FN_CM2SCENE_RENDER_DRAW: usize = 0x0070b360;
|
||||
|
||||
/// CM2Model_ManageRenderListNode — called for every model added/removed from render list.
|
||||
/// CM2Model_ManageRenderListNode - called for every model added/removed from render list.
|
||||
/// __thiscall(model_ECX, addToList_stack)
|
||||
pub const FN_CM2MODEL_MANAGE_RENDER_LIST: usize = 0x00710b90;
|
||||
|
||||
/// CM2Scene_DrawModelBatchProjected — called per batch (type 0) during rendering.
|
||||
/// CM2Scene_DrawModelBatchProjected - called per batch (type 0) during rendering.
|
||||
/// __fastcall(renderContext_ECX)
|
||||
pub const FN_DRAW_BATCH_PROJ: usize = 0x0070cb30;
|
||||
|
||||
+10
-7
@@ -2,12 +2,12 @@
|
||||
//!
|
||||
//! Maintains fixed-size arrays of tracked object pointers (target, raid marks,
|
||||
//! dead players) and the per-frame set of outline model entries.
|
||||
//! All state is single-threaded (main WoW thread) — no synchronisation needed.
|
||||
//! All state is single-threaded (main WoW thread) - no synchronisation needed.
|
||||
//!
|
||||
//! Model classification uses forward mapping: scanObjects stores object pointers
|
||||
//! from the object manager, then classifyModel (ManageRenderListNode) reads the
|
||||
//! model's back-pointers (model+0x28, model+0x3C0) and compares them against
|
||||
//! known object pointers. No dereferencing of unknown memory — just value
|
||||
//! known object pointers. No dereferencing of unknown memory - just value
|
||||
//! comparison against the object manager's validated set.
|
||||
|
||||
const std = @import("std");
|
||||
@@ -27,7 +27,7 @@ const MAX_OUTLINE_MODELS = 256;
|
||||
// Tracked object set (populated by scanObjects from the object manager)
|
||||
// =============================================================================
|
||||
// Stores obj_ptr + category for entities we want to outline.
|
||||
// classifyModel matches model back-pointers against these — no pointer chasing.
|
||||
// classifyModel matches model back-pointers against these - no pointer chasing.
|
||||
|
||||
const TrackedObj = struct {
|
||||
obj_ptr: u32,
|
||||
@@ -46,7 +46,7 @@ var frame_outlines: [MAX_OUTLINE_MODELS]types.OutlineEntry = undefined;
|
||||
var frame_outline_count: usize = 0;
|
||||
|
||||
// =============================================================================
|
||||
// Per-frame game object tracking (for render ordering — game objects first)
|
||||
// Per-frame game object tracking (for render ordering - game objects first)
|
||||
// =============================================================================
|
||||
// Game object M2 models need to render before outline targets so their depth
|
||||
// is in the buffer when stencil marks are written. Tracked separately from
|
||||
@@ -127,7 +127,7 @@ pub fn getOutlinePixels(cat: types.ModelCategory) f32 {
|
||||
|
||||
/// Classify a model by comparing its back-pointers against known object pointers.
|
||||
/// Safe: only reads from the model struct (which WoW just handed us via the
|
||||
/// ManageRenderListNode __thiscall), then compares values — never dereferences
|
||||
/// ManageRenderListNode __thiscall), then compares values - never dereferences
|
||||
/// the back-pointer values as pointers.
|
||||
pub fn classifyModel(model_ptr: u32) void {
|
||||
if (model_ptr == 0 or !enabled) return;
|
||||
@@ -155,7 +155,10 @@ pub fn classifyModel(model_ptr: u32) void {
|
||||
// Deduplicate
|
||||
var found = false;
|
||||
for (game_obj_models[0..game_obj_model_count]) |m| {
|
||||
if (m == model_ptr) { found = true; break; }
|
||||
if (m == model_ptr) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
game_obj_models[game_obj_model_count] = model_ptr;
|
||||
@@ -214,7 +217,7 @@ pub fn scanObjects() void {
|
||||
// Cache raid target GUIDs
|
||||
wow.cacheRaidTargets();
|
||||
|
||||
// Resolve target to object pointer (highest priority — added first)
|
||||
// Resolve target to object pointer (highest priority - added first)
|
||||
const target_guid = wow.getTargetGUID();
|
||||
if (target_guid != 0) {
|
||||
const target_obj = wow.getObjectByGUID(target_guid);
|
||||
|
||||
@@ -41,7 +41,7 @@ pub const OutlineEntry = struct {
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Outline colours — D3DCOLOR ARGB format (0xAARRGGBB)
|
||||
// Outline colours - D3DCOLOR ARGB format (0xAARRGGBB)
|
||||
// =============================================================================
|
||||
|
||||
/// Dead player / corpse outline (cyan).
|
||||
@@ -53,14 +53,14 @@ pub const COLOR_TARGET: u32 = 0xFFFFC800;
|
||||
/// Raid mark colours, indexed 0-8. Index 0 = fallback cyan.
|
||||
pub const RAID_MARK_COLORS = [9]u32{
|
||||
0xFF00FFFF, // 0: fallback
|
||||
0xFFFFFF00, // 1: Star — Yellow
|
||||
0xFFFF8000, // 2: Circle — Orange
|
||||
0xFFCC44FF, // 3: Diamond — Purple
|
||||
0xFF00FF00, // 4: Triangle — Green
|
||||
0xFFC0C0FF, // 5: Moon — Silver/Pale Blue
|
||||
0xFF4040FF, // 6: Square — Blue
|
||||
0xFFFF2828, // 7: Cross — Soft Red
|
||||
0xFFFFF5DC, // 8: Skull — Bone White
|
||||
0xFFFFFF00, // 1: Star - Yellow
|
||||
0xFFFF8000, // 2: Circle - Orange
|
||||
0xFFCC44FF, // 3: Diamond - Purple
|
||||
0xFF00FF00, // 4: Triangle - Green
|
||||
0xFFC0C0FF, // 5: Moon - Silver/Pale Blue
|
||||
0xFF4040FF, // 6: Square - Blue
|
||||
0xFFFF2828, // 7: Cross - Soft Red
|
||||
0xFFFFF5DC, // 8: Skull - Bone White
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
|
||||
+6
-9
@@ -20,7 +20,7 @@ extern "kernel32" fn IsBadReadPtr(
|
||||
ucb: usize,
|
||||
) callconv(WINAPI) i32;
|
||||
|
||||
/// Quick sanity check — reject null, low-address, and kernel-space pointers.
|
||||
/// Quick sanity check - reject null, low-address, and kernel-space pointers.
|
||||
pub fn isValidPtr(addr: u32) bool {
|
||||
return addr >= 0x10000 and addr < 0x7F000000;
|
||||
}
|
||||
@@ -118,7 +118,7 @@ pub fn resolveModelOwner(model: u32) u32 {
|
||||
// Need to read up to model+0x3C0+4
|
||||
if (!isReadablePtr(model, o.MODEL_OWNER_CALLBACK + 4)) return 0;
|
||||
|
||||
// Try callback owner first — more reliable for units
|
||||
// Try callback owner first - more reliable for units
|
||||
const candidate_cb = hook.readMem(u32, model + o.MODEL_OWNER_CALLBACK);
|
||||
if (candidate_cb != 0 and isReadablePtr(candidate_cb, 0x40)) {
|
||||
const guid_lo = hook.readMem(u32, candidate_cb + o.OBJECT_GUID_OFFSET);
|
||||
@@ -149,14 +149,13 @@ pub fn unitGUID(unit_id: [*:0]const u8) u64 {
|
||||
[_] "={edx}" (hi),
|
||||
: [_] "{ecx}" (@intFromPtr(unit_id)),
|
||||
[func] "r" (@as(u32, o.FN_UNIT_GUID)),
|
||||
: .{ .memory = true, .cc = true }
|
||||
);
|
||||
: .{ .memory = true, .cc = true });
|
||||
return (@as(u64, hi) << 32) | lo;
|
||||
}
|
||||
|
||||
/// Resolve a GUID → object pointer via the object manager hash table.
|
||||
/// Ghidra-verified: __stdcall(guidLow, guidHigh) with RET 8.
|
||||
/// NOT __fastcall — params are read from stack, not registers.
|
||||
/// NOT __fastcall - params are read from stack, not registers.
|
||||
pub fn getObjectByGUID(guid: u64) u32 {
|
||||
if (guid == 0) return 0;
|
||||
const lo: u32 = @truncate(guid);
|
||||
@@ -169,8 +168,7 @@ pub fn getObjectByGUID(guid: u64) u32 {
|
||||
: [lo] "r" (lo),
|
||||
[hi] "r" (hi),
|
||||
[func] "r" (@as(u32, o.FN_GET_OBJECT_BY_GUID)),
|
||||
: .{ .ecx = true, .edx = true, .memory = true, .cc = true }
|
||||
);
|
||||
: .{ .ecx = true, .edx = true, .memory = true, .cc = true });
|
||||
}
|
||||
|
||||
/// Get the local player's object pointer.
|
||||
@@ -197,8 +195,7 @@ pub fn isUnitFriendly(unit: u32, local_player: u32) bool {
|
||||
: [_] "{ecx}" (local_player),
|
||||
[unit] "r" (unit),
|
||||
[func] "r" (@as(u32, o.FN_UNIT_REACTION)),
|
||||
: .{ .edx = true, .memory = true, .cc = true }
|
||||
);
|
||||
: .{ .edx = true, .memory = true, .cc = true });
|
||||
return reaction >= 4;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user