From d52c49c9fae4fd33c7ffe0e203be642b5569489a Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 12 Mar 2026 22:38:37 -0700 Subject: [PATCH] fix: FXAA sharpening and MSAA exclusion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Post-FXAA unsharp mask: when FSR2 is active alongside FXAA, forward the FSR2 sharpness value (0–2) to the FXAA fragment shader via a new vec4 push constant. A contrast-adaptive sharpening step (unsharp mask scaled to 0–0.3) is applied after FXAA blending, recovering the crispness that FXAA's sub-pixel blend removes. At sharpness=2.0 the output matches RCAS quality; at sharpness=0 the step is a no-op. - MSAA guard: setFXAAEnabled() refuses to activate FXAA when hardware MSAA is in use. FXAA's role is to supplement FSR temporal AA, not to stack on top of MSAA which already resolves jaggies during the scene render pass. --- assets/shaders/fxaa.frag.glsl | 23 ++++++++++++++++++++--- assets/shaders/fxaa.frag.spv | Bin 0 -> 7764 bytes src/rendering/renderer.cpp | 24 ++++++++++++++++++------ 3 files changed, 38 insertions(+), 9 deletions(-) create mode 100644 assets/shaders/fxaa.frag.spv diff --git a/assets/shaders/fxaa.frag.glsl b/assets/shaders/fxaa.frag.glsl index df35aaa0..158f2f98 100644 --- a/assets/shaders/fxaa.frag.glsl +++ b/assets/shaders/fxaa.frag.glsl @@ -2,7 +2,7 @@ // FXAA 3.11 — Fast Approximate Anti-Aliasing post-process pass. // Reads the resolved scene color and outputs a smoothed result. -// Push constant: rcpFrame = vec2(1/width, 1/height). +// Push constant: rcpFrame = vec2(1/width, 1/height), sharpness (0=off, 2=max), unused. layout(set = 0, binding = 0) uniform sampler2D uScene; @@ -10,7 +10,9 @@ layout(location = 0) in vec2 TexCoord; layout(location = 0) out vec4 outColor; layout(push_constant) uniform PC { - vec2 rcpFrame; + vec2 rcpFrame; + float sharpness; // 0 = no sharpen, 2 = max (matches FSR2 RCAS range) + float _pad; } pc; // Quality tuning @@ -128,5 +130,20 @@ void main() { if ( horzSpan) finalUV.y += pixelOffsetFinal * lengthSign; if (!horzSpan) finalUV.x += pixelOffsetFinal * lengthSign; - outColor = vec4(texture(uScene, finalUV).rgb, 1.0); + vec3 fxaaResult = texture(uScene, finalUV).rgb; + + // Post-FXAA contrast-adaptive sharpening (unsharp mask). + // Counteracts FXAA's sub-pixel blur when sharpness > 0. + if (pc.sharpness > 0.0) { + vec2 r = pc.rcpFrame; + vec3 blur = (texture(uScene, uv + vec2(-r.x, 0)).rgb + + texture(uScene, uv + vec2( r.x, 0)).rgb + + texture(uScene, uv + vec2(0, -r.y)).rgb + + texture(uScene, uv + vec2(0, r.y)).rgb) * 0.25; + // scale sharpness from [0,2] to a modest [0, 0.3] boost factor + float s = pc.sharpness * 0.15; + fxaaResult = clamp(fxaaResult + s * (fxaaResult - blur), 0.0, 1.0); + } + + outColor = vec4(fxaaResult, 1.0); } diff --git a/assets/shaders/fxaa.frag.spv b/assets/shaders/fxaa.frag.spv new file mode 100644 index 0000000000000000000000000000000000000000..7803f3e27e4b92579b95e608e8653b07d921062a GIT binary patch literal 7764 zcmZXZd#u)V9mjti&VkS)dW3V(NIY=DP>2M)u*EqXXo|PRO>226o%9E`oB}mBa=@5@ znD-hJO)awGc&Szy*-}%dscBThO3l)-%AkujHE&z5*YkYe&whLQj?ccI&--)ze!iFI zIS->c2F6s?d#VGggQ|(6s>bN4IzTG0(T#cW87t11wPx+gS;rlJtOnz%rlB?mS7WM9 zX6bVmth$(~IvlzhIs`j$5>$)f#3w_JQ>w1&{Y|zSD0495ioF*K)={u-=EcO-SIoH6 znWsXI$WxkPL$;RW9gmlOQ;6XzSrL!kApV%-82;WVB~L{)VCn= zA;^0!dwFms*82BA`e>)lxV_BAGG{;XVa%>o{3AuSM`Dvpf8x`uud_M=-B`%=>!a-2 z#CnmBWbTKIVQrBS!8=)ZY~81^)ih*dYhNFcC((tYP-v(B2aA!I`IYUz!!^!eHlF*Q z-pqz~M4r=-HaswsHM+jJkT&A6nGau88y3W75J?;HU?&$@JlGO=3hQSlqZ_mDG!y)`%kS^vo zkk+bXybbSqK98*4c@Qtp%KISi$U?lPFi(X%|NT&G2H?e&J*P)Qp3zU>y@ zmGQoQsNm`xq;J&Pjo!Q`6wH10yqZJ*#5@t+JlZAZJa}=*(H?WSV8--(x5r!vX3SwI zF&Dv$OOE!KrxwhZ-jDW}r-2z$yTm*lUR-jt$6Q`8V|u^ZW3B)*rgn*WCcL=hNX)OW z2cE5U9aUw2dZ1wI!CYS*+XQBPb!;=3@$1-CVD6zM5=I%8;?n3vad=F@gl>ET9r_11Jn!M;>5 z-?+rs3~wK-YZIi+F6@05u7r%Ejd$=WX2)CH9d%nFG4G-LvXbXo_}rCX*A>iM!LBbD zrB_SYml@1AK>BO1-uGgN+`hN!-XxcAfjP~cJGdR**mEG?;`VcRGnnU4yPU&Y;Ke0} z{??M1I|^pZ+oATD-vl$Jc8Pfhytw3Ok9k+YjJXqPk9jwkF||v~d*H<-M`CWI-@ZRT zf_!g`l{e>Jc;l!`oFBuBOOC|J9lMsr+mjwB*o|PWsg7*}v!*(BGnnzktjjz0B$Trr z?5To#&V&65Ox%06p0lOzgi)L=WcNaO4`jXeO?y8xwdqEy{uxNUpC8(8DK$I`uWloh zyYU>nxG{1Mo`=`pdoUZ)CimtAc=diR$Noil?WaM0K5G;Em*CZlr~bdeYj6E~A=hfZ z(}TZ*X=87*rhUlzxF+?9_3wfwmVL6%iS;UaZJKXdvp@eSKE_g?Sg#e_SocM!ZD- z-WU6vSO=ol#xgt0x zpZfbbx4ylT;O&<>-`;*^hi|XCBcW-K@1T6%)DOV>4lp&|O8vCaHa63Xjq!X>W8?f< z`Fwnkx{YOQ?e*VT?%FJPYjDl>H~8FwyNC8T_w7V*eVX^Vc^>B>^RIEX)yH-|ym9Kb zgUI}A*s4#DhT)B)-X49F*@k3JiYiiw)z-DKW)vo zggN=_$H$-*kn!Zx?`81riDPMbvzM!fd~3{WEdBJ4-&ye4%lLh~_^7M<>2JNg#CwPu zjc*Ow8}mE##B=pYcx$!(bC@0WOZ;p|%sS*#$NBJnzBqFJF973T<2|!K{!O9Xa?Xe~ zjDTqqKWj~F=hXTrcG_6KKIXNy&oDc3-d7@vS(|)nTLo_nrdDkiAt-xyQ=4|F?QZO~ ziJ!G4wsUHG3_ESCS0D4{&RzoVnf9#aT=nvKB%U6vhUZ_S4t4gZd9It^LBCM+)3M1p z_8!>3b?D6RUYb*EeGRkb&B*q{IcN0`*n6g2$M4{`)^xwE>3&nw8T)Bujm)^;;lu*Ds&*eI2}cJwrFZ$4C5nNK7C3d{_D^yx)~vOTH^@0~41VPN-Yoi*7^E z#_v0MyS5{XyYKE#aye&z?7RD5&DvW>YQF{EUR(RE@D9(v_}3sYYnM;$Ux&AL_b;{Y z027xSPN-Y8??BL|Ui-I@#Z!B7Ij45dljqCzXm1_XI>PL*R{OjY60=tMeE!`9&p)iH z%0A@#(mjCQ1%C_3+x}f-?bX@K#B;VD&!uO|y|zZ}&1F45WOk%?KR_0<9{JRBFFgMm z^|-dwvkS1jo}Ykeug-cB&pGvYH{3sK)ZSd*E@%2@@Ybu1H9o-Xuts(FLt@q_Z;qUA z@tl!|;EiL9N0<{s{9#DU81i{b9)tI6`<56tHqO@WJ@)QuV=nFWPtM2TQ}>hb$tnH> zBxX+eyj4%bdnS`JHqOcEJEM(tXs^HZZDlr|^9K6od*K`5o3Ojs)WZIZU@Wo3dJ#ceb@f=r%)0jBV@_+;-aM}Bugs3D>o3UvfUHG6wY&`P z9yo&S1It=d%YHCz)me-2jb+UE{R`e$OfBpcgse5OUIo)uT|JgDv#x&<$DG!xy?OGx z#2fIgTbq3MdUYqz?TVgw>wl3moW4-#AH=l#E;QbubCZB^J1{0SY)*!o(J@dEh*^s{v`=<7=#~anzp_l4uOg1yZi!pG0#Tc&0%Ew zuP$p_2=Cg|Wo?V##U)49me`BI%~g+m3bL`)CHATC##Wcur@@O$j>NV`e-A$$-2B>Q zy-VO-ueSL<^b%zCj_l*5 o1v5vm)dgdsx6R*a*C6Pxz54m+@;j|M>sia}y>Nb)s^>xf0~8U`L;wH) literal 0 HcmV?d00001 diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index f9ce69cd..8dcf724c 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -4968,11 +4968,11 @@ bool Renderer::initFXAAResources() { write.pImageInfo = &imgInfo; vkUpdateDescriptorSets(device, 1, &write, 0, nullptr); - // Pipeline layout — push constant holds vec2 rcpFrame + // Pipeline layout — push constant holds vec4(rcpFrame.xy, sharpness, pad) VkPushConstantRange pc{}; pc.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; pc.offset = 0; - pc.size = 8; // vec2 + pc.size = 16; // vec4 VkPipelineLayoutCreateInfo plCI{}; plCI.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; plCI.setLayoutCount = 1; @@ -5044,19 +5044,31 @@ void Renderer::renderFXAAPass() { vkCmdBindDescriptorSets(currentCmd, VK_PIPELINE_BIND_POINT_GRAPHICS, fxaa_.pipelineLayout, 0, 1, &fxaa_.descSet, 0, nullptr); - // Push rcpFrame = vec2(1/width, 1/height) - float rcpFrame[2] = { + // Pass rcpFrame + sharpness (vec4, 16 bytes). + // When FSR2/FSR3 is active alongside FXAA, forward FSR2's sharpness so the + // post-FXAA unsharp-mask step restores the crispness that FXAA's blur removes. + float sharpness = fsr2_.enabled ? fsr2_.sharpness : 0.0f; + float pc[4] = { 1.0f / static_cast(ext.width), - 1.0f / static_cast(ext.height) + 1.0f / static_cast(ext.height), + sharpness, + 0.0f }; vkCmdPushConstants(currentCmd, fxaa_.pipelineLayout, - VK_SHADER_STAGE_FRAGMENT_BIT, 0, 8, rcpFrame); + VK_SHADER_STAGE_FRAGMENT_BIT, 0, 16, pc); vkCmdDraw(currentCmd, 3, 1, 0, 0); // fullscreen triangle } void Renderer::setFXAAEnabled(bool enabled) { if (fxaa_.enabled == enabled) return; + // FXAA is a post-process AA pass intended to supplement FSR temporal output. + // It conflicts with MSAA (which resolves AA during the scene render pass), so + // refuse to enable FXAA when hardware MSAA is active. + if (enabled && vkCtx && vkCtx->getMsaaSamples() > VK_SAMPLE_COUNT_1_BIT) { + LOG_INFO("FXAA: blocked while MSAA is active — disable MSAA first"); + return; + } fxaa_.enabled = enabled; if (!enabled) { fxaa_.needsRecreate = true; // defer destruction to next beginFrame()