diff --git a/src/outline/d3d9_hook.zig b/src/outline/d3d9_hook.zig index 03e1a98..6529721 100644 --- a/src/outline/d3d9_hook.zig +++ b/src/outline/d3d9_hook.zig @@ -63,6 +63,7 @@ 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_state_block_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; @@ -155,6 +156,7 @@ pub fn requestStencilCheck() void { 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 material_mask_ps: ?*anyopaque = null; // exact-material scratch -> binary mask 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 @@ -181,7 +183,9 @@ const D3DXAssembleShaderFn = *const fn ( // Render target resources // ============================================================================= -var rt_silhouette_tex: ?*anyopaque = null; // A8R8G8B8 silhouette texture +var rt_material_tex: ?*anyopaque = null; // A8R8G8B8 exact-material scratch +var rt_material_surf: ?*anyopaque = null; +var rt_silhouette_tex: ?*anyopaque = null; // A8R8G8B8 normalized silhouette mask var rt_silhouette_surf: ?*anyopaque = null; var rt_jfa_a_tex: ?*anyopaque = null; // G16R16F JFA ping texture var rt_jfa_a_surf: ?*anyopaque = null; @@ -224,6 +228,7 @@ const CachedDraw = struct { vertex_shader: ?*anyopaque = null, tex: [4]?*anyopaque = .{ null, null, null, null }, pixel_shader: ?*anyopaque = null, + state_block: ?*anyopaque = null, alpha_op: [4]u32 = .{ 1, 1, 1, 1 }, alpha_arg1: [4]u32 = .{ 0, 0, 0, 0 }, alpha_arg2: [4]u32 = .{ 0, 0, 0, 0 }, @@ -342,6 +347,20 @@ fn deviceSetTSS(dev: *anyopaque, stage: u32, state_type: u32, value: u32) void { _ = f(dev, stage, state_type, value); } +fn deviceCreateStateBlock(dev: *anyopaque) ?*anyopaque { + var out: ?*anyopaque = null; + const f: *const fn (*anyopaque, u32, *?*anyopaque) callconv(hook.cc.stdcall) i32 = + @ptrFromInt(vt(dev)[types.VT.CreateStateBlock]); + if (f(dev, types.D3DSBT_ALL, &out) < 0) return null; + return out; +} + +fn stateBlockApply(sb: *anyopaque) bool { + const f: *const fn (*anyopaque) callconv(hook.cc.stdcall) i32 = + @ptrFromInt(vt(sb)[4]); + return f(sb) >= 0; +} + fn deviceSetFVF(dev: *anyopaque, fvf: u32) void { const f: *const fn (*anyopaque, u32) callconv(hook.cc.stdcall) i32 = @ptrFromInt(vt(dev)[types.VT.SetFVF]); @@ -469,14 +488,25 @@ fn ensureResources(device: *anyopaque) void { // Check if resources already match current dimensions if (vp.Width == resource_width and vp.Height == resource_height and - rt_silhouette_tex != null) return; + rt_material_tex != null and rt_silhouette_tex != null) return; // Release old and create new releaseResources(); resource_width = vp.Width; resource_height = vp.Height; - // Silhouette RT (A8R8G8B8) + // Exact-material scratch RT (A8R8G8B8) + if (deviceCreateTexture(device, vp.Width, vp.Height, 1, types.D3DUSAGE_RENDERTARGET, types.D3DFMT_A8R8G8B8, types.D3DPOOL_DEFAULT, &rt_material_tex) < 0) { + releaseResources(); + return; + } + rt_material_surf = textureGetSurfaceLevel(rt_material_tex.?); + if (rt_material_surf == null) { + releaseResources(); + return; + } + + // Normalized 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; @@ -520,7 +550,7 @@ fn ensureResources(device: *anyopaque) void { fn releaseResources() void { inline for (.{ - &rt_silhouette_surf, &rt_jfa_a_surf, &rt_jfa_b_surf, + &rt_material_surf, &rt_silhouette_surf, &rt_jfa_a_surf, &rt_jfa_b_surf, }) |surf_ptr| { if (surf_ptr.*) |s| { comRelease(s); @@ -528,7 +558,7 @@ fn releaseResources() void { } } inline for (.{ - &rt_silhouette_tex, &rt_jfa_a_tex, &rt_jfa_b_tex, + &rt_material_tex, &rt_silhouette_tex, &rt_jfa_a_tex, &rt_jfa_b_tex, }) |tex_ptr| { if (tex_ptr.*) |t| { comRelease(t); @@ -570,6 +600,22 @@ const ps_rgb_src = "texkill r1\n" ++ "mov oC0, c0\n"; +/// Normalize the exact-material scratch into the uniform outline mask. +/// c0 = outline colour (alpha forced to 1), c1.x = -coverage threshold. +/// Coverage accepts either alpha or RGB, so opaque dark materials and additive +/// effects both survive while untouched transparent pixels remain discarded. +const material_mask_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" ++ + "max r1.x, r1.x, r0.a\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 = @@ -745,7 +791,11 @@ fn ensureShaders(device: *anyopaque) void { releaseShaders(); return; }; - debug_shader_stage = 4; // silhouette PS variants ready + material_mask_ps = assemblePS(device, assemble, material_mask_src, material_mask_src.len) orelse { + releaseShaders(); + return; + }; + debug_shader_stage = 4; // silhouette/material-mask PS variants ready // --- JFA Init PS --- jfa_init_ps = assemblePS(device, assemble, jfa_init_src, jfa_init_src.len) orelse { @@ -832,7 +882,7 @@ fn assemblePS(device: *anyopaque, assemble: D3DXAssembleShaderFn, src: [*]const } 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| { + inline for (.{ &outline_ps, &outline_alpha_ps, &outline_rgb_ps, &material_mask_ps, &jfa_init_ps, &jfa_prop_ps, &jfa_decode_ps, &debug_sil_ps }) |ps| { if (ps.*) |p| { comRelease(p); ps.* = null; @@ -988,6 +1038,8 @@ fn hkDIP( draw.alpha_arg2[stage] = deviceGetTSS(device, @intCast(stage), types.D3DTSS.ALPHAARG2); } draw.pixel_shader = deviceGetPtr(device, types.VT.GetPixelShader); + draw.state_block = deviceCreateStateBlock(device); + if (draw.state_block != null) debug_state_block_seen = true; 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); @@ -1036,6 +1088,10 @@ fn clearCachedDraws() void { comRelease(obj); draw.pixel_shader = null; } + if (draw.state_block) |obj| { + comRelease(obj); + draw.state_block = null; + } for (0..4) |stage| { if (draw.tex[stage]) |obj| { comRelease(obj); @@ -1054,10 +1110,10 @@ 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 (rt_material_tex == null or rt_material_surf == null or 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; + if (outline_ps == null or material_mask_ps == null or rt_silhouette_surf == null) return; debug_pipeline_ready_seen = true; @@ -1157,74 +1213,71 @@ fn runJfaPipeline(device: *anyopaque) void { if (cached_draw_count > 0) { const origFn: *const fn (*anyopaque, u32, i32, u32, u32, u32, u32) callconv(hook.cc.stdcall) i32 = @ptrFromInt(orig_dip); + const quad = buildFullscreenQuad(vp.Width, vp.Height); + // Accumulate normalized per-draw coverage into the silhouette mask. 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); - - // DEBUG27: if WoW used the fixed-function pixel pipeline, let - // D3D9 rebuild the original material alpha itself. We override only - // the colour chain with TEXTUREFACTOR/CURRENT, while preserving the - // original ALPHAOP/ALPHAARG chain and alpha-test states. - if (draw.pixel_shader == null) { - deviceSetPtrOrNull(device, types.VT.SetPixelShader, null); - deviceSetRS(device, types.D3DRS.TEXTUREFACTOR, draw.color); - for (0..4) |stage| { - deviceSetTexture(device, @intCast(stage), draw.tex[stage]); - deviceSetTSS(device, @intCast(stage), types.D3DTSS.ALPHAOP, draw.alpha_op[stage]); - deviceSetTSS(device, @intCast(stage), types.D3DTSS.ALPHAARG1, draw.alpha_arg1[stage]); - deviceSetTSS(device, @intCast(stage), types.D3DTSS.ALPHAARG2, draw.alpha_arg2[stage]); - deviceSetTSS(device, @intCast(stage), types.D3DTSS.COLOROP, types.D3DTOP_SELECTARG1); - deviceSetTSS(device, @intCast(stage), types.D3DTSS.COLORARG1, - if (stage == 0) types.D3DTA_TFACTOR else types.D3DTA_CURRENT); - } - deviceSetRS(device, types.D3DRS.ALPHATESTENABLE, draw.alpha_test_enable); - deviceSetRS(device, types.D3DRS.ALPHAREF, draw.alpha_ref); - deviceSetRS(device, types.D3DRS.ALPHAFUNC, draw.alpha_func); + // 1) Replay this draw with WoW's exact captured D3D state into a + // transparent scratch RT. A D3DSBT_ALL state block restores pixel + // shader, PS constants, textures, samplers, texture stages, blend, + // alpha-test, vertex state and stream bindings. + if (draw.state_block) |sb| { + _ = stateBlockApply(sb); } else { - // Custom-PS fallback: Texture0 alpha cutout remains safer than - // filling the entire carrier polygon. - deviceSetRS(device, types.D3DRS.ALPHATESTENABLE, 0); - if (draw.tex[0]) |tex0| { - deviceSetTexture(device, 0, tex0); - const cut: [4]f32 = .{ -0.05, 0, 0, 0 }; - deviceSetPSConstF(device, 1, &cut); - deviceSetPtr(device, types.VT.SetPixelShader, outline_alpha_ps.?); - } else { - deviceSetPtr(device, types.VT.SetPixelShader, outline_ps.?); - } + // Safe fallback for a failed state-block capture. + 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); } - // DEBUG24: still no stencil/reset dependency. + deviceSetRenderTarget(device, 0, rt_material_surf.?); + clearRenderTarget(device, 0x00000000); + deviceSetRS(device, types.D3DRS.ZENABLE, types.D3DZB_FALSE); + deviceSetRS(device, types.D3DRS.ZWRITEENABLE, 0); deviceSetRS(device, types.D3DRS.STENCILENABLE, 0); + deviceSetRS(device, types.D3DRS.COLORWRITEENABLE, 0x0F); _ = origFn(device, draw.prim_type, draw.base_vtx, draw.min_vtx, draw.num_verts, draw.start_idx, draw.prim_count); + + // 2) Convert only pixels actually produced by that exact material + // into the uniform outline mask, preserving the draw/category colour. + deviceSetRenderTarget(device, 0, rt_silhouette_surf.?); + 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.STENCILENABLE, 0); + deviceSetRS(device, types.D3DRS.ALPHATESTENABLE, 0); + deviceSetRS(device, types.D3DRS.CULLMODE, types.D3DCULL_NONE); + 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_material_tex); + deviceSetPtr(device, types.VT.SetPixelShader, material_mask_ps.?); + + var mask_color = argbToFloat4(draw.color); + mask_color[3] = 1.0; + deviceSetPSConstF(device, 0, &mask_color); + const threshold: [4]f32 = .{ -0.01, 0.0, 0.0, 0.0 }; + deviceSetPSConstF(device, 1, &threshold); + deviceDrawPrimitiveUP(device, types.D3DPT_TRIANGLESTRIP, 2, @ptrCast(&quad), @sizeOf(QuadVertex)); } clearCachedDraws(); - } // =====================================================================