Files
WeirdUtils/src/outline/d3d9_hook.zig
T

1504 lines
62 KiB
Zig

//! D3D9 vtable hooks for outline rendering.
//!
//! Patches IDirect3DDevice9 vtable entries for EndScene, DrawIndexedPrimitive,
//! and Reset via the game's existing device (shared vtable across all devices).
//!
//! - **EndScene**: per-frame object scan, JFA pipeline for outline compositing.
//! - **DIP**: caches outline-target draws for EndScene replay; writes stencil
//! marks where models pass the terrain depth test (stencil=1 = visible).
//! - **Reset**: forces D24S8 depth/stencil format, releases resources.
//!
//! Batch reordering in model_hook.zig partitions M2 batches into 3 groups:
//! game objects first (write depth), then outline targets (DIP hook writes
//! stencil against that depth), then other players/gear/NPCs. Outlines are
//! occluded by world/WMO/game objects but show through other players.
const std = @import("std");
const hook = @import("zhook");
const types = @import("types.zig");
const tracker = @import("tracker.zig");
const model_hook = @import("model_hook.zig");
const WINAPI = std.builtin.CallingConvention.winapi;
// =============================================================================
// Windows / D3D9 externs
// =============================================================================
extern "kernel32" fn LoadLibraryA(name: [*:0]const u8) callconv(WINAPI) ?*anyopaque;
extern "kernel32" fn GetProcAddress(module: *anyopaque, name: [*:0]const u8) callconv(WINAPI) ?*anyopaque;
// =============================================================================
// COM helper: read vtable pointer, call method by index
// =============================================================================
inline fn vt(obj: *anyopaque) [*]usize {
return @ptrFromInt(hook.readMem(u32, @intFromPtr(obj)));
}
/// Call a COM method that takes only (self) and returns HRESULT.
fn comCall0(obj: *anyopaque, idx: usize) i32 {
const f: *const fn (*anyopaque) callconv(hook.cc.stdcall) i32 = @ptrFromInt(vt(obj)[idx]);
return f(obj);
}
/// Release a COM object.
fn comRelease(obj: *anyopaque) void {
_ = comCall0(obj, types.VT.Release);
}
// =============================================================================
// Saved original vtable function pointers
// =============================================================================
var orig_endscene: usize = 0;
var orig_dip: usize = 0;
var orig_reset: usize = 0;
var d3d9_vtable: ?[*]usize = null;
var hooks_installed: bool = false;
// Sticky diagnostics for the in-game OutlineDebug() command.
pub var debug_endscene_seen: bool = false;
pub var debug_dip_seen: bool = false;
pub var debug_outline_dip_seen: bool = false;
pub var debug_cached_draw_seen: bool = false;
pub var debug_translucent_skipped_seen: bool = false;
pub var debug_shaders_ready_seen: bool = false;
pub var debug_resources_ready_seen: bool = false;
pub var debug_pipeline_entered_seen: bool = false;
pub var debug_pipeline_ready_seen: bool = false;
pub var debug_shader_stage: u32 = 0;
pub var debug_resource_stage: u32 = 0;
pub var debug_shader_assemble_hr: i32 = 0;
pub var debug_shader_create_hr: i32 = 0;
pub var debug_texture_create_hr: i32 = 0;
pub var debug_shader_error_text: [160]u8 = [_]u8{0} ** 160;
pub fn hooksInstalled() bool {
return hooks_installed;
}
pub const LiveHookState = struct {
vtable_found: bool = false,
same_vtable: bool = false,
endscene_ours: bool = false,
dip_ours: bool = false,
reset_ours: bool = false,
endscene_ptr: usize = 0,
dip_ptr: usize = 0,
reset_ptr: usize = 0,
};
/// Read the game's current D3D9 vtable and verify whether our entries are
/// still installed. This is intentionally queried on demand from OutlineDebug.
pub fn getLiveHookState() LiveHookState {
const cur = getD3D9VTable() orelse return .{};
var out: LiveHookState = .{ .vtable_found = true };
out.same_vtable = if (d3d9_vtable) |saved| @intFromPtr(saved) == @intFromPtr(cur) else false;
out.endscene_ptr = cur[types.VT.EndScene];
out.dip_ptr = cur[types.VT.DrawIndexedPrimitive];
out.reset_ptr = cur[types.VT.Reset];
out.endscene_ours = out.endscene_ptr == @intFromPtr(&hkEndScene);
out.dip_ours = out.dip_ptr == @intFromPtr(&hkDIP);
out.reset_ours = out.reset_ptr == @intFromPtr(&hkReset);
return out;
}
pub var debug_late_rehook_attempted: bool = false;
pub var debug_late_rehook_succeeded: bool = false;
/// DEBUG15: re-apply the D3D9 hooks on demand after the client is fully loaded.
/// If the current entries are no longer ours, chain whatever is there now as
/// the new originals, then patch the three entries again.
pub fn lateRehookIfLost() bool {
debug_late_rehook_attempted = true;
const cur = getD3D9VTable() orelse return false;
d3d9_vtable = cur;
const ours_end = @intFromPtr(&hkEndScene);
const ours_dip = @intFromPtr(&hkDIP);
const ours_reset = @intFromPtr(&hkReset);
if (cur[types.VT.EndScene] != ours_end) {
if (!patchVtableEntry(cur, types.VT.EndScene, ours_end, &orig_endscene)) return false;
}
if (cur[types.VT.DrawIndexedPrimitive] != ours_dip) {
if (!patchVtableEntry(cur, types.VT.DrawIndexedPrimitive, ours_dip, &orig_dip)) return false;
}
if (cur[types.VT.Reset] != ours_reset) {
if (!patchVtableEntry(cur, types.VT.Reset, ours_reset, &orig_reset)) return false;
}
hooks_installed = true;
const live = getLiveHookState();
debug_late_rehook_succeeded = live.endscene_ours and live.dip_ours and live.reset_ours;
return debug_late_rehook_succeeded;
}
/// True until the first EndScene verifies (and if needed, forces) D24S8 format.
var need_force_reset: bool = false;
pub var debug_stencil_ready: bool = false;
pub var debug_stencil_format: u32 = 0;
pub var debug_stencil_reset_hr: i32 = 0;
pub fn requestStencilCheck() void {
// DEBUG23: intentionally disabled. Forcing IDirect3DDevice9::Reset on this
// client can hang the render thread while audio/game logic keeps running.
need_force_reset = false;
}
// =============================================================================
// Shader resources
// =============================================================================
var outline_ps: ?*anyopaque = null; // flat-color PS (solid silhouettes)
var outline_alpha_ps: ?*anyopaque = null; // texture-alpha-aware silhouette PS
var outline_rgb_ps: ?*anyopaque = null; // additive/modulated texture coverage PS
var jfa_init_ps: ?*anyopaque = null; // JFA seed init PS
var jfa_prop_ps: ?*anyopaque = null; // JFA propagation PS
var jfa_decode_ps: ?*anyopaque = null; // JFA decode + composite PS
var debug_sil_ps: ?*anyopaque = null; // debug: composite silhouette directly
var shaders_attempted: bool = false;
// Debug: set to true to skip JFA and composite raw silhouette RT to backbuffer.
// Used to diagnose whether banding artifacts originate in the silhouette (Phase 1
// replay / stale VB) or in the JFA pipeline (Phase 2 shader bug).
const DEBUG_SHOW_SILHOUETTE = false;
// D3DXAssembleShader function pointer (loaded dynamically)
const D3DXAssembleShaderFn = *const fn (
[*]const u8, // pSrcData
u32, // SrcDataLen
?*anyopaque, // pDefines
?*anyopaque, // pInclude
u32, // Flags
*?*anyopaque, // ppShader (ID3DXBuffer**)
*?*anyopaque, // ppErrorMsgs (ID3DXBuffer**)
) callconv(hook.cc.stdcall) i32;
// =============================================================================
// Render target resources
// =============================================================================
var rt_silhouette_tex: ?*anyopaque = null; // A8R8G8B8 silhouette texture
var rt_silhouette_surf: ?*anyopaque = null;
var rt_jfa_a_tex: ?*anyopaque = null; // G16R16F JFA ping texture
var rt_jfa_a_surf: ?*anyopaque = null;
var rt_jfa_b_tex: ?*anyopaque = null; // G16R16F JFA pong texture
var rt_jfa_b_surf: ?*anyopaque = null;
var resource_width: u32 = 0;
var resource_height: u32 = 0;
// =============================================================================
// Per-frame flags
// =============================================================================
var frame_has_outlines: bool = false;
var silhouette_cleared: bool = false;
// =============================================================================
// Cached draw calls for EndScene replay (avoids double-DIP in hook)
// =============================================================================
// Instead of drawing silhouettes inside the DIP hook (which corrupts WoW's
// internal rendering state), we cache draw parameters and replay them in
// EndScene before the JFA pipeline. This matches the C reference approach.
const MAX_CACHED_DRAWS = 32;
const MAX_VS_CONST_REGS = 256;
const CachedDraw = struct {
// Draw call parameters
prim_type: u32 = 0,
base_vtx: i32 = 0,
min_vtx: u32 = 0,
num_verts: u32 = 0,
start_idx: u32 = 0,
prim_count: u32 = 0,
// GPU state (AddRef'd COM objects - released after replay)
vb: ?*anyopaque = null,
vb_offset: u32 = 0,
vb_stride: u32 = 0,
ib: ?*anyopaque = null,
vertex_decl: ?*anyopaque = null,
vertex_shader: ?*anyopaque = null,
tex0: ?*anyopaque = null,
alpha_test_enable: u32 = 0,
alpha_ref: u32 = 0,
alpha_func: u32 = types.D3DCMP_ALWAYS,
alpha_blend_enable: u32 = 0,
src_blend: u32 = types.D3DBLEND_ONE,
dst_blend: u32 = types.D3DBLEND_ZERO,
// Per-model outline info
color: u32 = 0,
category: types.ModelCategory = .none,
// VS constants (bone matrices, world/view/proj) - copied by value
vs_consts: [MAX_VS_CONST_REGS][4]f32 = undefined,
};
var cached_draws: [MAX_CACHED_DRAWS]CachedDraw = [_]CachedDraw{.{}} ** MAX_CACHED_DRAWS;
var cached_draw_count: u32 = 0;
// =============================================================================
// Fullscreen quad vertex (pretransformed + 1 texcoord)
// =============================================================================
const QuadVertex = extern struct {
x: f32,
y: f32,
z: f32,
rhw: f32,
u: f32,
v: f32,
};
// =============================================================================
// Device helper wrappers (stdcall COM vtable calls)
// =============================================================================
fn deviceSetRS(dev: *anyopaque, state: u32, value: u32) void {
const f: *const fn (*anyopaque, u32, u32) callconv(hook.cc.stdcall) i32 = @ptrFromInt(vt(dev)[types.VT.SetRenderState]);
_ = f(dev, state, value);
}
fn deviceGetRS(dev: *anyopaque, state: u32) u32 {
var val: u32 = 0;
const f: *const fn (*anyopaque, u32, *u32) callconv(hook.cc.stdcall) i32 = @ptrFromInt(vt(dev)[types.VT.GetRenderState]);
_ = f(dev, state, &val);
return val;
}
fn deviceSetPtr(dev: *anyopaque, idx: usize, ptr: *anyopaque) void {
const f: *const fn (*anyopaque, *anyopaque) callconv(hook.cc.stdcall) i32 = @ptrFromInt(vt(dev)[idx]);
_ = f(dev, ptr);
}
fn deviceGetPtr(dev: *anyopaque, idx: usize) ?*anyopaque {
var ptr: ?*anyopaque = null;
const f: *const fn (*anyopaque, *?*anyopaque) callconv(hook.cc.stdcall) i32 = @ptrFromInt(vt(dev)[idx]);
_ = f(dev, &ptr);
return ptr;
}
/// Set a COM pointer, handling null via optional pointer (ABI-equivalent to passing 0).
fn deviceSetPtrOrNull(dev: *anyopaque, idx: usize, ptr: ?*anyopaque) void {
const f: *const fn (*anyopaque, ?*anyopaque) callconv(hook.cc.stdcall) i32 = @ptrFromInt(vt(dev)[idx]);
_ = @call(.never_tail, f, .{ dev, ptr });
}
fn deviceSetPSConstF(dev: *anyopaque, start: u32, data: *const [4]f32) void {
const f: *const fn (*anyopaque, u32, *const [4]f32, u32) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(dev)[types.VT.SetPixelShaderConstantF]);
_ = f(dev, start, data, 1);
}
fn deviceGetPSConstF(dev: *anyopaque, start: u32, data: *[4]f32) void {
const f: *const fn (*anyopaque, u32, *[4]f32, u32) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(dev)[types.VT.GetPixelShaderConstantF]);
_ = f(dev, start, data, 1);
}
fn deviceGetViewport(dev: *anyopaque, vp_out: *types.D3DVIEWPORT9) void {
const f: *const fn (*anyopaque, *types.D3DVIEWPORT9) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(dev)[types.VT.GetViewport]);
_ = f(dev, vp_out);
}
fn deviceSetRenderTarget(dev: *anyopaque, idx: u32, surf: *anyopaque) void {
const f: *const fn (*anyopaque, u32, *anyopaque) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(dev)[types.VT.SetRenderTarget]);
_ = f(dev, idx, surf);
}
fn deviceGetRenderTarget(dev: *anyopaque, idx: u32) ?*anyopaque {
var surf: ?*anyopaque = null;
const f: *const fn (*anyopaque, u32, *?*anyopaque) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(dev)[types.VT.GetRenderTarget]);
_ = f(dev, idx, &surf);
return surf;
}
fn deviceSetTexture(dev: *anyopaque, stage: u32, tex: ?*anyopaque) void {
const f: *const fn (*anyopaque, u32, ?*anyopaque) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(dev)[types.VT.SetTexture]);
_ = @call(.never_tail, f, .{ dev, stage, tex });
}
fn deviceSetFVF(dev: *anyopaque, fvf: u32) void {
const f: *const fn (*anyopaque, u32) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(dev)[types.VT.SetFVF]);
_ = f(dev, fvf);
}
fn deviceGetSamplerState(dev: *anyopaque, sampler: u32, state_type: u32) u32 {
var val: u32 = 0;
const f: *const fn (*anyopaque, u32, u32, *u32) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(dev)[types.VT.GetSamplerState]);
_ = f(dev, sampler, state_type, &val);
return val;
}
fn deviceSetSamplerState(dev: *anyopaque, sampler: u32, state_type: u32, value: u32) void {
const f: *const fn (*anyopaque, u32, u32, u32) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(dev)[types.VT.SetSamplerState]);
_ = f(dev, sampler, state_type, value);
}
fn deviceGetStreamSource(dev: *anyopaque, stream: u32, vb_out: *?*anyopaque, offset_out: *u32, stride_out: *u32) void {
const f: *const fn (*anyopaque, u32, *?*anyopaque, *u32, *u32) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(dev)[types.VT.GetStreamSource]);
_ = f(dev, stream, vb_out, offset_out, stride_out);
}
fn deviceSetStreamSource(dev: *anyopaque, stream: u32, vb: ?*anyopaque, offset: u32, stride: u32) void {
const f: *const fn (*anyopaque, u32, ?*anyopaque, u32, u32) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(dev)[types.VT.SetStreamSource]);
_ = @call(.never_tail, f, .{ dev, stream, vb, offset, stride });
}
fn deviceGetTexture(dev: *anyopaque, stage: u32) ?*anyopaque {
var tex: ?*anyopaque = null;
const f: *const fn (*anyopaque, u32, *?*anyopaque) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(dev)[types.VT.GetTexture]);
_ = f(dev, stage, &tex);
return tex;
}
fn deviceGetIndices(dev: *anyopaque) ?*anyopaque {
var ib: ?*anyopaque = null;
const f: *const fn (*anyopaque, *?*anyopaque) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(dev)[types.VT.GetIndices]);
_ = f(dev, &ib);
return ib;
}
fn deviceSetIndices(dev: *anyopaque, ib: ?*anyopaque) void {
const f: *const fn (*anyopaque, ?*anyopaque) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(dev)[types.VT.SetIndices]);
_ = @call(.never_tail, f, .{ dev, ib });
}
fn deviceGetVSConstF(dev: *anyopaque, start: u32, data: [*][4]f32, count: u32) void {
const f: *const fn (*anyopaque, u32, [*][4]f32, u32) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(dev)[types.VT.GetVertexShaderConstantF]);
_ = f(dev, start, data, count);
}
fn deviceSetVSConstF(dev: *anyopaque, start: u32, data: [*]const [4]f32, count: u32) void {
const f: *const fn (*anyopaque, u32, [*]const [4]f32, u32) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(dev)[types.VT.SetVertexShaderConstantF]);
_ = f(dev, start, data, count);
}
fn deviceDrawPrimitiveUP(dev: *anyopaque, prim_type: u32, prim_count: u32, data: *const anyopaque, stride: u32) void {
const f: *const fn (*anyopaque, u32, u32, *const anyopaque, u32) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(dev)[types.VT.DrawPrimitiveUP]);
_ = f(dev, prim_type, prim_count, data, stride);
}
fn deviceCreateTexture(dev: *anyopaque, w: u32, h: u32, levels: u32, usage: u32, fmt: u32, pool: u32, out: *?*anyopaque) i32 {
const f: *const fn (*anyopaque, u32, u32, u32, u32, u32, u32, *?*anyopaque, u32) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(dev)[types.VT.CreateTexture]);
const hr = f(dev, w, h, levels, usage, fmt, pool, out, 0);
if (hr < 0) debug_texture_create_hr = hr;
return hr;
}
/// Get surface level 0 from a texture. Returns AddRef'd surface or null.
fn textureGetSurfaceLevel(tex: *anyopaque) ?*anyopaque {
// IDirect3DTexture9::GetSurfaceLevel is vtable index 18
var surf: ?*anyopaque = null;
const f: *const fn (*anyopaque, u32, *?*anyopaque) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(tex)[18]);
if (f(tex, 0, &surf) < 0) return null;
return surf;
}
/// Clear the current render target to a colour.
fn clearRenderTarget(dev: *anyopaque, color: u32) void {
const f: *const fn (*anyopaque, u32, ?*anyopaque, u32, u32, f32, u32) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(dev)[types.VT.Clear]);
_ = f(dev, 0, null, types.D3DCLEAR_TARGET, color, 1.0, 0);
}
/// Convert D3DCOLOR ARGB to float4 RGBA.
fn argbToFloat4(argb: u32) [4]f32 {
return .{
@as(f32, @floatFromInt((argb >> 16) & 0xFF)) / 255.0,
@as(f32, @floatFromInt((argb >> 8) & 0xFF)) / 255.0,
@as(f32, @floatFromInt(argb & 0xFF)) / 255.0,
@as(f32, @floatFromInt((argb >> 24) & 0xFF)) / 255.0,
};
}
// =============================================================================
// Terrain depth snapshot
// =============================================================================
// (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.)
// =============================================================================
// Resource management
// =============================================================================
fn ensureResources(device: *anyopaque) void {
var vp: types.D3DVIEWPORT9 = .{};
deviceGetViewport(device, &vp);
if (vp.Width == 0 or vp.Height == 0) return;
debug_resource_stage = 1; // viewport valid
// Check if resources already match current dimensions
if (vp.Width == resource_width and vp.Height == resource_height and
rt_silhouette_tex != null) return;
// Release old and create new
releaseResources();
resource_width = vp.Width;
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;
}
debug_resource_stage = 2; // silhouette texture ready
rt_silhouette_surf = textureGetSurfaceLevel(rt_silhouette_tex.?);
if (rt_silhouette_surf == null) {
releaseResources();
return;
}
debug_resource_stage = 3; // silhouette surface ready
// 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;
}
debug_resource_stage = 4; // JFA A texture ready
rt_jfa_a_surf = textureGetSurfaceLevel(rt_jfa_a_tex.?);
if (rt_jfa_a_surf == null) {
releaseResources();
return;
}
debug_resource_stage = 5; // JFA A surface ready
// 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;
}
debug_resource_stage = 6; // JFA B texture ready
rt_jfa_b_surf = textureGetSurfaceLevel(rt_jfa_b_tex.?);
if (rt_jfa_b_surf == null) {
releaseResources();
return;
}
debug_resource_stage = 7; // all RT surfaces ready
debug_resources_ready_seen = true;
}
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;
}
}
inline for (.{
&rt_silhouette_tex, &rt_jfa_a_tex, &rt_jfa_b_tex,
}) |tex_ptr| {
if (tex_ptr.*) |t| {
comRelease(t);
tex_ptr.* = null;
}
}
resource_width = 0;
resource_height = 0;
}
// =============================================================================
// Shader source strings
// =============================================================================
/// Flat colour pixel shader - outputs PS constant c0.
const ps_flat_src = "ps_3_0\nmov oC0, c0\n";
/// Texture-alpha-aware silhouette shader.
/// c0 = outline colour/encoded width, c1.x = -coverage threshold.
const ps_alpha_src =
"ps_3_0\n" ++
"dcl_2d s0\n" ++
"dcl_texcoord0 v0\n" ++
"texld r0, v0, s0\n" ++
"add r1, r0.aaaa, c1.xxxx\n" ++
"texkill r1\n" ++
"mov oC0, c0\n";
/// Coverage shader for additive/modulated M2 layers where transparency can be
/// encoded as black RGB rather than useful texture alpha.
const ps_rgb_src =
"ps_3_0\n" ++
"dcl_2d s0\n" ++
"dcl_texcoord0 v0\n" ++
"texld r0, v0, s0\n" ++
"max r1.x, r0.r, r0.g\n" ++
"max r1.x, r1.x, r0.b\n" ++
"add r1, r1.xxxx, c1.xxxx\n" ++
"texkill r1\n" ++
"mov oC0, c0\n";
/// JFA init: sample silhouette, output own UV as seed or sentinel (-1,-1).
/// Sentinel must be outside [0,1] UV space so it never wins distance comparisons.
const jfa_init_src =
"ps_3_0\n" ++
"def c0, -1.0, -1.0, -0.002, 0.0\n" ++
"dcl_2d s0\n" ++
"dcl_texcoord0 v0\n" ++
"texld r0, v0, s0\n" ++
"add r0.x, r0.a, c0.z\n" ++ // alpha - 0.002
"cmp oC0.xy, r0.x, v0.xy, c0.xy\n" ++ // >= 0 → own UV (seed), < 0 → sentinel
"mov oC0.zw, c0.ww\n";
/// JFA propagation: 9-tap (self + 8 neighbors), keeps nearest seed.
/// c0.xy = step_size_uv (set per-pass by CPU).
const jfa_prop_src =
"ps_3_0\n" ++
"def c1, 0.0, 0.0, 0.0, 0.0\n" ++
"def c2, -1.0, -1.0, 0.0, 0.0\n" ++
"def c3, -1.0, 0.0, 0.0, 0.0\n" ++
"def c4, -1.0, 1.0, 0.0, 0.0\n" ++
"def c5, 0.0, -1.0, 0.0, 0.0\n" ++
"def c6, 0.0, 1.0, 0.0, 0.0\n" ++
"def c7, 1.0, -1.0, 0.0, 0.0\n" ++
"def c8, 1.0, 0.0, 0.0, 0.0\n" ++
"def c9, 1.0, 1.0, 0.0, 0.0\n" ++
"dcl_2d s0\n" ++
"dcl_texcoord0 v0\n" ++
// D3DX on this client only allows one c# register per arithmetic
// instruction. Copy c0.xy (step size) to a temp once, then combine
// that temp with c2..c9 in the neighbor MADs.
"mov r7.xy, c0.xy\n" ++
// 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²
"mov r8.xy, r0.xy\n" ++ // best seed UV
// Neighbor (-1,-1) via c2
"mad r4.xy, c2.xy, r7.xy, v0.xy\n" ++
"texld r5, r4, s0\n" ++
"sub r2.xy, v0.xy, r5.xy\n" ++
"dp2add r2.z, r2, r2, c1.x\n" ++
"sub r3.x, r2.z, r9.x\n" ++
"cmp r8.xy, r3.x, r8.xy, r5.xy\n" ++
"cmp r9.x, r3.x, r9.x, r2.z\n" ++
// Neighbor (-1, 0) via c3
"mad r4.xy, c3.xy, r7.xy, v0.xy\n" ++
"texld r5, r4, s0\n" ++
"sub r2.xy, v0.xy, r5.xy\n" ++
"dp2add r2.z, r2, r2, c1.x\n" ++
"sub r3.x, r2.z, r9.x\n" ++
"cmp r8.xy, r3.x, r8.xy, r5.xy\n" ++
"cmp r9.x, r3.x, r9.x, r2.z\n" ++
// Neighbor (-1, 1) via c4
"mad r4.xy, c4.xy, r7.xy, v0.xy\n" ++
"texld r5, r4, s0\n" ++
"sub r2.xy, v0.xy, r5.xy\n" ++
"dp2add r2.z, r2, r2, c1.x\n" ++
"sub r3.x, r2.z, r9.x\n" ++
"cmp r8.xy, r3.x, r8.xy, r5.xy\n" ++
"cmp r9.x, r3.x, r9.x, r2.z\n" ++
// Neighbor (0, -1) via c5
"mad r4.xy, c5.xy, r7.xy, v0.xy\n" ++
"texld r5, r4, s0\n" ++
"sub r2.xy, v0.xy, r5.xy\n" ++
"dp2add r2.z, r2, r2, c1.x\n" ++
"sub r3.x, r2.z, r9.x\n" ++
"cmp r8.xy, r3.x, r8.xy, r5.xy\n" ++
"cmp r9.x, r3.x, r9.x, r2.z\n" ++
// Neighbor (0, 1) via c6
"mad r4.xy, c6.xy, r7.xy, v0.xy\n" ++
"texld r5, r4, s0\n" ++
"sub r2.xy, v0.xy, r5.xy\n" ++
"dp2add r2.z, r2, r2, c1.x\n" ++
"sub r3.x, r2.z, r9.x\n" ++
"cmp r8.xy, r3.x, r8.xy, r5.xy\n" ++
"cmp r9.x, r3.x, r9.x, r2.z\n" ++
// Neighbor (1, -1) via c7
"mad r4.xy, c7.xy, r7.xy, v0.xy\n" ++
"texld r5, r4, s0\n" ++
"sub r2.xy, v0.xy, r5.xy\n" ++
"dp2add r2.z, r2, r2, c1.x\n" ++
"sub r3.x, r2.z, r9.x\n" ++
"cmp r8.xy, r3.x, r8.xy, r5.xy\n" ++
"cmp r9.x, r3.x, r9.x, r2.z\n" ++
// Neighbor (1, 0) via c8
"mad r4.xy, c8.xy, r7.xy, v0.xy\n" ++
"texld r5, r4, s0\n" ++
"sub r2.xy, v0.xy, r5.xy\n" ++
"dp2add r2.z, r2, r2, c1.x\n" ++
"sub r3.x, r2.z, r9.x\n" ++
"cmp r8.xy, r3.x, r8.xy, r5.xy\n" ++
"cmp r9.x, r3.x, r9.x, r2.z\n" ++
// Neighbor (1, 1) via c9
"mad r4.xy, c9.xy, r7.xy, v0.xy\n" ++
"texld r5, r4, s0\n" ++
"sub r2.xy, v0.xy, r5.xy\n" ++
"dp2add r2.z, r2, r2, c1.x\n" ++
"sub r3.x, r2.z, r9.x\n" ++
"cmp r8.xy, r3.x, r8.xy, r5.xy\n" ++
"cmp r9.x, r3.x, r9.x, r2.z\n" ++
// Output best seed UV
"mov oC0.xy, r8.xy\n" ++
"mov oC0.zw, c1.xx\n";
/// JFA decode + composite: compute distance to nearest seed, threshold, output outline.
/// c0 = (screen_width, screen_height, 4.0, 0.0) set by CPU.
const jfa_decode_src =
"ps_3_0\n" ++
"def c1, 0.0, 1.0, -0.002, 0.0\n" ++
"dcl_2d s0\n" ++ // JFA result (nearest seed UV)
"dcl_2d s1\n" ++ // silhouette (colour + width-encoded alpha)
"dcl_texcoord0 v0\n" ++
// Read nearest seed UV
"texld r0, v0, s0\n" ++
// Pixel-space squared distance
"sub r1.xy, v0.xy, r0.xy\n" ++
"mul r1.xy, r1.xy, c0.xy\n" ++ // (du*W, dv*H)
"dp2add r1.z, r1, r1, c0.w\n" ++ // dist² in pixels
// Read seed's silhouette colour + width
"texld r2, r0, s1\n" ++
"mul r3.x, r2.a, c0.z\n" ++ // outline_width = alpha * 4.0
"mul r3.x, r3.x, r3.x\n" ++ // width²
// Threshold: inside outline if dist² < width²
"sub r3.y, r1.z, r3.x\n" ++ // dist² - width²
"mov r6.w, c1.y\n" ++ // temp 1.0: avoid reading c0 + c1 in one instruction
"cmp r4.w, r3.y, c0.w, r6.w\n" ++ // >= 0 → 0 (outside), < 0 → 1 (inside)
// Exclude silhouette interior (don't draw outline ON the model)
"texld r5, v0, s1\n" ++ // silhouette at current pixel
"add r5.x, r5.a, c1.z\n" ++ // alpha - 0.002
"cmp r4.w, r5.x, c0.w, r4.w\n" ++ // if inside silhouette → 0
// Output
"mov r4.xyz, r2.xyz\n" ++ // outline colour from seed
"mov oC0, r4\n";
/// Debug: composite silhouette RT directly. Forces alpha to 1.0 where silhouette
/// has any content (alpha >= 0.002), 0.0 elsewhere. Bypasses JFA entirely.
const debug_sil_src =
"ps_3_0\n" ++
"def c0, 0.0, 0.0, -0.002, 1.0\n" ++
"dcl_2d s0\n" ++
"dcl_texcoord0 v0\n" ++
"texld r0, v0, s0\n" ++
"add r1.x, r0.a, c0.z\n" ++ // alpha - 0.002
"cmp r0.w, r1.x, c0.w, c0.x\n" ++ // >= 0 → 1.0 (opaque), < 0 → 0.0 (transparent)
"mov oC0, r0\n";
// =============================================================================
// Shader creation
// =============================================================================
fn ensureShaders(device: *anyopaque) void {
shaders_attempted = true;
debug_shader_stage = 1; // entered
const d3dx = LoadLibraryA("d3dx9_43.dll") orelse
LoadLibraryA("d3dx9_42.dll") orelse
LoadLibraryA("d3dx9_41.dll") orelse return;
debug_shader_stage = 2; // D3DX loaded
const assemble_ptr = GetProcAddress(d3dx, "D3DXAssembleShader") orelse return;
debug_shader_stage = 3; // assembler found
const assemble: D3DXAssembleShaderFn = @ptrCast(assemble_ptr);
// --- Flat-colour PS (for solid silhouettes) ---
outline_ps = assemblePS(device, assemble, ps_flat_src, ps_flat_src.len) orelse return;
// --- Alpha-aware silhouette PS (for textured cutout/translucent planes) ---
outline_alpha_ps = assemblePS(device, assemble, ps_alpha_src, ps_alpha_src.len) orelse {
releaseShaders();
return;
};
outline_rgb_ps = assemblePS(device, assemble, ps_rgb_src, ps_rgb_src.len) orelse {
releaseShaders();
return;
};
debug_shader_stage = 4; // silhouette PS variants ready
// --- JFA Init PS ---
jfa_init_ps = assemblePS(device, assemble, jfa_init_src, jfa_init_src.len) orelse {
releaseShaders();
return;
};
debug_shader_stage = 5; // JFA init ready
// --- JFA Propagation PS ---
jfa_prop_ps = assemblePS(device, assemble, jfa_prop_src, jfa_prop_src.len) orelse {
releaseShaders();
return;
};
debug_shader_stage = 6; // JFA propagation ready
// --- JFA Decode + Composite PS ---
jfa_decode_ps = assemblePS(device, assemble, jfa_decode_src, jfa_decode_src.len) orelse {
releaseShaders();
return;
};
debug_shader_stage = 7; // JFA decode ready
// --- Debug silhouette composite PS (only when diagnostic enabled) ---
if (DEBUG_SHOW_SILHOUETTE) {
debug_sil_ps = assemblePS(device, assemble, debug_sil_src, debug_sil_src.len) orelse {
releaseShaders();
return;
};
}
debug_shader_stage = 8; // complete
debug_shaders_ready_seen = true;
}
/// Assemble a pixel shader from source text, create device PS object.
fn captureD3DXError(buf: *anyopaque) void {
@memset(&debug_shader_error_text, 0);
const get_ptr: *const fn (*anyopaque) callconv(hook.cc.stdcall) usize =
@ptrFromInt(vt(buf)[3]);
const get_size: *const fn (*anyopaque) callconv(hook.cc.stdcall) usize =
@ptrFromInt(vt(buf)[4]);
const ptr_val = get_ptr(buf);
const size = get_size(buf);
if (ptr_val == 0 or size == 0) return;
const src_ptr: [*]const u8 = @ptrFromInt(ptr_val);
const n = @min(size, debug_shader_error_text.len - 1);
@memcpy(debug_shader_error_text[0..n], src_ptr[0..n]);
// Make sure the chat string ends cleanly even if the D3DX buffer does not.
debug_shader_error_text[n] = 0;
}
fn assemblePS(device: *anyopaque, assemble: D3DXAssembleShaderFn, src: [*]const u8, len: usize) ?*anyopaque {
var code: ?*anyopaque = null;
var err_buf: ?*anyopaque = null;
const assemble_hr = assemble(src, @intCast(len), null, null, 0, &code, &err_buf);
if (assemble_hr < 0 or code == null) {
debug_shader_assemble_hr = assemble_hr;
if (err_buf) |e| {
captureD3DXError(e);
comRelease(e);
}
return null;
}
defer comRelease(code.?);
if (err_buf) |e| comRelease(e);
// ID3DXBuffer::GetBufferPointer is vtable[3]
const buf_ptr: *anyopaque = @ptrFromInt(
@as(*const fn (*anyopaque) callconv(hook.cc.stdcall) usize, @ptrFromInt(vt(code.?)[3]))(code.?),
);
var ps_out: ?*anyopaque = null;
const create: *const fn (*anyopaque, *anyopaque, *?*anyopaque) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(device)[types.VT.CreatePixelShader]);
const create_hr = create(device, buf_ptr, &ps_out);
if (create_hr < 0) {
debug_shader_create_hr = create_hr;
return null;
}
return ps_out;
}
fn releaseShaders() void {
inline for (.{ &outline_ps, &outline_alpha_ps, &outline_rgb_ps, &jfa_init_ps, &jfa_prop_ps, &jfa_decode_ps, &debug_sil_ps }) |ps| {
if (ps.*) |p| {
comRelease(p);
ps.* = null;
}
}
shaders_attempted = false;
}
// =============================================================================
// Fullscreen quad builder
// =============================================================================
fn buildFullscreenQuad(w: u32, h: u32) [4]QuadVertex {
const fw = @as(f32, @floatFromInt(w));
const fh = @as(f32, @floatFromInt(h));
// D3D9 half-pixel offset for pixel-perfect UV mapping
return .{
.{ .x = -0.5, .y = -0.5, .z = 0.0, .rhw = 1.0, .u = 0.0, .v = 0.0 },
.{ .x = fw - 0.5, .y = -0.5, .z = 0.0, .rhw = 1.0, .u = 1.0, .v = 0.0 },
.{ .x = -0.5, .y = fh - 0.5, .z = 0.0, .rhw = 1.0, .u = 0.0, .v = 1.0 },
.{ .x = fw - 0.5, .y = fh - 0.5, .z = 0.0, .rhw = 1.0, .u = 1.0, .v = 1.0 },
};
}
// =============================================================================
// EndScene hook
// =============================================================================
fn hkEndScene(device: *anyopaque) callconv(hook.cc.stdcall) i32 {
debug_endscene_seen = true;
// DEBUG23: no forced D3D9 Reset. The client can hang the render thread
// during Reset, so Outline runs without a stencil dependency.
// Per-frame: scan objects for outline tracking
tracker.scanObjects();
// Run outline pipeline if any draws were cached this frame:
// Phase 1 (inside runJfaPipeline): replay cached draws → silhouette RT
// Phase 2: JFA init → propagation → decode+composite → backbuffer
if (frame_has_outlines) {
runJfaPipeline(device);
}
// Diagnostics: log pipeline stage counts (first 20 active frames)
tracker.logDiagnostics(cached_draw_count);
// Safety: release any cached draws that weren't replayed (e.g. missing resources)
if (cached_draw_count > 0) clearCachedDraws();
// Reset per-frame flags for next frame
model_hook.rendering_outline = false;
model_hook.current_model = 0;
frame_has_outlines = false;
// Call original EndScene
const f: *const fn (*anyopaque) callconv(hook.cc.stdcall) i32 = @ptrFromInt(orig_endscene);
return f(device);
}
// =============================================================================
// Reset hook - force D24S8 depth/stencil format, release resources
// =============================================================================
fn hkReset(device: *anyopaque, pp: *types.D3DPRESENT_PARAMETERS) callconv(hook.cc.stdcall) i32 {
if (pp.EnableAutoDepthStencil != 0) {
const fmt = pp.AutoDepthStencilFormat;
if (fmt != types.D3DFMT_D24S8 and fmt != types.D3DFMT_D24FS8 and
fmt != types.D3DFMT_D24X4S4 and fmt != types.D3DFMT_D15S1)
{
pp.AutoDepthStencilFormat = types.D3DFMT_D24S8;
}
}
// Release all resources (device state lost on reset)
clearCachedDraws();
releaseShaders();
releaseResources();
const f: *const fn (*anyopaque, *types.D3DPRESENT_PARAMETERS) callconv(hook.cc.stdcall) i32 = @ptrFromInt(orig_reset);
return f(device, pp);
}
// =============================================================================
// 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
// replayed to the silhouette RT in EndScene. The DIP hook itself does NOT
// modify any device state and calls the original DIP exactly once.
//
// This avoids the "double-DIP" pattern that corrupted WoW's internal
// rendering state (likely GxDevice state cache or batch counters).
fn hkDIP(
device: *anyopaque,
prim_type: u32,
base_vtx: i32,
min_vtx: u32,
num_verts: u32,
start_idx: u32,
prim_count: u32,
) callconv(hook.cc.stdcall) i32 {
debug_dip_seen = true;
const OrigDIP = *const fn (*anyopaque, u32, i32, u32, u32, u32, u32) callconv(hook.cc.stdcall) i32;
const origFn: OrigDIP = @ptrFromInt(orig_dip);
// ---- Cache outline draws for EndScene replay ----
if (model_hook.rendering_outline) {
debug_outline_dip_seen = true;
const model_ptr = model_hook.current_model;
const color = tracker.getModelColor(model_ptr) orelse
return origFn(device, prim_type, base_vtx, min_vtx, num_verts, start_idx, prim_count);
const category = tracker.getModelCategory(model_ptr);
// DEBUG26: skip genuinely blended/translucent passes when building the
// silhouette mask. These passes often use multi-texture/vertex-alpha/
// material shader logic that cannot be reproduced correctly by sampling
// Texture0 alone, and their carrier polygons create huge straight edges.
//
// Keep alpha-tested cutouts: those are discrete visible/hidden texels
// and can be replayed cleanly using WoW's ALPHAREF.
const cur_alpha_test = deviceGetRS(device, types.D3DRS.ALPHATESTENABLE);
const cur_alpha_blend = deviceGetRS(device, types.D3DRS.ALPHABLENDENABLE);
if (cur_alpha_blend != 0 and cur_alpha_test == 0) {
debug_translucent_skipped_seen = true;
return origFn(device, prim_type, base_vtx, min_vtx, num_verts, start_idx, prim_count);
}
// Ensure resources will be available for replay in EndScene
if (!shaders_attempted) ensureShaders(device);
ensureResources(device);
if (outline_ps == null or rt_silhouette_surf == null)
return origFn(device, prim_type, base_vtx, min_vtx, num_verts, start_idx, prim_count);
// Cache if room available
if (cached_draw_count < MAX_CACHED_DRAWS) {
const idx = cached_draw_count;
var draw = &cached_draws[idx];
// Draw parameters
draw.prim_type = prim_type;
draw.base_vtx = base_vtx;
draw.min_vtx = min_vtx;
draw.num_verts = num_verts;
draw.start_idx = start_idx;
draw.prim_count = prim_count;
// Outline info
draw.color = color;
draw.category = category;
// 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
draw.ib = deviceGetIndices(device);
// GetIndices AddRef's the IB
draw.vertex_decl = deviceGetPtr(device, types.VT.GetVertexDeclaration);
// GetVertexDeclaration AddRef's
draw.vertex_shader = deviceGetPtr(device, types.VT.GetVertexShader);
// GetVertexShader AddRef's
draw.tex0 = deviceGetTexture(device, 0);
// GetTexture AddRef's
draw.alpha_test_enable = deviceGetRS(device, types.D3DRS.ALPHATESTENABLE);
draw.alpha_ref = deviceGetRS(device, types.D3DRS.ALPHAREF);
draw.alpha_func = deviceGetRS(device, types.D3DRS.ALPHAFUNC);
draw.alpha_blend_enable = deviceGetRS(device, types.D3DRS.ALPHABLENDENABLE);
draw.src_blend = deviceGetRS(device, types.D3DRS.SRCBLEND);
draw.dst_blend = deviceGetRS(device, types.D3DRS.DESTBLEND);
// Capture VS constants (bone matrices, world/view/proj transforms)
deviceGetVSConstF(device, 0, &draw.vs_consts, MAX_VS_CONST_REGS);
cached_draw_count = idx + 1;
frame_has_outlines = true;
debug_cached_draw_seen = true;
}
// DEBUG23: no stencil writes. Preserve WoW's D3D state and draw once.
// The cached geometry is replayed later into the silhouette RT.
return origFn(device, prim_type, base_vtx, min_vtx, num_verts, start_idx, prim_count);
}
// ---- Normal path ----
return origFn(device, prim_type, base_vtx, min_vtx, num_verts, start_idx, prim_count);
}
/// Release AddRef'd COM objects in cached draws and reset count.
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.tex0) |obj| {
comRelease(obj);
draw.tex0 = null;
}
}
cached_draw_count = 0;
}
// =============================================================================
// JFA pipeline (called from EndScene when outlines exist)
// =============================================================================
fn runJfaPipeline(device: *anyopaque) void {
debug_pipeline_entered_seen = true;
// Verify all resources and shaders
if (rt_silhouette_tex == null or rt_jfa_a_surf == null or rt_jfa_b_surf == null) return;
if (!shaders_attempted) ensureShaders(device);
if (jfa_init_ps == null or jfa_prop_ps == null or jfa_decode_ps == null) return;
if (outline_ps == null or rt_silhouette_surf == null) return;
debug_pipeline_ready_seen = true;
var vp: types.D3DVIEWPORT9 = .{};
deviceGetViewport(device, &vp);
if (vp.Width == 0 or vp.Height == 0) return;
// =====================================================================
// Save ALL state that replay + JFA will modify (manual, no state blocks)
// =====================================================================
// 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);
const saved_decl = deviceGetPtr(device, types.VT.GetVertexDeclaration);
const saved_ib = deviceGetIndices(device);
const saved_tex0 = deviceGetTexture(device, 0);
const saved_tex1 = deviceGetTexture(device, 1);
var saved_vb: ?*anyopaque = null;
var saved_vb_offset: u32 = 0;
var saved_vb_stride: u32 = 0;
deviceGetStreamSource(device, 0, &saved_vb, &saved_vb_offset, &saved_vb_stride);
// Depth-stencil surface
var saved_ds: ?*anyopaque = null;
const getDS: *const fn (*anyopaque, *?*anyopaque) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(device)[types.VT.GetDepthStencilSurface]);
_ = getDS(device, &saved_ds);
// Render states
const saved_zenable = deviceGetRS(device, types.D3DRS.ZENABLE);
const saved_zwrite = deviceGetRS(device, types.D3DRS.ZWRITEENABLE);
const saved_zfunc = deviceGetRS(device, types.D3DRS.ZFUNC);
const saved_ablend = deviceGetRS(device, types.D3DRS.ALPHABLENDENABLE);
const saved_srcblend = deviceGetRS(device, types.D3DRS.SRCBLEND);
const saved_dstblend = deviceGetRS(device, types.D3DRS.DESTBLEND);
const saved_cull = deviceGetRS(device, types.D3DRS.CULLMODE);
const saved_atest = deviceGetRS(device, types.D3DRS.ALPHATESTENABLE);
const saved_cwrite = deviceGetRS(device, types.D3DRS.COLORWRITEENABLE);
// Stencil states (Phase 1 reads stencil marks written by DIP hook)
const saved_stencil_enable = deviceGetRS(device, types.D3DRS.STENCILENABLE);
const saved_stencil_func = deviceGetRS(device, types.D3DRS.STENCILFUNC);
const saved_stencil_ref = deviceGetRS(device, types.D3DRS.STENCILREF);
const saved_stencil_mask = deviceGetRS(device, types.D3DRS.STENCILMASK);
const saved_stencil_wmask = deviceGetRS(device, types.D3DRS.STENCILWRITEMASK);
const saved_stencil_pass = deviceGetRS(device, types.D3DRS.STENCILPASS);
// Sampler states (samplers 0 and 1, 5 states each)
const SampState = struct { addru: u32, addrv: u32, mag: u32, min: u32, mip: u32 };
const readSamp = struct {
fn f(dev: *anyopaque, stage: u32) SampState {
return .{
.addru = deviceGetSamplerState(dev, stage, types.D3DSAMP.ADDRESSU),
.addrv = deviceGetSamplerState(dev, stage, types.D3DSAMP.ADDRESSV),
.mag = deviceGetSamplerState(dev, stage, types.D3DSAMP.MAGFILTER),
.min = deviceGetSamplerState(dev, stage, types.D3DSAMP.MINFILTER),
.mip = deviceGetSamplerState(dev, stage, types.D3DSAMP.MIPFILTER),
};
}
}.f;
const saved_samp0 = readSamp(device, 0);
const saved_samp1 = readSamp(device, 1);
// Shader constants
var saved_psc0: [4]f32 = .{ 0, 0, 0, 0 };
deviceGetPSConstF(device, 0, &saved_psc0);
var saved_vs_consts: [MAX_VS_CONST_REGS][4]f32 = undefined;
deviceGetVSConstF(device, 0, &saved_vs_consts, MAX_VS_CONST_REGS);
// =====================================================================
// Phase 1: Replay cached draws to silhouette RT
// =====================================================================
if (cached_draw_count > 0) {
const origFn: *const fn (*anyopaque, u32, i32, u32, u32, u32, u32) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(orig_dip);
deviceSetRenderTarget(device, 0, rt_silhouette_surf.?);
clearRenderTarget(device, 0x00000000);
// 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);
deviceSetRS(device, types.D3DRS.ZENABLE, types.D3DZB_FALSE);
deviceSetRS(device, types.D3DRS.STENCILWRITEMASK, 0);
deviceSetRS(device, types.D3DRS.ALPHABLENDENABLE, 0);
deviceSetRS(device, types.D3DRS.COLORWRITEENABLE, 0x0F);
for (0..cached_draw_count) |i| {
const draw = &cached_draws[i];
deviceSetStreamSource(device, 0, draw.vb, draw.vb_offset, draw.vb_stride);
deviceSetIndices(device, draw.ib);
deviceSetPtrOrNull(device, types.VT.SetVertexDeclaration, draw.vertex_decl);
deviceSetPtrOrNull(device, types.VT.SetVertexShader, draw.vertex_shader);
deviceSetVSConstF(device, 0, &draw.vs_consts, MAX_VS_CONST_REGS);
var color_f4 = argbToFloat4(draw.color);
color_f4[3] = tracker.getOutlinePixels(draw.category) / 4.0;
deviceSetPSConstF(device, 0, &color_f4);
// DEBUG26: only opaque or alpha-tested draws reach the cache.
if (draw.tex0 != null and draw.alpha_test_enable != 0 and
(draw.alpha_func == types.D3DCMP_GREATER or draw.alpha_func == types.D3DCMP_GREATEREQUAL))
{
deviceSetTexture(device, 0, draw.tex0);
const ref_f = @as(f32, @floatFromInt(draw.alpha_ref & 0xFF)) / 255.0;
const alpha_cut: [4]f32 = .{ -@max(ref_f, 0.01), 0, 0, 0 };
deviceSetPSConstF(device, 1, &alpha_cut);
deviceSetPtr(device, types.VT.SetPixelShader, outline_alpha_ps.?);
} else {
deviceSetTexture(device, 0, null);
deviceSetPtr(device, types.VT.SetPixelShader, outline_ps.?);
}
// DEBUG24: still no stencil/reset dependency.
deviceSetRS(device, types.D3DRS.STENCILENABLE, 0);
_ = origFn(device, draw.prim_type, draw.base_vtx, draw.min_vtx, draw.num_verts, draw.start_idx, draw.prim_count);
}
clearCachedDraws();
}
// =====================================================================
// Debug: skip JFA, composite raw silhouette RT to see if banding is
// in the silhouette (stale VB / replay issue) or the JFA pipeline.
// =====================================================================
if (DEBUG_SHOW_SILHOUETTE) {
if (debug_sil_ps) |dps| {
if (saved_rt0) |rt| deviceSetRenderTarget(device, 0, rt);
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.CULLMODE, types.D3DCULL_NONE);
deviceSetRS(device, types.D3DRS.ALPHATESTENABLE, 0);
deviceSetRS(device, types.D3DRS.COLORWRITEENABLE, 0x0F);
deviceSetRS(device, types.D3DRS.ALPHABLENDENABLE, 1);
deviceSetRS(device, types.D3DRS.SRCBLEND, types.D3DBLEND_SRCALPHA);
deviceSetRS(device, types.D3DRS.DESTBLEND, types.D3DBLEND_INVSRCALPHA);
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);
deviceSetTexture(device, 0, rt_silhouette_tex);
deviceSetPtr(device, types.VT.SetPixelShader, dps);
const quad = buildFullscreenQuad(vp.Width, vp.Height);
deviceDrawPrimitiveUP(device, types.D3DPT_TRIANGLESTRIP, 2, @ptrCast(&quad), @sizeOf(QuadVertex));
}
// Skip JFA - jump straight to state restore
} else {
// =====================================================================
// 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);
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)));
// 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;
// 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=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);
} // end else (normal JFA path)
// =====================================================================
// Restore ALL state
// =====================================================================
// Render states
deviceSetRS(device, types.D3DRS.ZENABLE, saved_zenable);
deviceSetRS(device, types.D3DRS.ZWRITEENABLE, saved_zwrite);
deviceSetRS(device, types.D3DRS.ZFUNC, saved_zfunc);
deviceSetRS(device, types.D3DRS.ALPHABLENDENABLE, saved_ablend);
deviceSetRS(device, types.D3DRS.SRCBLEND, saved_srcblend);
deviceSetRS(device, types.D3DRS.DESTBLEND, saved_dstblend);
deviceSetRS(device, types.D3DRS.CULLMODE, saved_cull);
deviceSetRS(device, types.D3DRS.ALPHATESTENABLE, saved_atest);
deviceSetRS(device, types.D3DRS.COLORWRITEENABLE, saved_cwrite);
// Stencil states
deviceSetRS(device, types.D3DRS.STENCILENABLE, saved_stencil_enable);
deviceSetRS(device, types.D3DRS.STENCILFUNC, saved_stencil_func);
deviceSetRS(device, types.D3DRS.STENCILREF, saved_stencil_ref);
deviceSetRS(device, types.D3DRS.STENCILMASK, saved_stencil_mask);
deviceSetRS(device, types.D3DRS.STENCILWRITEMASK, saved_stencil_wmask);
deviceSetRS(device, types.D3DRS.STENCILPASS, saved_stencil_pass);
// Sampler states
const writeSamp = struct {
fn f(dev: *anyopaque, stage: u32, s: SampState) void {
deviceSetSamplerState(dev, stage, types.D3DSAMP.ADDRESSU, s.addru);
deviceSetSamplerState(dev, stage, types.D3DSAMP.ADDRESSV, s.addrv);
deviceSetSamplerState(dev, stage, types.D3DSAMP.MAGFILTER, s.mag);
deviceSetSamplerState(dev, stage, types.D3DSAMP.MINFILTER, s.min);
deviceSetSamplerState(dev, stage, types.D3DSAMP.MIPFILTER, s.mip);
}
}.f;
writeSamp(device, 0, saved_samp0);
writeSamp(device, 1, saved_samp1);
// Shader constants
deviceSetPSConstF(device, 0, &saved_psc0);
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
deviceSetPtrOrNull(device, types.VT.SetDepthStencilSurface, saved_ds); // DS
if (saved_ds) |ds| comRelease(ds);
deviceSetPtrOrNull(device, types.VT.SetPixelShader, saved_ps); // PS
if (saved_ps) |p| comRelease(p);
deviceSetPtrOrNull(device, types.VT.SetVertexShader, saved_vs); // VS
if (saved_vs) |v| comRelease(v);
deviceSetPtrOrNull(device, types.VT.SetVertexDeclaration, saved_decl); // Vertex decl
if (saved_decl) |d| comRelease(d);
deviceSetStreamSource(device, 0, saved_vb, saved_vb_offset, saved_vb_stride); // VB
if (saved_vb) |v| comRelease(v);
deviceSetIndices(device, saved_ib); // IB
if (saved_ib) |i| comRelease(i);
deviceSetTexture(device, 0, saved_tex0); // Tex0
if (saved_tex0) |t| comRelease(t);
deviceSetTexture(device, 1, saved_tex1); // Tex1
if (saved_tex1) |t| comRelease(t);
}
// =============================================================================
// Force D24S8 depth/stencil on first EndScene (deferred hooks miss initial Reset)
// =============================================================================
fn hasStencilBits(fmt: u32) bool {
return fmt == types.D3DFMT_D24S8 or fmt == types.D3DFMT_D24FS8 or
fmt == types.D3DFMT_D24X4S4 or fmt == types.D3DFMT_D15S1;
}
fn queryStencilFormat(device: *anyopaque) u32 {
var pDS: ?*anyopaque = null;
const getDS: *const fn (*anyopaque, *?*anyopaque) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(device)[types.VT.GetDepthStencilSurface]);
if (getDS(device, &pDS) < 0) return 0;
const ds = pDS orelse return 0;
var desc: types.D3DSURFACE_DESC = .{};
const getDesc: *const fn (*anyopaque, *types.D3DSURFACE_DESC) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(ds)[12]);
const hr = getDesc(ds, &desc);
comRelease(ds);
if (hr < 0) return 0;
return desc.Format;
}
fn forceD24S8IfNeeded(device: *anyopaque) bool {
const current_fmt = queryStencilFormat(device);
debug_stencil_format = current_fmt;
if (hasStencilBits(current_fmt)) {
debug_stencil_ready = true;
debug_stencil_reset_hr = 0;
return false;
}
var pSwap: ?*anyopaque = null;
const getSC: *const fn (*anyopaque, u32, *?*anyopaque) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(device)[types.VT.GetSwapChain]);
if (getSC(device, 0, &pSwap) < 0) return false;
const swap = pSwap orelse return false;
var pp: types.D3DPRESENT_PARAMETERS = .{};
const getPP: *const fn (*anyopaque, *types.D3DPRESENT_PARAMETERS) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(swap)[9]);
const pp_hr = getPP(swap, &pp);
// IMPORTANT: do not hold a swap-chain COM reference across Reset.
comRelease(swap);
if (pp_hr < 0) return false;
pp.AutoDepthStencilFormat = types.D3DFMT_D24S8;
pp.EnableAutoDepthStencil = 1;
clearCachedDraws();
releaseShaders();
releaseResources();
const resetFn: *const fn (*anyopaque, *types.D3DPRESENT_PARAMETERS) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(device)[types.VT.Reset]);
const reset_hr = resetFn(device, &pp);
debug_stencil_reset_hr = reset_hr;
if (reset_hr < 0) {
debug_stencil_ready = false;
return false;
}
const new_fmt = queryStencilFormat(device);
debug_stencil_format = new_fmt;
debug_stencil_ready = hasStencilBits(new_fmt);
return true;
}
// =============================================================================
// Vtable patching helpers
// =============================================================================
extern "kernel32" fn VirtualProtect(
addr: *anyopaque,
size: usize,
new_prot: u32,
old_prot: *u32,
) callconv(WINAPI) i32;
fn patchVtableEntry(vtable_ptr: [*]usize, idx: usize, new_fn: usize, old_fn: *usize) bool {
old_fn.* = vtable_ptr[idx];
var old_prot: u32 = 0;
const addr: *anyopaque = @ptrFromInt(@intFromPtr(&vtable_ptr[idx]));
if (VirtualProtect(addr, @sizeOf(usize), 0x40, &old_prot) == 0)
return false;
vtable_ptr[idx] = new_fn;
_ = VirtualProtect(addr, @sizeOf(usize), old_prot, &old_prot);
return true;
}
fn restoreVtableEntry(vtable_ptr: [*]usize, idx: usize, old_fn: usize) void {
var old_prot: u32 = 0;
const addr: *anyopaque = @ptrFromInt(@intFromPtr(&vtable_ptr[idx]));
if (VirtualProtect(addr, @sizeOf(usize), 0x40, &old_prot) == 0)
return;
vtable_ptr[idx] = old_fn;
_ = VirtualProtect(addr, @sizeOf(usize), old_prot, &old_prot);
}
// =============================================================================
// D3D9 device / vtable discovery from game's existing device
// =============================================================================
const offsets = @import("../offsets.zig");
fn getD3D9VTable() ?[*]usize {
const gx_device = hook.readMem(u32, offsets.GX_DEVICE_PTR);
if (gx_device == 0) return null;
const d3d_device = hook.readMem(u32, gx_device + offsets.GX_DEVICE_D3D_OFFSET);
if (d3d_device == 0) return null;
const vtable_addr = hook.readMem(u32, d3d_device);
if (vtable_addr == 0) return null;
return @ptrFromInt(vtable_addr);
}
// =============================================================================
// Install / Remove
// =============================================================================
pub fn installHooks() bool {
if (hooks_installed) return true;
const vtable_ptr = getD3D9VTable() orelse return false;
d3d9_vtable = vtable_ptr;
if (!patchVtableEntry(vtable_ptr, types.VT.EndScene, @intFromPtr(&hkEndScene), &orig_endscene)) return false;
if (!patchVtableEntry(vtable_ptr, types.VT.DrawIndexedPrimitive, @intFromPtr(&hkDIP), &orig_dip)) return false;
if (!patchVtableEntry(vtable_ptr, types.VT.Reset, @intFromPtr(&hkReset), &orig_reset)) return false;
hooks_installed = true;
return true;
}
pub fn removeHooks() void {
if (!hooks_installed) return;
clearCachedDraws();
releaseShaders();
releaseResources();
if (d3d9_vtable) |vtbl| {
if (orig_reset != 0) restoreVtableEntry(vtbl, types.VT.Reset, orig_reset);
if (orig_dip != 0) restoreVtableEntry(vtbl, types.VT.DrawIndexedPrimitive, orig_dip);
if (orig_endscene != 0) restoreVtableEntry(vtbl, types.VT.EndScene, orig_endscene);
}
hooks_installed = false;
}