From c3047c33baad3094f89ccf4fb068359fbc235940 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 8 Mar 2026 14:18:00 -0700 Subject: [PATCH] FSR2: fix motion vector jitter, add bicubic anti-ringing, depth-dilated MVs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Motion shader: unjitter NDC before reprojection (ndc+jitter, not ndc-jitter), compute motion against unjittered UV so static scenes produce zero motion - Pass jitter offset to motion shader (push constant 80→96 bytes) - Accumulate shader: restore Catmull-Rom bicubic with anti-ringing clamp to prevent negative-lobe halos at edges while maintaining sharpness - Add depth-dilated motion vectors (3x3 nearest-to-camera) to prevent background MVs bleeding over foreground edges - Widen neighborhood clamp gamma to 3.0, uniform 5% blend with disocclusion/velocity reactive boosting --- assets/shaders/fsr2_accumulate.comp.glsl | 89 +++++++++++++++++++++-- assets/shaders/fsr2_accumulate.comp.spv | Bin 10592 -> 18052 bytes assets/shaders/fsr2_motion.comp.glsl | 22 ++++-- assets/shaders/fsr2_motion.comp.spv | Bin 3096 -> 2184 bytes src/rendering/renderer.cpp | 15 ++-- 5 files changed, 108 insertions(+), 18 deletions(-) diff --git a/assets/shaders/fsr2_accumulate.comp.glsl b/assets/shaders/fsr2_accumulate.comp.glsl index 7fb0cb27..44e7c2ad 100644 --- a/assets/shaders/fsr2_accumulate.comp.glsl +++ b/assets/shaders/fsr2_accumulate.comp.glsl @@ -29,20 +29,84 @@ vec3 yCoCgToRgb(vec3 ycocg) { return vec3(y + co - cg, y + cg, y - co - cg); } +// Catmull-Rom bicubic (9 bilinear taps) with anti-ringing clamp. +// Sharper than bilinear; anti-ringing prevents edge halos that shift with jitter. +vec3 sampleBicubic(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; + + // Catmull-Rom weights + 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; + + // 3x3 bilinear taps covering 4x4 texel support + 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. + // Prevents Catmull-Rom negative lobe overshoots at high-contrast edges. + 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); +} + void main() { ivec2 outPixel = ivec2(gl_GlobalInvocationID.xy); ivec2 outSize = ivec2(pc.displaySize.xy); if (outPixel.x >= outSize.x || outPixel.y >= outSize.y) return; vec2 outUV = (vec2(outPixel) + 0.5) * pc.displaySize.zw; - vec3 currentColor = texture(sceneColor, outUV).rgb; + + // Bicubic upsampling with anti-ringing: sharp without edge halos + vec3 currentColor = sampleBicubic(sceneColor, outUV, pc.internalSize.xy); if (pc.params.x > 0.5) { imageStore(historyOutput, outPixel, vec4(currentColor, 1.0)); return; } - vec2 motion = texture(motionVectors, outUV).rg; + // Depth-dilated motion vector: pick the MV from the nearest-to-camera + // pixel in a 3x3 neighborhood. Prevents background MVs from bleeding + // over foreground edges. + vec2 texelSize = pc.internalSize.zw; + float closestDepth = texture(depthBuffer, outUV).r; + vec2 closestOffset = vec2(0.0); + for (int y = -1; y <= 1; y++) { + for (int x = -1; x <= 1; x++) { + vec2 off = vec2(float(x), float(y)) * texelSize; + float d = texture(depthBuffer, outUV + off).r; + if (d < closestDepth) { + closestDepth = d; + closestOffset = off; + } + } + } + vec2 motion = texture(motionVectors, outUV + closestOffset).rg; + vec2 historyUV = outUV + motion; float historyValid = (historyUV.x >= 0.0 && historyUV.x <= 1.0 && @@ -50,8 +114,9 @@ void main() { vec3 historyColor = texture(historyInput, historyUV).rgb; - // Neighborhood clamping in YCoCg space - vec2 texelSize = pc.internalSize.zw; + // Neighborhood clamping in YCoCg space with wide gamma. + // Wide gamma (3.0) prevents jitter-chasing: the clamp box only catches + // truly stale history (disocclusion), not normal jitter variation. 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); @@ -68,7 +133,7 @@ void main() { vec3 variance = max(m2 / 9.0 - mean * mean, vec3(0.0)); vec3 stddev = sqrt(variance); - float gamma = 1.5; + float gamma = 3.0; vec3 boxMin = mean - gamma * stddev; vec3 boxMax = mean + gamma * stddev; @@ -77,7 +142,19 @@ void main() { historyColor = yCoCgToRgb(clampedHistory); float clampDist = length(historyYCoCg - clampedHistory); - float blendFactor = mix(0.05, 0.30, clamp(clampDist * 2.0, 0.0, 1.0)); + + // Uniform 5% blend: ~45 frames for 90% convergence. + // Simpler than edge-aware; the anti-ringing bicubic handles edge stability. + float blendFactor = 0.05; + + // Disocclusion: large clamp distance → rapidly replace stale history + blendFactor = mix(blendFactor, 0.60, clamp(clampDist * 5.0, 0.0, 1.0)); + + // Velocity: higher blend during motion reduces ghosting + float motionMag = length(motion * pc.displaySize.xy); + blendFactor = max(blendFactor, clamp(motionMag * 0.15, 0.0, 0.35)); + + // Full current frame when history is out of bounds blendFactor = mix(blendFactor, 1.0, 1.0 - historyValid); vec3 result = mix(historyColor, currentColor, blendFactor); diff --git a/assets/shaders/fsr2_accumulate.comp.spv b/assets/shaders/fsr2_accumulate.comp.spv index 47529d75ae3b2f227713851a616bea13ce070b00..f69e0f3b26e197e6bb3f1616935effe61fa9b971 100644 GIT binary patch literal 18052 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 literal 10592 zcmZvh2b>*M6~-so>=GbA=pb!FP*9LwMFJtf5(p*~ks^ebw{JHOclWKkZ({-|BuG&d z8%Pl_f?#3+i4Bnwu!03tQ0$`E3wE*l|IfQ8xz5jF&+mTUcg{Wco-=o5vP;+GnHvl@7RU)(b0wD?PWuag+tAe#=`#QSfk!< zE@>DyHH&MG)Q2Ye^MGz5^7Xq}dt=rZ8(ef+>&!W=IfHvF893l@_SwaXr4H_)j-ptm z`?S_6gNq_&3Y|c&x9Wq8&asAN3D0D#w>tWsVuFmBmBq}?VvboC(_NeeZcVfoG?zAp zfXtg(_G)lwVc$?|QEh1M$dXpQ)^4^&<~q;17Io&F+Pt6v?3ns=F&uNW}w`(Kq zad=O$L8$|a=199SHc}fZCzQU<9DCp293LI3Eh}9y$9r*pv(2RW0|VoYHk5qbWj^on zXl<-E%p%q}Ag_1!wE|2nFF&2peO<*^pBNi!jI`&phFW7#^zPyy=}%G{5kw;SA`)IjTg0&Sd=?;f87mpx(B_msZ!j5 zTk`SoDY^b$xZesjZ3cY9gw^%i$C$Gx++15=MRCcjt>IgOdZk?YWAZ(dzK>K zxnSeX=lnXOd-h)L4Dwv4<>tCRboNTWdF2C1vkvz-h{ZaK!RC#9H^I&~?^dWj*ye39 zT2H6$DaG={oxeRZ-o$7g^X6CZ9jN@xIE*vj)hS5Leab!GUeWKjcWB97_gbtAbN)LS z-KT51{!IzTdeHK+uBh$iq#5g)a-RvWSo7ABU2fR`(4ht-{zeAZO*yh<>1~QzstexuixdI`(4ht z-{qY9J&+`c16G1v23 zV{YGDug26PW(@4SJZ2oOeiwOs=e9BDsjniB+61QlSJr(Equ1Z@sq;^(QuCP>$68Cl z?#FjR>}MHRJz_2ZyPrJfLb!VD=OVE4)c?jBuJ2lof2L>d@3olw^POV6`*5!pGkUpK zV_t`;IbZxQx+_+5kKVsaG56hqdp};Ev;}0BUxsa$Yd75o=Nqs8 zR;%K31{{_voWZe}EY8(Z?_~_vbzO1f!StNZ-dX zHSdu)a^C~4=Kds_`h6+)yT?=-9OF6t6xem_t?{nmKJI7qav#RrhpD*_aqQy( zu>05^vrnFf2Ql@iac$D9(S1Z7$Job1VApYPYcbbwAD?FQav#P#jH$T~aqQzW;Oag; zi>4m^_#9X*`Vn~?V;`RfyN-J^-ZfTJm34gq)9-w1@t&_?RF8A{Mew7EdxpM*xvw~v zUj`eaJ{dDlyaT=h)~|VaE{$=WM;P^6Z*Z}r8=Gt|}uVL!(F8g}Y)Xmd{nd>n| zea`cF{swjxrtitb4`UsD-^4b?)b&4+w5a!6@cf}td`gGOSrl8MNPi~yPm#kO}|0YuX*L?xvtQODcG;p;Ko1m%3oK3-M(U;A@=24HnOb5FkebJZA!R}dKwJ%$u>DRm*jInNi zf6^a)*$Ql5)b;!OmRj^>8*tv2ZQ*%ewu4vuvOSu5^x=6aCO`8#ps7b6b_AA!zDR_n~0tsYl(1 zfn85u)cq2$d(u~}`=x05H7|##+u!r`N8Lw&ty|r?_hVGEZok(@f}daveiYpAnS0Ye z8*Yqx#2gLw`w%g6;Kr#(%rW3+vzTMy#;HflabVw9k*5!CoO;9@5AMMtW-iz`^@uqE z?75DZ6T!x*N6b91_dH@w0vo3uF)st#pNN?cHcs8%`8N}_h(8&e_wJO${r%70oeDQb zJ?5MSw!gt&o_NeT9d3+z?Bf++bLoqDXMnv2!Ou)Q@|*=XMm_SJ4K|m)nD5F;ifxVZ(Yl%mmMQ~%(BTpS{E`2etAME`O-bg(148V<1k3555 zbLoqDi@~0W;LXG%&-rj;)FaQUz~<5y^M=5lo8ZHVN1hS5G3t@01vZzym^TV8pS|?l zy*ly8GX^(CJ@Sl$&85%tw}4SCa!#mY3;1or^Y$9>lFY75=Wr>SvHH!^W>kwj7l6$j zc`gM1zdRSA8>>I&z7}l0;IB(O>be+ijQXW%KbL^brEeNLcke5}{1kg&`j=zwbs3|+ z$p3n<+O$;jWng}aT`+ylxs*{|d^zJ4Sj>L|IQHxBXm5lY6aK5f&c8bC^-W+k<6{4B z2AfBpYh1~w7CGJm&ewP=JYVB&@O+K8!_|z7d;bowdGxu))r@Mf#yi3Oj;HThusq)R z*MWWKtLwjpQ7z`)09FfrBiI^(zYFYo)6(CXZUXbe|EJ(@L;&NBG57V1`eW`bU^RU= zGsgF|O6hT6TF6b*5O)r z!Og86`9BQir!c?s^&1!Y^+%u9fbCuIkHD`=-25MfyM8>+9|Nm}#eMxa8b9Tl<}{D7 zu6Z}3e%H8(m`{LjNo${ZW9G?!KiVegah|q?Z-J@Dyyt!RnD~2G~9HVv%cCu=(_PPpw@o^6mz%*7kff^~n1IuzOUGdAozF`|-W49`jxZ zcAk2i`xk-Dr_cS^d$q{>VsLdoJ`d`VcOURxn0n0H7wo*e*ZaZMW8VH?_oKc%_4)v? z`SeAu1HtCXdwmdGJ#rlkw%6)W+aX}{>9aP^ky_lZL&5ex_+en{5B?IcbK;pj9PAw9 z^!=N2+|kyb`AyNi@-x2)+%vx!W1R6>;QY+buJB_Lj~*NeH&1@%kAkaPoB8Ase>B+m zy#I6H>c+dCJmQZ98=v?1IJmm;?nfTI=_~d1{CK!Im#6zU7i^4r%sByUp140Jf{jy; zn0a7p34RjTIg$5eU}MxHW= imgSize.x || pixelCoord.y >= imgSize.y) return; - // Sample depth (Vulkan: 0 = near, 1 = far) float depth = texelFetch(depthBuffer, pixelCoord, 0).r; - // Pixel center in UV [0,1] and NDC [-1,1] + // Pixel center UV and NDC vec2 uv = (vec2(pixelCoord) + 0.5) * pc.resolution.zw; vec2 ndc = uv * 2.0 - 1.0; - // Clip-to-clip reprojection: current unjittered clip → previous unjittered clip - vec4 clipPos = vec4(ndc, depth, 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; + + // Reproject to previous frame via unjittered VP matrices + vec4 clipPos = vec4(unjitteredNDC, depth, 1.0); vec4 prevClip = pc.reprojMatrix * clipPos; vec2 prevNdc = prevClip.xy / prevClip.w; vec2 prevUV = prevNdc * 0.5 + 0.5; - // Motion = previous position - current position (both unjittered, in UV space) - vec2 motion = prevUV - uv; + // 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; imageStore(motionVectors, pixelCoord, vec4(motion, 0.0, 0.0)); } diff --git a/assets/shaders/fsr2_motion.comp.spv b/assets/shaders/fsr2_motion.comp.spv index faa3d8362634407aa4109d6e924888330bdb0a92..8a50542d0f9a23e5845c8ef189f505ef3763d5be 100644 GIT binary patch literal 2184 zcmZvbTT50^5QcYJb}&zP&a%&pPMT>)OH6}8DM>3j-K41mLKSw+zv8M`so zv-o7T=j5qgBdfD=MbTG7%og;`VE+Exf_xRIg_o-X+Y-z@0;|6g>;|LQO-Pq{M)`)_ zS!Dm^Xqw!AIdd8}T*8$lZUQ+%ujZ4p_BrG};0n7VH;eW%dZ}Mqf8Kj}rriqMyN>i6 z+VxbhMhEr09rZ?HDv$->{$_0Zmka+6^a60{H=mq23fMc5?v)=%w)YqLs?m)%UlY6P zkp*C`2Bfy{(89M7DW~0@?m0Ml>op^{1Mkdw+AWFe!nSwM8})agTf_S9Z$rA@d=0P# zr2Fi#1GvKOLN`wTRRIM+0~=kmVncM18wevS8y?ePWDWvo4FJ3p?dxtg0r+dFy#{9n4c zY2Vx#lM~)oH}B@1e*m6G1osi$GlXwm8Q0u`JA|83{SSuvmUum{MS&oi_qPTpm{g_LhEy8dhA z^DXuRYq)|tfNq@Y3vmVHL13)D&*Ul~4*_}4eFW+HiBJA8kn`;7k^3mR_3SCQW9ZhC zi+a82#_Ib)j>x6_$~gk<2Xhl{}2m&9m~B+eO&x4Mk(I|*|-Pirau3c7oY zd6&3xbb0@=)x=##mv>HMPHv$)C;n?OC%4h%qxKziIpOmDqu0CW<~1hHcn@9P72G7c zoO>tIJx!q-t8W-g0Q;Io&H(%J?(EC|eiq2TC&qhzkhm#i^g5S#eJ<-o4-e7hy}OBY zC-dm?Vm)yS=;qNE_xTuI-v2$$zKAaGJ4M`6bkA8D_Y7O!xOj`_=QWe muh5Ni1-FJSS9*`Hv5nQYnlMAo6?ErHf9Whc)4$2k5B>lsn|<8? literal 3096 zcmZvdYj0Fl6oyYPv_J(!C>K$jf_D%>1yO{SmO4}mEpkzdIxRDYa&S6ln3>jsm`IF? zpG^E0e)B*0pNyBp#OFD)H;#>)yj|;k_u6Z%eL2HG|KZ^z=}ShEZOJ!DckE31QIbS^ z0y~ln6}&uqb@o`hQ#TATkydJ1E3J02wbZ!HJ#ks1T3@a825cgtxBezN z+Cq(Hww~6f^1M|`P&OBRE!QjMdVXuEKGRsst4p0MZ_HdE)*Me^vz6tm*@F~5l++$n#(lQZc3xjKy}1XHeQ3>Ux6Ud2 zTH5T~!P@J)kx#DPzMZy;_(`ubT(Vbs`ujH|}p~QW4`3;UdZ^_Hl@*YRPGvFfJ^Kf|I7x^3c;X?hd#^d4* z{Hu8;#!Y}S=iyr4fpy&fA>fF7tV;`SKfUvE=gny@`ToKmOdewGS-y)3))*tk7pp%? zy&-G~xPC9(Ue%m0!F{i4!uYL)&L4*H=2tVP@lRmQrG6%G&o1WQj&HpA_poXgwgk+z z8!Pwy?&I$$R!x48zn)XnthE>J+g7)h+_$B61nxfEd#wF5zWL+c4&WPWjoqjvtn2J= z3^=TF2pi8O&L2TDuk$Cvyt((2@olWW^SvYT1MK@W6UMpzoe-qvKIFawN8HERE-m8E zao$?ffH5BVvxf72+o!=l?Dyvx diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 063bae9a..c3449f93 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -3785,7 +3785,7 @@ bool Renderer::initFSR2Resources() { VkPushConstantRange pc{}; pc.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; pc.offset = 0; - pc.size = sizeof(glm::mat4) + sizeof(glm::vec4); // 80 bytes + pc.size = sizeof(glm::mat4) + 2 * sizeof(glm::vec4); // 96 bytes VkPipelineLayoutCreateInfo plCI{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; plCI.setLayoutCount = 1; @@ -4086,17 +4086,20 @@ void Renderer::dispatchMotionVectors() { vkCmdBindDescriptorSets(currentCmd, VK_PIPELINE_BIND_POINT_COMPUTE, fsr2_.motionVecPipelineLayout, 0, 1, &fsr2_.motionVecDescSet, 0, nullptr); - // Single reprojection matrix: prevUnjitteredVP * inv(currentUnjitteredVP) - // Both matrices are unjittered — jitter only affects sub-pixel sampling, - // not motion vector computation. This avoids numerical instability from - // jitter amplification through large world coordinates. + // 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). struct { - glm::mat4 reprojMatrix; // prevUnjitteredVP * inv(currentUnjitteredVP) + glm::mat4 reprojMatrix; glm::vec4 resolution; + glm::vec4 jitterOffset; // xy = current jitter (NDC), zw = unused } 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),