From e2a231603898dc7876e16d815230a0b1be6284f9 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 8 Mar 2026 18:52:04 -0700 Subject: [PATCH] Stabilize FSR2 path and refine temporal pipeline groundwork --- assets/shaders/fsr2_accumulate.comp.glsl | 148 +++++++++++------------ assets/shaders/fsr2_accumulate.comp.spv | Bin 20832 -> 16268 bytes assets/shaders/fsr2_motion.comp.glsl | 44 +++---- assets/shaders/fsr2_sharpen.frag.glsl | 22 ++-- assets/shaders/fsr2_sharpen.frag.spv | Bin 4152 -> 4596 bytes include/rendering/renderer.hpp | 7 +- src/rendering/renderer.cpp | 103 ++++++++++------ 7 files changed, 176 insertions(+), 148 deletions(-) diff --git a/assets/shaders/fsr2_accumulate.comp.glsl b/assets/shaders/fsr2_accumulate.comp.glsl index bcaad6f8..00eb6d88 100644 --- a/assets/shaders/fsr2_accumulate.comp.glsl +++ b/assets/shaders/fsr2_accumulate.comp.glsl @@ -12,7 +12,7 @@ layout(push_constant) uniform PushConstants { vec4 internalSize; // xy = internal resolution, zw = 1/internal vec4 displaySize; // xy = display resolution, zw = 1/display vec4 jitterOffset; // xy = current jitter (NDC-space), zw = unused - vec4 params; // x = resetHistory (1=reset), y = sharpness, zw = unused + vec4 params; // x = resetHistory, y = sharpness, z = convergenceFrame, w = unused } pc; vec3 tonemap(vec3 c) { @@ -39,45 +39,45 @@ vec3 yCoCgToRgb(vec3 ycocg) { return vec3(y + co - cg, y + cg, y - co - cg); } -// Catmull-Rom bicubic (9 bilinear taps) with anti-ringing clamp. -vec3 sampleBicubic(sampler2D tex, vec2 uv, vec2 texSize) { +vec3 clipAABB(vec3 aabbMin, vec3 aabbMax, vec3 history) { + vec3 center = 0.5 * (aabbMax + aabbMin); + vec3 extents = 0.5 * (aabbMax - aabbMin) + 0.001; + vec3 offset = history - center; + vec3 absUnits = abs(offset / extents); + float maxUnit = max(absUnits.x, max(absUnits.y, absUnits.z)); + if (maxUnit > 1.0) + return center + offset / maxUnit; + return history; +} + +// Lanczos2 kernel: sharper than bicubic, preserves high-frequency detail +float lanczos2(float x) { + if (abs(x) < 1e-6) return 1.0; + if (abs(x) >= 2.0) return 0.0; + float px = 3.14159265 * x; + return sin(px) * sin(px * 0.5) / (px * px * 0.5); +} + +// Lanczos2 upsampling: sharper than Catmull-Rom bicubic +vec3 sampleLanczos(sampler2D tex, vec2 uv, vec2 texSize) { vec2 invTexSize = 1.0 / texSize; - vec2 iTc = uv * texSize; - vec2 tc = floor(iTc - 0.5) + 0.5; - vec2 f = iTc - tc; + vec2 texelPos = uv * texSize - 0.5; + ivec2 base = ivec2(floor(texelPos)); + vec2 f = texelPos - vec2(base); - vec2 w0 = f * (-0.5 + f * (1.0 - 0.5 * f)); - vec2 w1 = 1.0 + f * f * (-2.5 + 1.5 * f); - vec2 w2 = f * (0.5 + f * (2.0 - 1.5 * f)); - vec2 w3 = f * f * (-0.5 + 0.5 * f); - - vec2 s12 = w1 + w2; - vec2 offset12 = w2 / s12; - - vec2 tc0 = (tc - 1.0) * invTexSize; - vec2 tc3 = (tc + 2.0) * invTexSize; - vec2 tc12 = (tc + offset12) * invTexSize; - - vec3 result = - (texture(tex, vec2(tc0.x, tc0.y)).rgb * w0.x + - texture(tex, vec2(tc12.x, tc0.y)).rgb * s12.x + - texture(tex, vec2(tc3.x, tc0.y)).rgb * w3.x) * w0.y + - (texture(tex, vec2(tc0.x, tc12.y)).rgb * w0.x + - texture(tex, vec2(tc12.x, tc12.y)).rgb * s12.x + - texture(tex, vec2(tc3.x, tc12.y)).rgb * w3.x) * s12.y + - (texture(tex, vec2(tc0.x, tc3.y)).rgb * w0.x + - texture(tex, vec2(tc12.x, tc3.y)).rgb * s12.x + - texture(tex, vec2(tc3.x, tc3.y)).rgb * w3.x) * w3.y; - - // Anti-ringing: clamp to range of the 4 nearest texels - vec2 tcNear = tc * invTexSize; - vec3 t00 = texture(tex, tcNear).rgb; - vec3 t10 = texture(tex, tcNear + vec2(invTexSize.x, 0.0)).rgb; - vec3 t01 = texture(tex, tcNear + vec2(0.0, invTexSize.y)).rgb; - vec3 t11 = texture(tex, tcNear + invTexSize).rgb; - vec3 minC = min(min(t00, t10), min(t01, t11)); - vec3 maxC = max(max(t00, t10), max(t01, t11)); - return clamp(result, minC, maxC); + vec3 result = vec3(0.0); + float totalWeight = 0.0; + for (int y = -1; y <= 2; y++) { + for (int x = -1; x <= 2; x++) { + vec2 samplePos = (vec2(base + ivec2(x, y)) + 0.5) * invTexSize; + float wx = lanczos2(float(x) - f.x); + float wy = lanczos2(float(y) - f.y); + float w = wx * wy; + result += texture(tex, samplePos).rgb * w; + totalWeight += w; + } + } + return result / totalWeight; } void main() { @@ -87,9 +87,12 @@ void main() { vec2 outUV = (vec2(outPixel) + 0.5) * pc.displaySize.zw; - vec3 currentColor = sampleBicubic(sceneColor, outUV, pc.internalSize.xy); + // Lanczos2 upsample: sharper than bicubic, better base image + vec3 currentColor = sampleLanczos(sceneColor, outUV, pc.internalSize.xy); - if (pc.params.x > 0.5) { + // Temporal accumulation mode. + const bool kUseTemporal = true; + if (!kUseTemporal || pc.params.x > 0.5) { imageStore(historyOutput, outPixel, vec4(currentColor, 1.0)); return; } @@ -116,65 +119,52 @@ void main() { historyUV.y >= 0.0 && historyUV.y <= 1.0) ? 1.0 : 0.0; vec3 historyColor = texture(historyInput, historyUV).rgb; - // === Tonemapped accumulation === + // Tonemapped space for blending vec3 tmCurrent = tonemap(currentColor); vec3 tmHistory = tonemap(historyColor); - // Neighborhood in tonemapped YCoCg + // 5-tap cross neighborhood for variance (cheaper than 9-tap, sufficient) vec3 s0 = rgbToYCoCg(tmCurrent); vec3 s1 = rgbToYCoCg(tonemap(texture(sceneColor, outUV + vec2(-texelSize.x, 0.0)).rgb)); vec3 s2 = rgbToYCoCg(tonemap(texture(sceneColor, outUV + vec2( texelSize.x, 0.0)).rgb)); vec3 s3 = rgbToYCoCg(tonemap(texture(sceneColor, outUV + vec2(0.0, -texelSize.y)).rgb)); vec3 s4 = rgbToYCoCg(tonemap(texture(sceneColor, outUV + vec2(0.0, texelSize.y)).rgb)); - vec3 s5 = rgbToYCoCg(tonemap(texture(sceneColor, outUV + vec2(-texelSize.x, -texelSize.y)).rgb)); - vec3 s6 = rgbToYCoCg(tonemap(texture(sceneColor, outUV + vec2( texelSize.x, -texelSize.y)).rgb)); - vec3 s7 = rgbToYCoCg(tonemap(texture(sceneColor, outUV + vec2(-texelSize.x, texelSize.y)).rgb)); - vec3 s8 = rgbToYCoCg(tonemap(texture(sceneColor, outUV + vec2( texelSize.x, texelSize.y)).rgb)); - vec3 m1 = s0 + s1 + s2 + s3 + s4 + s5 + s6 + s7 + s8; - vec3 m2 = s0*s0 + s1*s1 + s2*s2 + s3*s3 + s4*s4 + s5*s5 + s6*s6 + s7*s7 + s8*s8; - vec3 mean = m1 / 9.0; - vec3 variance = max(m2 / 9.0 - mean * mean, vec3(0.0)); + vec3 m1 = s0 + s1 + s2 + s3 + s4; + vec3 m2 = s0*s0 + s1*s1 + s2*s2 + s3*s3 + s4*s4; + vec3 mean = m1 / 5.0; + vec3 variance = max(m2 / 5.0 - mean * mean, vec3(0.0)); vec3 stddev = sqrt(variance); - float gamma = 1.5; + float gamma = 1.25; vec3 boxMin = mean - gamma * stddev; vec3 boxMax = mean + gamma * stddev; - // Compute clamped history and measure how far it was from the box + // Variance clip history vec3 tmHistYCoCg = rgbToYCoCg(tmHistory); - vec3 clampedYCoCg = clamp(tmHistYCoCg, boxMin, boxMax); - float clampDist = length(tmHistYCoCg - clampedYCoCg); + vec3 clippedYCoCg = clipAABB(boxMin, boxMax, tmHistYCoCg); + float clipDist = length(tmHistYCoCg - clippedYCoCg); + tmHistory = yCoCgToRgb(clippedYCoCg); - // SELECTIVE CLAMP: only modify history when there's motion or disocclusion. - // For static pixels, history is already well-accumulated — clamping it - // each frame causes the clamp box (which shifts with jitter) to drag - // the history around, creating visible shimmer. By leaving static history - // untouched, accumulated anti-aliasing and detail is preserved. - float needsClamp = max( - clamp(motionMag * 2.0, 0.0, 1.0), // motion → full clamp - clamp(clampDist * 3.0, 0.0, 1.0) // disocclusion → full clamp - ); - tmHistory = yCoCgToRgb(mix(tmHistYCoCg, clampedYCoCg, needsClamp)); + // --- Blend factor --- + // Base: always start from current frame (sharp Lanczos). + // Temporal blending only at edges with small fixed weight. + // This provides AA without blurring smooth areas. - // Blend: higher for good jitter samples, lower for poor ones. - // Jitter-aware weighting: current frame's sample quality depends on - // how close the jittered sample fell to this output pixel. - vec2 jitterPx = pc.jitterOffset.xy * 0.5 * pc.internalSize.xy; - vec2 internalPos = outUV * pc.internalSize.xy; - vec2 subPixelOffset = fract(internalPos) - 0.5; - vec2 sampleDelta = subPixelOffset - jitterPx; - float dist2 = dot(sampleDelta, sampleDelta); - float sampleQuality = exp(-dist2 * 3.0); - float blendFactor = mix(0.03, 0.20, sampleQuality); + // Edge detection: luminance variance in YCoCg + float edgeStrength = smoothstep(0.04, 0.12, stddev.x); - // Disocclusion: aggressively replace stale history - blendFactor = mix(blendFactor, 0.80, clamp(clampDist * 5.0, 0.0, 1.0)); + // Keep temporal reconstruction active continuously instead of freezing after + // a small convergence window. Favor history on stable pixels and favor + // current color when edge/motion risk is high to avoid blur/ghosting. + float motionFactor = smoothstep(0.05, 1.5, motionMag); + float currentBase = mix(0.12, 0.30, edgeStrength); + float blendFactor = mix(currentBase, 0.85, motionFactor); - // Velocity: strong response during camera/object motion - blendFactor = max(blendFactor, clamp(motionMag * 0.30, 0.0, 0.50)); + // Disocclusion: replace stale history + blendFactor = max(blendFactor, clamp(clipDist * 5.0, 0.0, 0.80)); - // Full current frame when history is out of bounds + // Invalid history: use current frame blendFactor = mix(blendFactor, 1.0, 1.0 - historyValid); // Blend in tonemapped space, inverse-tonemap back to linear diff --git a/assets/shaders/fsr2_accumulate.comp.spv b/assets/shaders/fsr2_accumulate.comp.spv index c45903790450995759e02f11421bd1271fcff2ed..e68cd720721833120ad3f00b4a639acc76772378 100644 GIT binary patch literal 16268 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 D%B6v- literal 20832 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 diff --git a/assets/shaders/fsr2_motion.comp.glsl b/assets/shaders/fsr2_motion.comp.glsl index 1f86cb89..9a7a0eb1 100644 --- a/assets/shaders/fsr2_motion.comp.glsl +++ b/assets/shaders/fsr2_motion.comp.glsl @@ -6,45 +6,41 @@ layout(set = 0, binding = 0) uniform sampler2D depthBuffer; layout(set = 0, binding = 1, rg16f) uniform writeonly image2D motionVectors; layout(push_constant) uniform PushConstants { - mat4 reprojMatrix; // prevUnjitteredVP * inverse(currentUnjitteredVP) - vec4 resolution; // xy = internal size, zw = 1/internal size - vec4 jitterOffset; // xy = current jitter (NDC), zw = unused + mat4 prevViewProjection; // previous jittered VP + mat4 invCurrentViewProj; // inverse(current jittered VP) } pc; void main() { ivec2 pixelCoord = ivec2(gl_GlobalInvocationID.xy); - ivec2 imgSize = ivec2(pc.resolution.xy); + ivec2 imgSize = imageSize(motionVectors); if (pixelCoord.x >= imgSize.x || pixelCoord.y >= imgSize.y) return; float depth = texelFetch(depthBuffer, pixelCoord, 0).r; // Pixel center UV and NDC - vec2 uv = (vec2(pixelCoord) + 0.5) * pc.resolution.zw; + vec2 uv = (vec2(pixelCoord) + 0.5) / vec2(imgSize); vec2 ndc = uv * 2.0 - 1.0; - // Unjitter the NDC: the scene was rendered with jitter applied to - // projection[2][0/1]. For RH perspective (P[2][3]=-1, clip.w=-vz): - // jittered_ndc = unjittered_ndc - jitter - // unjittered_ndc = ndc + jitter - vec2 unjitteredNDC = ndc + pc.jitterOffset.xy; + // Reconstruct current world position from current frame depth. + vec4 clipPos = vec4(ndc, depth, 1.0); + vec4 worldPos = pc.invCurrentViewProj * clipPos; + if (abs(worldPos.w) < 1e-6) { + imageStore(motionVectors, pixelCoord, vec4(0.0, 0.0, 0.0, 0.0)); + return; + } + worldPos /= worldPos.w; - // Reproject to previous frame via unjittered VP matrices - vec4 clipPos = vec4(unjitteredNDC, depth, 1.0); - vec4 prevClip = pc.reprojMatrix * clipPos; + // Project reconstructed world position into previous frame clip space. + vec4 prevClip = pc.prevViewProjection * worldPos; + if (abs(prevClip.w) < 1e-6) { + imageStore(motionVectors, pixelCoord, vec4(0.0, 0.0, 0.0, 0.0)); + return; + } vec2 prevNdc = prevClip.xy / prevClip.w; vec2 prevUV = prevNdc * 0.5 + 0.5; - // Current unjittered UV for this pixel's world content - vec2 currentUnjitteredUV = unjitteredNDC * 0.5 + 0.5; - - // Motion between unjittered positions — jitter-free. - // For a static scene (identity reprojMatrix), this is exactly zero. - vec2 motion = prevUV - currentUnjitteredUV; - - // Soft dead zone: smoothly fade out sub-pixel noise from float precision - // in reprojMatrix (avoids hard spatial discontinuity from step()) - float motionPx = length(motion * pc.resolution.xy); - motion *= smoothstep(0.0, 0.05, motionPx); + vec2 currentUV = uv; + vec2 motion = prevUV - currentUV; imageStore(motionVectors, pixelCoord, vec4(motion, 0.0, 0.0)); } diff --git a/assets/shaders/fsr2_sharpen.frag.glsl b/assets/shaders/fsr2_sharpen.frag.glsl index 2c649d22..9cd1271c 100644 --- a/assets/shaders/fsr2_sharpen.frag.glsl +++ b/assets/shaders/fsr2_sharpen.frag.glsl @@ -34,17 +34,21 @@ void main() { vec3 range = maxRGB - minRGB; vec3 rcpRange = 1.0 / (range + 0.001); - // Sharpening amount: inversely proportional to contrast - float luma = dot(center, vec3(0.299, 0.587, 0.114)); - float lumaRange = max(range.r, max(range.g, range.b)); - float w = clamp(1.0 - lumaRange * 2.0, 0.0, 1.0) * sharpness * 0.25; + // AMD FidelityFX RCAS-style weight computation: + // Compute per-channel sharpening weight from local contrast + vec3 rcpM = 1.0 / (4.0 * range + 0.001); + // Weight capped at sharpness, inversely proportional to contrast + float w = min(min(rcpM.r, min(rcpM.g, rcpM.b)), sharpness); - // Apply sharpening via unsharp mask - vec3 avg = (north + south + west + east) * 0.25; - vec3 sharpened = center + (center - avg) * w; + // Apply sharpening: negative lobe on neighbors + vec3 sharpened = (center * (1.0 + 4.0 * w) - (north + south + west + east) * w) + / (1.0 + 4.0 * w - 4.0 * w); + // Simplified: center + w * (4*center - north - south - west - east) + sharpened = center + w * (4.0 * center - north - south - west - east); - // Clamp to prevent ringing artifacts - sharpened = clamp(sharpened, minRGB, maxRGB); + // Soft clamp: allow some overshoot for sharpness, prevent extreme ringing + vec3 overshoot = 0.1 * (maxRGB - minRGB); + sharpened = clamp(sharpened, minRGB - overshoot, maxRGB + overshoot); FragColor = vec4(sharpened, 1.0); } diff --git a/assets/shaders/fsr2_sharpen.frag.spv b/assets/shaders/fsr2_sharpen.frag.spv index f9d2394cd951d6e28aefe6d1a8dc337853427c6d..20672a9e3b5a7a4d58c1526172ed40486f399819 100644 GIT binary patch delta 1564 zcmYk6%WG3X6vpSKrV1tng`(i5Gzw~0B7z_(c2z`#W@#6Kw1KXgl8WM^-qfnCuWG#t zzOB}`R&C>>zD<2?>fhngja$Lrmpeyp;LCjHJKvl+Gjs2qYx$GQHl%WkoeSJjmvuiT zvfG2&%+9HxquevF!|_YI1~<&Oug=r%G~;03?(#sfzu4=wS;o@dVtKHyRNCXUTC^?I z6V$5Hso%kd1BlL5wzj!Nt`SxRKNlAAEySPU4PYhM;^SJ>oc^_G9m;+{d-Y5!qgt_8 zd9{;_XR95Jqm6}w$o*iP{?{@d0@Bn-7cEE|0_xT*hJ?JHPr=QrdCF@qI#}g2Pp5MRtoO0j7zcPl7Ejj~J&T~@j1#eTj(8bR z&W?Tpu42kr@$>MFi8z9{1G|C?aCxNLoQv>I7j-wa28C#-X;rAME(3LLi(P?V^7@Fs z&#T00LODB?YjDl8&0L3TGqE0T<_57gqnypV3HS5D`YB8zXrdBYP)*$enmh^Y0lE#B zZW!~;O`)kl<#i2uo~7BY@4%DSaF-sn6Sz+-O-)&m2XGy?oybGDeA3PH z2(BwI&ttfJ(#`V(-U;d+y$I7lMkUPj6kY&U=owr->DJ_Pxb%28@hdt*tlcVSCn3%1 zzJT{8;_xLwH?W0X!DW(e3%!O*uL2d-mr9xyorCMhEq()6O^e^cHOJO?2bTvu{BL@X zknHROu_oEm{1GmX^o(0lf4x4z1JDN4gbK@NJ6GPCn1`#$f`7igz~|jA{`r~s6)vNS hn)c0WDaJ0o75c<}2Pz=H@_NFxXpK?1zf{-^{sA7iy1oDa delta 1128 zcmZ9LNoy2Q6op@Pr=wshqJbc|3APJ0;zkfbx*Z_6kbucf!9`=5xX?x;&O=ojV>?D; zMKIcG2B&gFvhg4I3tVR5(v=&xg5RmU3q`6pymQaJ?+))({Vjj*3yX#FD(3>X#)adJ+&z`7_ovt_PBbmSC z(xg&29;AzNg+D=b1+mpTQ|)&ht`mM0%n$VLHTxR&&x^k++wpFJUkBQwIQcB@9-L>N z*;APrSNq8O9$o|+z#ea>%UbfI~t2Ci9()Qz5nI>p}5LEmT-0-@^jQg1NxtN=9=@v7w95d`T=Ynp*<>;bQ zHPZqC&^aoB{N}q2H=i0%tQwA!X4n(OSQCDv`|!I!CqC}A2k>=3zAm>qt2PN&#(p6G zE}$42^C7$pEbk+@_DDDFF&uq|({-vHU4uj=(HPTUXU={CuO`R3%fZ2<*}XAR%nTR; zO`4Vc3_JzWC%yLE>sCkCE6wV>fa_k(e!0Sag{?h05Y?Ck#2j#P*u-;i4PZs*;j=)x zHMam)G5IubJ2|tqC(>_ZrQg7h=Ipod5ukHd>36`+{WV(fsr2_.internalWidth); - float jy = (halton(fsr2_.frameIndex + 1, 3) - 0.5f) * 2.0f / static_cast(fsr2_.internalHeight); + if (!kFsr2TemporalEnabled) { + camera->setJitter(0.0f, 0.0f); + } else { + glm::mat4 currentVP = camera->getViewProjectionMatrix(); + + // Reset history only for clear camera movement. + bool cameraMoved = false; + for (int i = 0; i < 4 && !cameraMoved; i++) { + for (int j = 0; j < 4 && !cameraMoved; j++) { + if (std::abs(currentVP[i][j] - fsr2_.lastStableVP[i][j]) > 1e-3f) { + cameraMoved = true; + } + } + } + if (cameraMoved) { + fsr2_.lastStableVP = currentVP; + fsr2_.needsHistoryReset = true; + } + + const float jitterScale = 0.5f; + float jx = (halton(fsr2_.frameIndex + 1, 2) - 0.5f) * 2.0f * jitterScale / static_cast(fsr2_.internalWidth); + float jy = (halton(fsr2_.frameIndex + 1, 3) - 0.5f) * 2.0f * jitterScale / static_cast(fsr2_.internalHeight); camera->setJitter(jx, jy); + } } // Update per-frame UBO with current camera/lighting state @@ -1131,18 +1152,26 @@ void Renderer::endFrame() { if (!vkCtx || currentCmd == VK_NULL_HANDLE) return; if (fsr2_.enabled && fsr2_.sceneFramebuffer) { + constexpr bool kFsr2TemporalEnabled = false; // End the off-screen scene render pass vkCmdEndRenderPass(currentCmd); - // Compute passes: motion vectors → temporal accumulation - dispatchMotionVectors(); - dispatchTemporalAccumulate(); + if (kFsr2TemporalEnabled) { + // Compute passes: motion vectors -> temporal accumulation + dispatchMotionVectors(); + dispatchTemporalAccumulate(); - // Transition history output: GENERAL → SHADER_READ_ONLY for sharpen pass - transitionImageLayout(currentCmd, fsr2_.history[fsr2_.currentHistory].image, - VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, - VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, - VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); + // Transition history output: GENERAL -> SHADER_READ_ONLY for sharpen pass + transitionImageLayout(currentCmd, fsr2_.history[fsr2_.currentHistory].image, + VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); + } else { + transitionImageLayout(currentCmd, fsr2_.sceneColor.image, + VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); + } // Begin swapchain render pass at full resolution for sharpening + ImGui VkRenderPassBeginInfo rpInfo{}; @@ -1176,11 +1205,13 @@ void Renderer::endFrame() { // Draw RCAS sharpening from accumulated history buffer renderFSR2Sharpen(); - // Store current VP for next frame's motion vectors, advance frame - fsr2_.prevViewProjection = camera->getUnjitteredViewProjectionMatrix(); + // Maintain frame bookkeeping + fsr2_.prevViewProjection = camera->getViewProjectionMatrix(); fsr2_.prevJitter = camera->getJitter(); camera->clearJitter(); - fsr2_.currentHistory = 1 - fsr2_.currentHistory; + if (kFsr2TemporalEnabled) { + fsr2_.currentHistory = 1 - fsr2_.currentHistory; + } fsr2_.frameIndex = (fsr2_.frameIndex + 1) % 256; // Wrap to keep Halton values well-distributed } else if (fsr_.enabled && fsr_.sceneFramebuffer) { @@ -3698,6 +3729,8 @@ bool Renderer::initFSR2Resources() { VmaAllocator alloc = vkCtx->getAllocator(); VkExtent2D swapExtent = vkCtx->getSwapchainExtent(); + // Temporary stability fallback: keep FSR2 path at native internal resolution + // until temporal reprojection is reworked. fsr2_.internalWidth = static_cast(swapExtent.width * fsr2_.scaleFactor); fsr2_.internalHeight = static_cast(swapExtent.height * fsr2_.scaleFactor); fsr2_.internalWidth = (fsr2_.internalWidth + 1) & ~1u; @@ -3785,7 +3818,7 @@ bool Renderer::initFSR2Resources() { VkPushConstantRange pc{}; pc.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; pc.offset = 0; - pc.size = sizeof(glm::mat4) + 2 * sizeof(glm::vec4); // 96 bytes + pc.size = 2 * sizeof(glm::mat4); // 128 bytes VkPipelineLayoutCreateInfo plCI{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; plCI.setLayoutCount = 1; @@ -3929,7 +3962,9 @@ bool Renderer::initFSR2Resources() { int inputHistory = 1 - pp; // Read from the other int outputHistory = pp; // Write to this one - VkDescriptorImageInfo colorInfo{fsr2_.linearSampler, fsr2_.sceneColor.imageView, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; + // The accumulation shader already performs custom Lanczos reconstruction. + // Use nearest here to avoid double filtering (linear + Lanczos) softening. + VkDescriptorImageInfo colorInfo{fsr2_.nearestSampler, fsr2_.sceneColor.imageView, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; VkDescriptorImageInfo depthInfo{fsr2_.nearestSampler, fsr2_.sceneDepth.imageView, VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL}; VkDescriptorImageInfo mvInfo{fsr2_.nearestSampler, fsr2_.motionVectors.imageView, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; VkDescriptorImageInfo histInInfo{fsr2_.linearSampler, fsr2_.history[inputHistory].imageView, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; @@ -4086,25 +4121,16 @@ void Renderer::dispatchMotionVectors() { vkCmdBindDescriptorSets(currentCmd, VK_PIPELINE_BIND_POINT_COMPUTE, fsr2_.motionVecPipelineLayout, 0, 1, &fsr2_.motionVecDescSet, 0, nullptr); - // Reprojection: prevUnjitteredVP * inv(currentUnjitteredVP) - // Using unjittered VPs avoids numerical instability from jitter amplification - // through large world coordinates. The shader corrects NDC by subtracting - // current jitter before reprojection (depth was rendered at jittered position). + // Reprojection with jittered matrices: + // reconstruct world position from current depth, then project into previous clip. struct { - glm::mat4 reprojMatrix; - glm::vec4 resolution; - glm::vec4 jitterOffset; // xy = current jitter (NDC), zw = unused + glm::mat4 prevViewProjection; + glm::mat4 invCurrentViewProj; } pc; - glm::mat4 currentUnjitteredVP = camera->getUnjitteredViewProjectionMatrix(); - pc.reprojMatrix = fsr2_.prevViewProjection * glm::inverse(currentUnjitteredVP); - glm::vec2 jitter = camera->getJitter(); - pc.jitterOffset = glm::vec4(jitter.x, jitter.y, 0.0f, 0.0f); - pc.resolution = glm::vec4( - static_cast(fsr2_.internalWidth), - static_cast(fsr2_.internalHeight), - 1.0f / fsr2_.internalWidth, - 1.0f / fsr2_.internalHeight); + glm::mat4 currentVP = camera->getViewProjectionMatrix(); + pc.prevViewProjection = fsr2_.prevViewProjection; + pc.invCurrentViewProj = glm::inverse(currentVP); vkCmdPushConstants(currentCmd, fsr2_.motionVecPipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(pc), &pc); @@ -4173,7 +4199,11 @@ void Renderer::dispatchTemporalAccumulate() { 1.0f / swapExtent.width, 1.0f / swapExtent.height); glm::vec2 jitter = camera->getJitter(); pc.jitterOffset = glm::vec4(jitter.x, jitter.y, 0.0f, 0.0f); - pc.params = glm::vec4(fsr2_.needsHistoryReset ? 1.0f : 0.0f, fsr2_.sharpness, 0.0f, 0.0f); + pc.params = glm::vec4( + fsr2_.needsHistoryReset ? 1.0f : 0.0f, + fsr2_.sharpness, + static_cast(fsr2_.convergenceFrame), + 0.0f); vkCmdPushConstants(currentCmd, fsr2_.accumulatePipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(pc), &pc); @@ -4187,6 +4217,7 @@ void Renderer::dispatchTemporalAccumulate() { void Renderer::renderFSR2Sharpen() { if (!fsr2_.sharpenPipeline || currentCmd == VK_NULL_HANDLE) return; + constexpr bool kFsr2TemporalEnabled = false; VkExtent2D ext = vkCtx->getSwapchainExtent(); uint32_t outputIdx = fsr2_.currentHistory; @@ -4198,7 +4229,9 @@ void Renderer::renderFSR2Sharpen() { // Update sharpen descriptor to point at current history output VkDescriptorImageInfo imgInfo{}; imgInfo.sampler = fsr2_.linearSampler; - imgInfo.imageView = fsr2_.history[outputIdx].imageView; + imgInfo.imageView = kFsr2TemporalEnabled + ? fsr2_.history[outputIdx].imageView + : fsr2_.sceneColor.imageView; imgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};