FSR2 temporal upscaling fixes: unjittered reprojection, sharpen Y-flip, MSAA guard, descriptor double-buffering
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run

- Motion vectors: single unjittered reprojection matrix (80 bytes) instead of
  two jittered matrices (160 bytes), eliminating numerical instability from
  jitter amplification through large world coordinates
- Sharpen pass: fix Y-flip for correct UV sampling, double-buffer descriptor
  sets to avoid race with in-flight command buffers
- MSAA: auto-disable when FSR2 enabled, grey out AA setting in UI
- Accumulation: variance-based neighborhood clamping in YCoCg space,
  correct history layout transitions
- Frame index: wrap at 256 for stable Halton sequence
This commit is contained in:
Kelsi 2026-03-08 01:22:15 -08:00
parent 52317d1edd
commit e94eb7f2d1
9 changed files with 95 additions and 106 deletions

View file

@ -2,25 +2,19 @@
layout(local_size_x = 8, local_size_y = 8) in;
// Inputs (internal resolution)
layout(set = 0, binding = 0) uniform sampler2D sceneColor;
layout(set = 0, binding = 1) uniform sampler2D depthBuffer;
layout(set = 0, binding = 2) uniform sampler2D motionVectors;
// History (display resolution)
layout(set = 0, binding = 3) uniform sampler2D historyInput;
// Output (display resolution)
layout(set = 0, binding = 4, rgba16f) uniform writeonly image2D historyOutput;
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 (pixel-space), zw = unused
vec4 jitterOffset; // xy = current jitter (NDC-space), zw = unused
vec4 params; // x = resetHistory (1=reset), y = sharpness, zw = unused
} pc;
// RGB <-> YCoCg for neighborhood clamping
vec3 rgbToYCoCg(vec3 rgb) {
float y = 0.25 * rgb.r + 0.5 * rgb.g + 0.25 * rgb.b;
float co = 0.5 * rgb.r - 0.5 * rgb.b;
@ -40,76 +34,52 @@ void main() {
ivec2 outSize = ivec2(pc.displaySize.xy);
if (outPixel.x >= outSize.x || outPixel.y >= outSize.y) return;
// Output UV in display space
vec2 outUV = (vec2(outPixel) + 0.5) * pc.displaySize.zw;
vec3 currentColor = texture(sceneColor, outUV).rgb;
// Map display pixel to internal resolution UV (accounting for jitter)
vec2 internalUV = outUV;
// Sample current frame color at internal resolution
vec3 currentColor = texture(sceneColor, internalUV).rgb;
// Sample motion vector at internal resolution
vec2 inUV = outUV; // Approximate — display maps to internal via scale
vec2 motion = texture(motionVectors, inUV).rg;
// Reproject: where was this pixel in the previous frame's history?
vec2 historyUV = outUV - motion;
// History reset: on teleport / camera cut, just use current frame
if (pc.params.x > 0.5) {
imageStore(historyOutput, outPixel, vec4(currentColor, 1.0));
return;
}
// Sample reprojected history
vec2 motion = texture(motionVectors, outUV).rg;
vec2 historyUV = outUV + motion;
float historyValid = (historyUV.x >= 0.0 && historyUV.x <= 1.0 &&
historyUV.y >= 0.0 && historyUV.y <= 1.0) ? 1.0 : 0.0;
vec3 historyColor = texture(historyInput, historyUV).rgb;
// Neighborhood clamping in YCoCg space to prevent ghosting
// Sample 3x3 neighborhood from current frame
// Neighborhood clamping in YCoCg space
vec2 texelSize = pc.internalSize.zw;
vec3 samples[9];
int idx = 0;
for (int dy = -1; dy <= 1; dy++) {
for (int dx = -1; dx <= 1; dx++) {
samples[idx] = rgbToYCoCg(texture(sceneColor, internalUV + vec2(dx, dy) * texelSize).rgb);
idx++;
}
}
vec3 s0 = rgbToYCoCg(currentColor);
vec3 s1 = rgbToYCoCg(texture(sceneColor, outUV + vec2(-texelSize.x, 0.0)).rgb);
vec3 s2 = rgbToYCoCg(texture(sceneColor, outUV + vec2( texelSize.x, 0.0)).rgb);
vec3 s3 = rgbToYCoCg(texture(sceneColor, outUV + vec2(0.0, -texelSize.y)).rgb);
vec3 s4 = rgbToYCoCg(texture(sceneColor, outUV + vec2(0.0, texelSize.y)).rgb);
vec3 s5 = rgbToYCoCg(texture(sceneColor, outUV + vec2(-texelSize.x, -texelSize.y)).rgb);
vec3 s6 = rgbToYCoCg(texture(sceneColor, outUV + vec2( texelSize.x, -texelSize.y)).rgb);
vec3 s7 = rgbToYCoCg(texture(sceneColor, outUV + vec2(-texelSize.x, texelSize.y)).rgb);
vec3 s8 = rgbToYCoCg(texture(sceneColor, outUV + vec2( texelSize.x, texelSize.y)).rgb);
// Compute AABB in YCoCg
vec3 boxMin = samples[0];
vec3 boxMax = samples[0];
for (int i = 1; i < 9; i++) {
boxMin = min(boxMin, samples[i]);
boxMax = max(boxMax, samples[i]);
}
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 stddev = sqrt(variance);
// Slightly expand the box to reduce flickering on edges
vec3 boxCenter = (boxMin + boxMax) * 0.5;
vec3 boxExtent = (boxMax - boxMin) * 0.5;
boxMin = boxCenter - boxExtent * 1.25;
boxMax = boxCenter + boxExtent * 1.25;
float gamma = 1.5;
vec3 boxMin = mean - gamma * stddev;
vec3 boxMax = mean + gamma * stddev;
// Clamp history to the neighborhood AABB
vec3 historyYCoCg = rgbToYCoCg(historyColor);
vec3 clampedHistory = clamp(historyYCoCg, boxMin, boxMax);
historyColor = yCoCgToRgb(clampedHistory);
// Check if history UV is valid (within [0,1])
float historyValid = (historyUV.x >= 0.0 && historyUV.x <= 1.0 &&
historyUV.y >= 0.0 && historyUV.y <= 1.0) ? 1.0 : 0.0;
// Blend factor: use more current frame for disoccluded regions
// Luminance difference between clamped history and original → confidence
float clampDist = length(historyYCoCg - clampedHistory);
float blendFactor = mix(0.05, 0.3, clamp(clampDist * 4.0, 0.0, 1.0));
// If history is off-screen, use current frame entirely
float blendFactor = mix(0.05, 0.30, clamp(clampDist * 2.0, 0.0, 1.0));
blendFactor = mix(blendFactor, 1.0, 1.0 - historyValid);
// Final blend
vec3 result = mix(historyColor, currentColor, blendFactor);
imageStore(historyOutput, outPixel, vec4(result, 1.0));
}

View file

@ -6,10 +6,8 @@ layout(set = 0, binding = 0) uniform sampler2D depthBuffer;
layout(set = 0, binding = 1, rg16f) uniform writeonly image2D motionVectors;
layout(push_constant) uniform PushConstants {
mat4 invViewProj; // Inverse of current jittered VP
mat4 prevViewProj; // Previous frame unjittered VP
mat4 reprojMatrix; // prevUnjitteredVP * inverse(currentUnjitteredVP)
vec4 resolution; // xy = internal size, zw = 1/internal size
vec4 jitterOffset; // xy = current jitter (NDC), zw = previous jitter
} pc;
void main() {
@ -20,25 +18,18 @@ void main() {
// Sample depth (Vulkan: 0 = near, 1 = far)
float depth = texelFetch(depthBuffer, pixelCoord, 0).r;
// Pixel center in NDC [-1, 1]
// Pixel center in UV [0,1] and NDC [-1,1]
vec2 uv = (vec2(pixelCoord) + 0.5) * pc.resolution.zw;
vec2 ndc = uv * 2.0 - 1.0;
// Reconstruct world position from depth
// Clip-to-clip reprojection: current unjittered clip → previous unjittered clip
vec4 clipPos = vec4(ndc, depth, 1.0);
vec4 worldPos = pc.invViewProj * clipPos;
worldPos /= worldPos.w;
// Project into previous frame's clip space (unjittered)
vec4 prevClip = pc.prevViewProj * worldPos;
vec4 prevClip = pc.reprojMatrix * clipPos;
vec2 prevNdc = prevClip.xy / prevClip.w;
vec2 prevUV = prevNdc * 0.5 + 0.5;
// Remove jitter from current UV to get unjittered position
vec2 unjitteredUV = uv - pc.jitterOffset.xy * 0.5;
// Motion = previous position - current unjittered position (in UV space)
vec2 motion = prevUV - unjitteredUV;
// Motion = previous position - current position (both unjittered, in UV space)
vec2 motion = prevUV - uv;
imageStore(motionVectors, pixelCoord, vec4(motion, 0.0, 0.0));
}

Binary file not shown.

View file

@ -10,16 +10,20 @@ layout(push_constant) uniform PushConstants {
} pc;
void main() {
// Undo the vertex shader Y flip (postprocess.vert flips for Vulkan overlay,
// but we need standard UV coords for texture sampling)
vec2 tc = vec2(TexCoord.x, 1.0 - TexCoord.y);
vec2 texelSize = pc.params.xy;
float sharpness = pc.params.z;
// RCAS: Robust Contrast-Adaptive Sharpening
// 5-tap cross pattern
vec3 center = texture(inputImage, TexCoord).rgb;
vec3 north = texture(inputImage, TexCoord + vec2(0.0, -texelSize.y)).rgb;
vec3 south = texture(inputImage, TexCoord + vec2(0.0, texelSize.y)).rgb;
vec3 west = texture(inputImage, TexCoord + vec2(-texelSize.x, 0.0)).rgb;
vec3 east = texture(inputImage, TexCoord + vec2( texelSize.x, 0.0)).rgb;
vec3 center = texture(inputImage, tc).rgb;
vec3 north = texture(inputImage, tc + vec2(0.0, -texelSize.y)).rgb;
vec3 south = texture(inputImage, tc + vec2(0.0, texelSize.y)).rgb;
vec3 west = texture(inputImage, tc + vec2(-texelSize.x, 0.0)).rgb;
vec3 east = texture(inputImage, tc + vec2( texelSize.x, 0.0)).rgb;
// Compute local contrast (min/max of neighborhood)
vec3 minRGB = min(center, min(min(north, south), min(west, east)));

Binary file not shown.