//! 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; }