DEBUG28 replay exact D3D material via state blocks

This commit is contained in:
Dusk-92
2026-08-28 22:41:04 +02:00
parent e2f3b3092a
commit ef0e32de53
+114 -61
View File
@@ -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();
}
// =====================================================================