Add module control API, bigcursor with fractional scaling, shared mutex

Module control API:
- Three cdecl exports: WeirdUtils_IsModuleActive, WeirdUtils_DisableModule, WeirdUtils_DisableAll
- C header include/weirdutils_api.h for runtime DLL discovery
- All modules gain pub module_name, isActive(), shared mutex via src/mutex.zig
- build.zig refactored to module_list array of ModuleDesc

Bigcursor:
- D3D9 vtable hooks on SetCursorProperties/ShowCursor
- Scale2x/Scale3x pixel-art upscalers in pure Zig (replaces 12K-line hqx C)
- Bilinear resampler for fractional scales (1.0-4.0, default 1.2)
- Win32 cursor via CreateIconIndirect bypasses D3D9 32x32 limit
- FNV-1a hash cache for ~16 cursor bitmaps
- Lua API: SetCursorScale(n) / GetCursorScale()
- CVar cursorScale for Config.wtf persistence (tenths)

Other:
- Add dpslog module stub
- Docs and README updates
This commit is contained in:
MarcelineVQ
2026-03-06 13:27:35 -08:00
parent 0783f3b0dd
commit 2aaa089c76
44 changed files with 1904 additions and 1022 deletions
+143 -114
View File
@@ -126,7 +126,7 @@ const CachedDraw = struct {
num_verts: u32 = 0,
start_idx: u32 = 0,
prim_count: u32 = 0,
// GPU state (AddRef'd COM objects — released after replay)
// GPU state (AddRef'd COM objects - released after replay)
vb: ?*anyopaque = null,
vb_offset: u32 = 0,
vb_stride: u32 = 0,
@@ -136,7 +136,7 @@ const CachedDraw = struct {
// Per-model outline info
color: u32 = 0,
category: types.ModelCategory = .none,
// VS constants (bone matrices, world/view/proj) — copied by value
// VS constants (bone matrices, world/view/proj) - copied by value
vs_consts: [MAX_VS_CONST_REGS][4]f32 = undefined,
};
@@ -197,8 +197,7 @@ fn deviceSetPtrOrNull(dev: *anyopaque, idx: usize, ptr: ?*anyopaque) void {
:
: [self] "r" (@intFromPtr(dev)),
[func] "r" (func_addr),
: .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true }
);
: .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true });
}
}
@@ -251,8 +250,7 @@ fn deviceSetTexture(dev: *anyopaque, stage: u32, tex: ?*anyopaque) void {
: [self] "r" (@intFromPtr(dev)),
[stage] "r" (stage),
[func] "r" (func_addr),
: .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true }
);
: .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true });
}
}
@@ -302,8 +300,7 @@ fn deviceSetStreamSource(dev: *anyopaque, stream: u32, vb: ?*anyopaque, offset:
: [self] "r" (@intFromPtr(dev)),
[a] "r" (&args),
[func] "r" (func_addr),
: .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true }
);
: .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true });
}
}
@@ -337,8 +334,7 @@ fn deviceSetIndices(dev: *anyopaque, ib: ?*anyopaque) void {
:
: [self] "r" (@intFromPtr(dev)),
[func] "r" (func_addr),
: .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true }
);
: .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true });
}
}
@@ -397,7 +393,7 @@ fn argbToFloat4(argb: u32) [4]f32 {
// Terrain depth snapshot
// =============================================================================
// (Terrain DS snapshot removed — DXVK does not support StretchRect for
// (Terrain DS snapshot removed - DXVK does not support StretchRect for
// depth-stencil surfaces. Terrain occlusion is achieved via stencil marks
// written during the DIP hook, when the game's DS already has terrain depth.)
@@ -420,38 +416,55 @@ fn ensureResources(device: *anyopaque) void {
resource_height = vp.Height;
// Silhouette RT (A8R8G8B8)
if (deviceCreateTexture(device, vp.Width, vp.Height, 1,
types.D3DUSAGE_RENDERTARGET, types.D3DFMT_A8R8G8B8, types.D3DPOOL_DEFAULT,
&rt_silhouette_tex) < 0) { releaseResources(); return; }
if (deviceCreateTexture(device, vp.Width, vp.Height, 1, types.D3DUSAGE_RENDERTARGET, types.D3DFMT_A8R8G8B8, types.D3DPOOL_DEFAULT, &rt_silhouette_tex) < 0) {
releaseResources();
return;
}
rt_silhouette_surf = textureGetSurfaceLevel(rt_silhouette_tex.?);
if (rt_silhouette_surf == null) { releaseResources(); return; }
if (rt_silhouette_surf == null) {
releaseResources();
return;
}
// JFA A RT (G16R16F)
if (deviceCreateTexture(device, vp.Width, vp.Height, 1,
types.D3DUSAGE_RENDERTARGET, types.D3DFMT_G16R16F, types.D3DPOOL_DEFAULT,
&rt_jfa_a_tex) < 0) { releaseResources(); return; }
if (deviceCreateTexture(device, vp.Width, vp.Height, 1, types.D3DUSAGE_RENDERTARGET, types.D3DFMT_G16R16F, types.D3DPOOL_DEFAULT, &rt_jfa_a_tex) < 0) {
releaseResources();
return;
}
rt_jfa_a_surf = textureGetSurfaceLevel(rt_jfa_a_tex.?);
if (rt_jfa_a_surf == null) { releaseResources(); return; }
if (rt_jfa_a_surf == null) {
releaseResources();
return;
}
// JFA B RT (G16R16F)
if (deviceCreateTexture(device, vp.Width, vp.Height, 1,
types.D3DUSAGE_RENDERTARGET, types.D3DFMT_G16R16F, types.D3DPOOL_DEFAULT,
&rt_jfa_b_tex) < 0) { releaseResources(); return; }
if (deviceCreateTexture(device, vp.Width, vp.Height, 1, types.D3DUSAGE_RENDERTARGET, types.D3DFMT_G16R16F, types.D3DPOOL_DEFAULT, &rt_jfa_b_tex) < 0) {
releaseResources();
return;
}
rt_jfa_b_surf = textureGetSurfaceLevel(rt_jfa_b_tex.?);
if (rt_jfa_b_surf == null) { releaseResources(); return; }
if (rt_jfa_b_surf == null) {
releaseResources();
return;
}
}
fn releaseResources() void {
inline for (.{
&rt_silhouette_surf, &rt_jfa_a_surf, &rt_jfa_b_surf,
}) |surf_ptr| {
if (surf_ptr.*) |s| { comRelease(s); surf_ptr.* = null; }
if (surf_ptr.*) |s| {
comRelease(s);
surf_ptr.* = null;
}
}
inline for (.{
&rt_silhouette_tex, &rt_jfa_a_tex, &rt_jfa_b_tex,
}) |tex_ptr| {
if (tex_ptr.*) |t| { comRelease(t); tex_ptr.* = null; }
if (tex_ptr.*) |t| {
comRelease(t);
tex_ptr.* = null;
}
}
resource_width = 0;
resource_height = 0;
@@ -461,7 +474,7 @@ fn releaseResources() void {
// Shader source strings
// =============================================================================
/// Flat colour pixel shader — outputs PS constant c0.
/// Flat colour pixel shader - outputs PS constant c0.
const ps_flat_src = "ps_3_0\nmov oC0, c0\n";
/// JFA init: sample silhouette, output own UV as seed or sentinel (-1,-1).
@@ -491,7 +504,7 @@ const jfa_prop_src =
"def c9, 1.0, 1.0, 0.0, 0.0\n" ++
"dcl_2d s0\n" ++
"dcl_texcoord0 v0\n" ++
// Self sample — initialize best seed and distance
// Self sample - initialize best seed and distance
"texld r0, v0, s0\n" ++
"sub r2.xy, v0.xy, r0.xy\n" ++
"dp2add r9.x, r2, r2, c1.x\n" ++ // best dist²
@@ -672,7 +685,10 @@ fn assemblePS(device: *anyopaque, assemble: D3DXAssembleShaderFn, src: [*]const
fn releaseShaders() void {
inline for (.{ &outline_ps, &jfa_init_ps, &jfa_prop_ps, &jfa_decode_ps, &debug_sil_ps }) |ps| {
if (ps.*) |p| { comRelease(p); ps.* = null; }
if (ps.*) |p| {
comRelease(p);
ps.* = null;
}
}
shaders_attempted = false;
}
@@ -731,7 +747,7 @@ fn hkEndScene(device: *anyopaque) callconv(sc) i32 {
}
// =============================================================================
// Reset hook — force D24S8 depth/stencil format, release resources
// Reset hook - force D24S8 depth/stencil format, release resources
// =============================================================================
fn hkReset(device: *anyopaque, pp: *types.D3DPRESENT_PARAMETERS) callconv(sc) i32 {
@@ -754,7 +770,7 @@ fn hkReset(device: *anyopaque, pp: *types.D3DPRESENT_PARAMETERS) callconv(sc) i3
}
// =============================================================================
// DrawIndexedPrimitive hook — cache outline draws for EndScene replay
// DrawIndexedPrimitive hook - cache outline draws for EndScene replay
// =============================================================================
// When an outline target is being drawn, we cache the draw parameters and
// current GPU state (VB, IB, vertex decl, VS, VS constants) so they can be
@@ -808,7 +824,7 @@ fn hkDIP(
// Capture current GPU state (AddRef COM objects to keep them alive)
deviceGetStreamSource(device, 0, &draw.vb, &draw.vb_offset, &draw.vb_stride);
// GetStreamSource AddRef's the VB — we keep the ref until replay
// GetStreamSource AddRef's the VB - we keep the ref until replay
draw.ib = deviceGetIndices(device);
// GetIndices AddRef's the IB
draw.vertex_decl = deviceGetPtr(device, types.VT.GetVertexDeclaration);
@@ -832,7 +848,7 @@ fn hkDIP(
const s_enable = deviceGetRS(device, types.D3DRS.STENCILENABLE);
const s_func = deviceGetRS(device, types.D3DRS.STENCILFUNC);
const s_ref = deviceGetRS(device, types.D3DRS.STENCILREF);
// (STENCILWRITEMASK not saved — intentionally set to 0 on restore)
// (STENCILWRITEMASK not saved - intentionally set to 0 on restore)
const s_pass = deviceGetRS(device, types.D3DRS.STENCILPASS);
const s_fail = deviceGetRS(device, types.D3DRS.STENCILFAIL);
const s_zfail = deviceGetRS(device, types.D3DRS.STENCILZFAIL);
@@ -856,7 +872,7 @@ fn hkDIP(
deviceSetRS(device, types.D3DRS.STENCILPASS, s_pass);
deviceSetRS(device, types.D3DRS.STENCILFAIL, s_fail);
deviceSetRS(device, types.D3DRS.STENCILZFAIL, s_zfail);
// Write mask 0 instead of restoring original — prevents any
// Write mask 0 instead of restoring original - prevents any
// subsequent DIP from overwriting our stencil=1 marks.
// Restored properly in EndScene before the JFA pipeline.
deviceSetRS(device, types.D3DRS.STENCILWRITEMASK, 0);
@@ -871,10 +887,22 @@ fn hkDIP(
fn clearCachedDraws() void {
for (0..cached_draw_count) |i| {
var draw = &cached_draws[i];
if (draw.vb) |obj| { comRelease(obj); draw.vb = null; }
if (draw.ib) |obj| { comRelease(obj); draw.ib = null; }
if (draw.vertex_decl) |obj| { comRelease(obj); draw.vertex_decl = null; }
if (draw.vertex_shader) |obj| { comRelease(obj); draw.vertex_shader = null; }
if (draw.vb) |obj| {
comRelease(obj);
draw.vb = null;
}
if (draw.ib) |obj| {
comRelease(obj);
draw.ib = null;
}
if (draw.vertex_decl) |obj| {
comRelease(obj);
draw.vertex_decl = null;
}
if (draw.vertex_shader) |obj| {
comRelease(obj);
draw.vertex_shader = null;
}
}
cached_draw_count = 0;
}
@@ -898,7 +926,7 @@ fn runJfaPipeline(device: *anyopaque) void {
// Save ALL state that replay + JFA will modify (manual, no state blocks)
// =====================================================================
// COM objects (Get* AddRefs — must Release after restore)
// COM objects (Get* AddRefs - must Release after restore)
const saved_rt0 = deviceGetRenderTarget(device, 0);
const saved_ps = deviceGetPtr(device, types.VT.GetPixelShader);
const saved_vs = deviceGetPtr(device, types.VT.GetVertexShader);
@@ -969,7 +997,7 @@ fn runJfaPipeline(device: *anyopaque) void {
deviceSetRenderTarget(device, 0, rt_silhouette_surf.?);
clearRenderTarget(device, 0x00000000);
// Keep game's DS bound — it has stencil marks from DIP hook where
// Keep game's DS bound - it has stencil marks from DIP hook where
// outline targets passed the terrain depth test (stencil=1 = visible).
// Don't write depth or stencil during replay.
deviceSetRS(device, types.D3DRS.ZWRITEENABLE, 0);
@@ -1006,8 +1034,7 @@ fn runJfaPipeline(device: *anyopaque) void {
deviceSetRS(device, types.D3DRS.STENCILPASS, types.D3DSTENCILOP_KEEP);
}
_ = origFn(device, draw.prim_type, draw.base_vtx, draw.min_vtx,
draw.num_verts, draw.start_idx, draw.prim_count);
_ = origFn(device, draw.prim_type, draw.base_vtx, draw.min_vtx, draw.num_verts, draw.start_idx, draw.prim_count);
}
clearCachedDraws();
@@ -1048,89 +1075,88 @@ fn runJfaPipeline(device: *anyopaque) void {
const quad = buildFullscreenQuad(vp.Width, vp.Height);
deviceDrawPrimitiveUP(device, types.D3DPT_TRIANGLESTRIP, 2, @ptrCast(&quad), @sizeOf(QuadVertex));
}
// Skip JFA — jump straight to state restore
// Skip JFA - jump straight to state restore
} else {
// =====================================================================
// Phase 2: JFA pipeline (silhouette → outline composite)
// =====================================================================
// =====================================================================
// Phase 2: JFA pipeline (silhouette → outline composite)
// =====================================================================
deviceSetPtrOrNull(device, types.VT.SetDepthStencilSurface, null);
deviceSetPtrOrNull(device, types.VT.SetVertexShader, null);
deviceSetFVF(device, types.D3DFVF_XYZRHW | types.D3DFVF_TEX1);
deviceSetRS(device, types.D3DRS.ZENABLE, types.D3DZB_FALSE);
deviceSetRS(device, types.D3DRS.ZWRITEENABLE, 0);
deviceSetRS(device, types.D3DRS.ALPHABLENDENABLE, 0);
deviceSetRS(device, types.D3DRS.CULLMODE, types.D3DCULL_NONE);
deviceSetRS(device, types.D3DRS.ALPHATESTENABLE, 0);
deviceSetRS(device, types.D3DRS.COLORWRITEENABLE, 0x0F);
deviceSetPtrOrNull(device, types.VT.SetDepthStencilSurface, null);
deviceSetPtrOrNull(device, types.VT.SetVertexShader, null);
deviceSetFVF(device, types.D3DFVF_XYZRHW | types.D3DFVF_TEX1);
deviceSetRS(device, types.D3DRS.ZENABLE, types.D3DZB_FALSE);
deviceSetRS(device, types.D3DRS.ZWRITEENABLE, 0);
deviceSetRS(device, types.D3DRS.ALPHABLENDENABLE, 0);
deviceSetRS(device, types.D3DRS.CULLMODE, types.D3DCULL_NONE);
deviceSetRS(device, types.D3DRS.ALPHATESTENABLE, 0);
deviceSetRS(device, types.D3DRS.COLORWRITEENABLE, 0x0F);
deviceSetSamplerState(device, 0, types.D3DSAMP.ADDRESSU, types.D3DTADDRESS_CLAMP);
deviceSetSamplerState(device, 0, types.D3DSAMP.ADDRESSV, types.D3DTADDRESS_CLAMP);
deviceSetSamplerState(device, 0, types.D3DSAMP.MAGFILTER, types.D3DTEXF_POINT);
deviceSetSamplerState(device, 0, types.D3DSAMP.MINFILTER, types.D3DTEXF_POINT);
deviceSetSamplerState(device, 0, types.D3DSAMP.MIPFILTER, types.D3DTEXF_NONE);
deviceSetSamplerState(device, 1, types.D3DSAMP.ADDRESSU, types.D3DTADDRESS_CLAMP);
deviceSetSamplerState(device, 1, types.D3DSAMP.ADDRESSV, types.D3DTADDRESS_CLAMP);
deviceSetSamplerState(device, 1, types.D3DSAMP.MAGFILTER, types.D3DTEXF_POINT);
deviceSetSamplerState(device, 1, types.D3DSAMP.MINFILTER, types.D3DTEXF_POINT);
deviceSetSamplerState(device, 1, types.D3DSAMP.MIPFILTER, types.D3DTEXF_NONE);
deviceSetSamplerState(device, 0, types.D3DSAMP.ADDRESSU, types.D3DTADDRESS_CLAMP);
deviceSetSamplerState(device, 0, types.D3DSAMP.ADDRESSV, types.D3DTADDRESS_CLAMP);
deviceSetSamplerState(device, 0, types.D3DSAMP.MAGFILTER, types.D3DTEXF_POINT);
deviceSetSamplerState(device, 0, types.D3DSAMP.MINFILTER, types.D3DTEXF_POINT);
deviceSetSamplerState(device, 0, types.D3DSAMP.MIPFILTER, types.D3DTEXF_NONE);
deviceSetSamplerState(device, 1, types.D3DSAMP.ADDRESSU, types.D3DTADDRESS_CLAMP);
deviceSetSamplerState(device, 1, types.D3DSAMP.ADDRESSV, types.D3DTADDRESS_CLAMP);
deviceSetSamplerState(device, 1, types.D3DSAMP.MAGFILTER, types.D3DTEXF_POINT);
deviceSetSamplerState(device, 1, types.D3DSAMP.MINFILTER, types.D3DTEXF_POINT);
deviceSetSamplerState(device, 1, types.D3DSAMP.MIPFILTER, types.D3DTEXF_NONE);
const quad = buildFullscreenQuad(vp.Width, vp.Height);
const qstride: u32 = @sizeOf(QuadVertex);
const fw = @as(f32, @floatFromInt(@max(vp.Width, 1)));
const fh = @as(f32, @floatFromInt(@max(vp.Height, 1)));
const quad = buildFullscreenQuad(vp.Width, vp.Height);
const qstride: u32 = @sizeOf(QuadVertex);
const fw = @as(f32, @floatFromInt(@max(vp.Width, 1)));
const fh = @as(f32, @floatFromInt(@max(vp.Height, 1)));
// Pass 1: JFA Init (silhouette → JFA_A)
deviceSetRenderTarget(device, 0, rt_jfa_a_surf.?);
deviceSetTexture(device, 0, rt_silhouette_tex);
deviceSetPtr(device, types.VT.SetPixelShader, jfa_init_ps.?);
deviceDrawPrimitiveUP(device, types.D3DPT_TRIANGLESTRIP, 2, @ptrCast(&quad), qstride);
// Pass 1: JFA Init (silhouette → JFA_A)
deviceSetRenderTarget(device, 0, rt_jfa_a_surf.?);
deviceSetTexture(device, 0, rt_silhouette_tex);
deviceSetPtr(device, types.VT.SetPixelShader, jfa_init_ps.?);
deviceDrawPrimitiveUP(device, types.D3DPT_TRIANGLESTRIP, 2, @ptrCast(&quad), qstride);
// JFA Propagation: steps [8, 4, 2, 1] ping-ponging between A and B.
deviceSetPtr(device, types.VT.SetPixelShader, jfa_prop_ps.?);
var c0: [4]f32 = undefined;
// JFA Propagation: steps [8, 4, 2, 1] ping-ponging between A and B.
deviceSetPtr(device, types.VT.SetPixelShader, jfa_prop_ps.?);
var c0: [4]f32 = undefined;
// step=8 (JFA_A → JFA_B)
deviceSetRenderTarget(device, 0, rt_jfa_b_surf.?);
deviceSetTexture(device, 0, rt_jfa_a_tex);
c0 = .{ 8.0 / fw, 8.0 / fh, 0.0, 0.0 };
deviceSetPSConstF(device, 0, &c0);
deviceDrawPrimitiveUP(device, types.D3DPT_TRIANGLESTRIP, 2, @ptrCast(&quad), qstride);
// step=8 (JFA_A → JFA_B)
deviceSetRenderTarget(device, 0, rt_jfa_b_surf.?);
deviceSetTexture(device, 0, rt_jfa_a_tex);
c0 = .{ 8.0 / fw, 8.0 / fh, 0.0, 0.0 };
deviceSetPSConstF(device, 0, &c0);
deviceDrawPrimitiveUP(device, types.D3DPT_TRIANGLESTRIP, 2, @ptrCast(&quad), qstride);
// step=4 (JFA_B → JFA_A)
deviceSetRenderTarget(device, 0, rt_jfa_a_surf.?);
deviceSetTexture(device, 0, rt_jfa_b_tex);
c0 = .{ 4.0 / fw, 4.0 / fh, 0.0, 0.0 };
deviceSetPSConstF(device, 0, &c0);
deviceDrawPrimitiveUP(device, types.D3DPT_TRIANGLESTRIP, 2, @ptrCast(&quad), qstride);
// step=4 (JFA_B → JFA_A)
deviceSetRenderTarget(device, 0, rt_jfa_a_surf.?);
deviceSetTexture(device, 0, rt_jfa_b_tex);
c0 = .{ 4.0 / fw, 4.0 / fh, 0.0, 0.0 };
deviceSetPSConstF(device, 0, &c0);
deviceDrawPrimitiveUP(device, types.D3DPT_TRIANGLESTRIP, 2, @ptrCast(&quad), qstride);
// step=2 (JFA_A → JFA_B)
deviceSetRenderTarget(device, 0, rt_jfa_b_surf.?);
deviceSetTexture(device, 0, rt_jfa_a_tex);
c0 = .{ 2.0 / fw, 2.0 / fh, 0.0, 0.0 };
deviceSetPSConstF(device, 0, &c0);
deviceDrawPrimitiveUP(device, types.D3DPT_TRIANGLESTRIP, 2, @ptrCast(&quad), qstride);
// step=2 (JFA_A → JFA_B)
deviceSetRenderTarget(device, 0, rt_jfa_b_surf.?);
deviceSetTexture(device, 0, rt_jfa_a_tex);
c0 = .{ 2.0 / fw, 2.0 / fh, 0.0, 0.0 };
deviceSetPSConstF(device, 0, &c0);
deviceDrawPrimitiveUP(device, types.D3DPT_TRIANGLESTRIP, 2, @ptrCast(&quad), qstride);
// step=1 (JFA_B → JFA_A)
deviceSetRenderTarget(device, 0, rt_jfa_a_surf.?);
deviceSetTexture(device, 0, rt_jfa_b_tex);
c0 = .{ 1.0 / fw, 1.0 / fh, 0.0, 0.0 };
deviceSetPSConstF(device, 0, &c0);
deviceDrawPrimitiveUP(device, types.D3DPT_TRIANGLESTRIP, 2, @ptrCast(&quad), qstride);
// Pass 4: Decode + Composite (JFA_A + silhouette → backbuffer)
if (saved_rt0) |rt| deviceSetRenderTarget(device, 0, rt);
deviceSetTexture(device, 0, rt_jfa_a_tex);
deviceSetTexture(device, 1, rt_silhouette_tex);
c0 = [4]f32{ fw, fh, 4.0, 0.0 };
deviceSetPSConstF(device, 0, &c0);
deviceSetPtr(device, types.VT.SetPixelShader, jfa_decode_ps.?);
deviceSetRS(device, types.D3DRS.ALPHABLENDENABLE, 1);
deviceSetRS(device, types.D3DRS.SRCBLEND, types.D3DBLEND_SRCALPHA);
deviceSetRS(device, types.D3DRS.DESTBLEND, types.D3DBLEND_INVSRCALPHA);
deviceDrawPrimitiveUP(device, types.D3DPT_TRIANGLESTRIP, 2, @ptrCast(&quad), qstride);
// step=1 (JFA_B → JFA_A)
deviceSetRenderTarget(device, 0, rt_jfa_a_surf.?);
deviceSetTexture(device, 0, rt_jfa_b_tex);
c0 = .{ 1.0 / fw, 1.0 / fh, 0.0, 0.0 };
deviceSetPSConstF(device, 0, &c0);
deviceDrawPrimitiveUP(device, types.D3DPT_TRIANGLESTRIP, 2, @ptrCast(&quad), qstride);
// Pass 4: Decode + Composite (JFA_A + silhouette → backbuffer)
if (saved_rt0) |rt| deviceSetRenderTarget(device, 0, rt);
deviceSetTexture(device, 0, rt_jfa_a_tex);
deviceSetTexture(device, 1, rt_silhouette_tex);
c0 = [4]f32{ fw, fh, 4.0, 0.0 };
deviceSetPSConstF(device, 0, &c0);
deviceSetPtr(device, types.VT.SetPixelShader, jfa_decode_ps.?);
deviceSetRS(device, types.D3DRS.ALPHABLENDENABLE, 1);
deviceSetRS(device, types.D3DRS.SRCBLEND, types.D3DBLEND_SRCALPHA);
deviceSetRS(device, types.D3DRS.DESTBLEND, types.D3DBLEND_INVSRCALPHA);
deviceDrawPrimitiveUP(device, types.D3DPT_TRIANGLESTRIP, 2, @ptrCast(&quad), qstride);
} // end else (normal JFA path)
// =====================================================================
@@ -1174,7 +1200,10 @@ fn runJfaPipeline(device: *anyopaque) void {
deviceSetVSConstF(device, 0, &saved_vs_consts, MAX_VS_CONST_REGS);
// COM objects (restore binding then release our ref)
if (saved_rt0) |rt| { deviceSetRenderTarget(device, 0, rt); comRelease(rt); } // RT0
if (saved_rt0) |rt| {
deviceSetRenderTarget(device, 0, rt);
comRelease(rt);
} // RT0
deviceSetPtrOrNull(device, types.VT.SetDepthStencilSurface, saved_ds); // DS
if (saved_ds) |ds| comRelease(ds);
deviceSetPtrOrNull(device, types.VT.SetPixelShader, saved_ps); // PS