From 1d44802b057200cb2f9fcd5e6e34967e8dbe94c3 Mon Sep 17 00:00:00 2001 From: ZenoArrows <129334871+ZenoArrows@users.noreply.github.com> Date: Wed, 11 Sep 2024 09:58:19 +0200 Subject: [PATCH] AreaScaling: Replace texture() by texelFetch() and use integer vectors. No functional difference, but it cleans up the code a bit. --- .../Effects/Shaders/area_scaling.glsl | 84 ++++-------------- .../Effects/Shaders/AreaScaling.glsl | 84 ++++-------------- .../Effects/Shaders/AreaScaling.spv | Bin 13560 -> 12380 bytes 3 files changed, 34 insertions(+), 134 deletions(-) diff --git a/src/Ryujinx.Graphics.OpenGL/Effects/Shaders/area_scaling.glsl b/src/Ryujinx.Graphics.OpenGL/Effects/Shaders/area_scaling.glsl index f7942315a2..273c64fd9b 100644 --- a/src/Ryujinx.Graphics.OpenGL/Effects/Shaders/area_scaling.glsl +++ b/src/Ryujinx.Graphics.OpenGL/Effects/Shaders/area_scaling.glsl @@ -14,49 +14,6 @@ layout( location=9 ) uniform float dstY1; layout( location=10 ) uniform float scaleX; layout( location=11 ) uniform float scaleY; -vec2 GetResolution() -{ - return vec2(abs(srcX1 - srcX0), abs(srcY1 - srcY0)); -} - -vec2 GetInvResolution() -{ - return vec2(1.0) / GetResolution(); -} - -vec2 GetWindowResolution() -{ - return vec2(abs(dstX1 - dstX0), abs(dstY1 - dstY0)); -} - -vec2 GetInvWindowResolution() -{ - return vec2(1.0) / GetWindowResolution(); -} - -/***** COLOR SAMPLING *****/ - -// Non filtered sample (nearest neighbor) -vec4 QuickSample(vec2 uv) -{ -#if 0 // Test sampling range - const float threshold = 0.00000001; - vec2 xy = uv.xy * GetResolution(); - // Sampling outside the valid range, draw in yellow - if (xy.x < (srcX0 - threshold) || xy.x >(srcX1 + threshold) || xy.y < (srcY0 - threshold) || xy.y >(srcY1 + threshold)) - return vec4(1.0, 1.0, 0.0, 1); - // Sampling at the edges, draw in purple - if (xy.x < srcX0 + 1.0 || xy.x >(srcX1 - 1.0) || xy.y < srcY0 + 1.0 || xy.y >(srcY1 - 1.0)) - return vec4(0.5, 0, 0.5, 1); -#endif - return texture(Source, uv); -} -vec4 QuickSampleByPixel(vec2 xy) -{ - vec2 uv = vec2(xy * GetInvResolution()); - return QuickSample(uv); -} - /***** Area Sampling *****/ // By Sam Belliveau and Filippo Tarpini. Public Domain license. @@ -68,9 +25,9 @@ vec4 QuickSampleByPixel(vec2 xy) vec4 AreaSampling(vec2 xy) { // Determine the sizes of the source and target images. - vec2 source_size = GetResolution(); - vec2 target_size = GetWindowResolution(); - vec2 inverted_target_size = GetInvWindowResolution(); + vec2 source_size = vec2(abs(srcX1 - srcX0), abs(srcY1 - srcY0)); + vec2 target_size = vec2(abs(dstX1 - dstX0), abs(dstY1 - dstY0)); + vec2 inverted_target_size = vec2(1.0) / target_size; // Compute the top-left and bottom-right corners of the target pixel box. vec2 t_beg = floor(xy - vec2(dstX0 < dstX1 ? dstX0 : dstX1, dstY0 < dstY1 ? dstY0 : dstY1)); @@ -81,8 +38,8 @@ vec4 AreaSampling(vec2 xy) vec2 end = t_end * inverted_target_size * source_size; // Compute the top-left and bottom-right corners of the pixel box. - vec2 f_beg = floor(beg); - vec2 f_end = floor(end); + ivec2 f_beg = ivec2(beg); + ivec2 f_end = ivec2(end); // Compute how much of the start and end pixels are covered horizontally & vertically. float area_w = 1.0 - fract(beg.x); @@ -99,39 +56,32 @@ vec4 AreaSampling(vec2 xy) // Initialize the color accumulator. vec4 avg_color = vec4(0.0, 0.0, 0.0, 0.0); - // Prevents rounding errors due to the coordinates flooring above - const vec2 offset = vec2(0.5, 0.5); - // Accumulate corner pixels. - avg_color += area_nw * QuickSampleByPixel(vec2(f_beg.x, f_beg.y) + offset); - avg_color += area_ne * QuickSampleByPixel(vec2(f_end.x, f_beg.y) + offset); - avg_color += area_sw * QuickSampleByPixel(vec2(f_beg.x, f_end.y) + offset); - avg_color += area_se * QuickSampleByPixel(vec2(f_end.x, f_end.y) + offset); + avg_color += area_nw * texelFetch(Source, ivec2(f_beg.x, f_beg.y), 0); + avg_color += area_ne * texelFetch(Source, ivec2(f_end.x, f_beg.y), 0); + avg_color += area_sw * texelFetch(Source, ivec2(f_beg.x, f_end.y), 0); + avg_color += area_se * texelFetch(Source, ivec2(f_end.x, f_end.y), 0); // Determine the size of the pixel box. int x_range = int(f_end.x - f_beg.x - 0.5); int y_range = int(f_end.y - f_beg.y - 0.5); // Accumulate top and bottom edge pixels. - for (int ix = 0; ix < x_range; ++ix) + for (int x = f_beg.x + 1; x <= f_beg.x + x_range; ++x) { - float x = f_beg.x + 1.0 + float(ix); - avg_color += area_n * QuickSampleByPixel(vec2(x, f_beg.y) + offset); - avg_color += area_s * QuickSampleByPixel(vec2(x, f_end.y) + offset); + avg_color += area_n * texelFetch(Source, ivec2(x, f_beg.y), 0); + avg_color += area_s * texelFetch(Source, ivec2(x, f_end.y), 0); } // Accumulate left and right edge pixels and all the pixels in between. - for (int iy = 0; iy < y_range; ++iy) + for (int y = f_beg.y + 1; y <= f_beg.y + y_range; ++y) { - float y = f_beg.y + 1.0 + float(iy); + avg_color += area_w * texelFetch(Source, ivec2(f_beg.x, y), 0); + avg_color += area_e * texelFetch(Source, ivec2(f_end.x, y), 0); - avg_color += area_w * QuickSampleByPixel(vec2(f_beg.x, y) + offset); - avg_color += area_e * QuickSampleByPixel(vec2(f_end.x, y) + offset); - - for (int ix = 0; ix < x_range; ++ix) + for (int x = f_beg.x + 1; x <= f_beg.x + x_range; ++x) { - float x = f_beg.x + 1.0 + float(ix); - avg_color += QuickSampleByPixel(vec2(x, y) + offset); + avg_color += texelFetch(Source, ivec2(x, y), 0); } } diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/AreaScaling.glsl b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/AreaScaling.glsl index 83f4246ed0..eb3290cecb 100644 --- a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/AreaScaling.glsl +++ b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/AreaScaling.glsl @@ -17,49 +17,6 @@ layout( binding = 2 ) uniform dimensions{ float scaleY; }; -vec2 GetResolution() -{ - return vec2(abs(srcX1 - srcX0), abs(srcY1 - srcY0)); -} - -vec2 GetInvResolution() -{ - return vec2(1.0) / GetResolution(); -} - -vec2 GetWindowResolution() -{ - return vec2(abs(dstX1 - dstX0), abs(dstY1 - dstY0)); -} - -vec2 GetInvWindowResolution() -{ - return vec2(1.0) / GetWindowResolution(); -} - -/***** COLOR SAMPLING *****/ - -// Non filtered sample (nearest neighbor) -vec4 QuickSample(vec2 uv) -{ -#if 0 // Test sampling range - const float threshold = 0.00000001; - vec2 xy = uv.xy * GetResolution(); - // Sampling outside the valid range, draw in yellow - if (xy.x < (srcX0 - threshold) || xy.x >(srcX1 + threshold) || xy.y < (srcY0 - threshold) || xy.y >(srcY1 + threshold)) - return vec4(1.0, 1.0, 0.0, 1); - // Sampling at the edges, draw in purple - if (xy.x < srcX0 + 1.0 || xy.x >(srcX1 - 1.0) || xy.y < srcY0 + 1.0 || xy.y >(srcY1 - 1.0)) - return vec4(0.5, 0, 0.5, 1); -#endif - return texture(Source, uv); -} -vec4 QuickSampleByPixel(vec2 xy) -{ - vec2 uv = vec2(xy * GetInvResolution()); - return QuickSample(uv); -} - /***** Area Sampling *****/ // By Sam Belliveau and Filippo Tarpini. Public Domain license. @@ -71,9 +28,9 @@ vec4 QuickSampleByPixel(vec2 xy) vec4 AreaSampling(vec2 xy) { // Determine the sizes of the source and target images. - vec2 source_size = GetResolution(); - vec2 target_size = GetWindowResolution(); - vec2 inverted_target_size = GetInvWindowResolution(); + vec2 source_size = vec2(abs(srcX1 - srcX0), abs(srcY1 - srcY0)); + vec2 target_size = vec2(abs(dstX1 - dstX0), abs(dstY1 - dstY0)); + vec2 inverted_target_size = vec2(1.0) / target_size; // Compute the top-left and bottom-right corners of the target pixel box. vec2 t_beg = floor(xy - vec2(dstX0 < dstX1 ? dstX0 : dstX1, dstY0 < dstY1 ? dstY0 : dstY1)); @@ -84,8 +41,8 @@ vec4 AreaSampling(vec2 xy) vec2 end = t_end * inverted_target_size * source_size; // Compute the top-left and bottom-right corners of the pixel box. - vec2 f_beg = floor(beg); - vec2 f_end = floor(end); + ivec2 f_beg = ivec2(beg); + ivec2 f_end = ivec2(end); // Compute how much of the start and end pixels are covered horizontally & vertically. float area_w = 1.0 - fract(beg.x); @@ -102,39 +59,32 @@ vec4 AreaSampling(vec2 xy) // Initialize the color accumulator. vec4 avg_color = vec4(0.0, 0.0, 0.0, 0.0); - // Prevents rounding errors due to the coordinates flooring above - const vec2 offset = vec2(0.5, 0.5); - // Accumulate corner pixels. - avg_color += area_nw * QuickSampleByPixel(vec2(f_beg.x, f_beg.y) + offset); - avg_color += area_ne * QuickSampleByPixel(vec2(f_end.x, f_beg.y) + offset); - avg_color += area_sw * QuickSampleByPixel(vec2(f_beg.x, f_end.y) + offset); - avg_color += area_se * QuickSampleByPixel(vec2(f_end.x, f_end.y) + offset); + avg_color += area_nw * texelFetch(Source, ivec2(f_beg.x, f_beg.y), 0); + avg_color += area_ne * texelFetch(Source, ivec2(f_end.x, f_beg.y), 0); + avg_color += area_sw * texelFetch(Source, ivec2(f_beg.x, f_end.y), 0); + avg_color += area_se * texelFetch(Source, ivec2(f_end.x, f_end.y), 0); // Determine the size of the pixel box. int x_range = int(f_end.x - f_beg.x - 0.5); int y_range = int(f_end.y - f_beg.y - 0.5); // Accumulate top and bottom edge pixels. - for (int ix = 0; ix < x_range; ++ix) + for (int x = f_beg.x + 1; x <= f_beg.x + x_range; ++x) { - float x = f_beg.x + 1.0 + float(ix); - avg_color += area_n * QuickSampleByPixel(vec2(x, f_beg.y) + offset); - avg_color += area_s * QuickSampleByPixel(vec2(x, f_end.y) + offset); + avg_color += area_n * texelFetch(Source, ivec2(x, f_beg.y), 0); + avg_color += area_s * texelFetch(Source, ivec2(x, f_end.y), 0); } // Accumulate left and right edge pixels and all the pixels in between. - for (int iy = 0; iy < y_range; ++iy) + for (int y = f_beg.y + 1; y <= f_beg.y + y_range; ++y) { - float y = f_beg.y + 1.0 + float(iy); + avg_color += area_w * texelFetch(Source, ivec2(f_beg.x, y), 0); + avg_color += area_e * texelFetch(Source, ivec2(f_end.x, y), 0); - avg_color += area_w * QuickSampleByPixel(vec2(f_beg.x, y) + offset); - avg_color += area_e * QuickSampleByPixel(vec2(f_end.x, y) + offset); - - for (int ix = 0; ix < x_range; ++ix) + for (int x = f_beg.x + 1; x <= f_beg.x + x_range; ++x) { - float x = f_beg.x + 1.0 + float(ix); - avg_color += QuickSampleByPixel(vec2(x, y) + offset); + avg_color += texelFetch(Source, ivec2(x, y), 0); } } diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/AreaScaling.spv b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/AreaScaling.spv index 2f92113f74b106046cf97c95af775c99f4a76062..a06e753781bab748c7299ea21c87f1a84638251d 100644 GIT binary patch literal 12380 zcmZQ(Qf6mhU}WHC;AK!_VgLg{1||kZ1_lOh1~xG5?i1{zS6q^!XJTpqQozT+45Gkt z{0wXitPBhcEDQ_`xrv#1Ac?<>3=9GcY+x}#1`r#9L1r>DFf)LxS;q*{!^Pn46YuWt z@9yIopIlH7UtCg|lNz6snU@-$l3A3RT#{Lq3R1^`q%JcrIj1xQSph2p3)n80{j3b^ z3_J`B430&qiNT4v1v!~{=^ABeM%D}r46F>y4BQM13>B3iHV1U+u3i68?SQ%IuBp4VNit|g0l2hZ0Gpka;Vv-CD3@Mqpsd>ej z`FX_*;7|pLfx@%6C^-TY@Su`U#XNSX7_vGJkYAD$b5bJ^ZsCN9MMC^84e|sy%vc%pL1C0wl%84w z^E(HFBRCGqQj1DbQ{qdIB*A)E7~DX5;*(O-q5MoRKQ%7}%x7lE0mVydIyi2N7#J8p zGN3e3#lXOjhNQ0+%m>MW+{MDsz`($eSd^L=Uk>57!}xg+elLul3gJ(L@rxn+sW5(C zIRnVPnQ%TRPFWb{!uZ7y`Gs&kNS>8p8Hit&9-o|_lMiw?$c(iN3=F~Gv;nc>9w@EG z7bWJUgY>X4ykKBpsD$&G89svKq2VCG$iPquW!o|`FjRoqYzz^MP=6-p7v-fE6@#J! zlnxVM;;AX=sbD{`GUUO;l2h|aQj5T0TF1!10Mf+Dz{b$ah?K@t0`ei^EDZY?QS-(f zkUDtYU}pHm$iR@3pUl9@z`+12AJTK;-E;Dj5_3HB%JP#FOEUBGJYB%T92p!^0B1C>26F_0dR7^v)kiGlJP zNK6FD4p4priGj)nm^x5?1BuBXsRP*u5>r4D1LZf6mUR61IUjcKFEFr1{Ma8|3G3O43Y==4I~c= zM@0q}1}+8$29Ov?ogxDp10T2yWB}^}iGeUk9Vl!;W`Ou|3_M`>fXpEVgWMqn4I@yR zux4Ok0P#U#338_>0}BHvJV1T`g_AavFUtU`Nf{VG`sJYdLH5DqVc`Vht1&QuYf>0r z6RHQs2bJZZv;yNBL+e=>--3Yw9DXo9sD6Xl2cn%ok;2emufqVU5g8bq85kHqVjwKS zz{KFg0CB4y)Etl=P?&?t4Uqppeg*jjRHlH!5+n}tdm~sM*l-4r-5`DwgbU|)Ace(J zsQI9H1E~kuvmB~khk+d&pDP#`7(imkSdxL0VHGrfLE#M50;XVmX$EG7)eH<^bs)E{ z0n0Lg#X#`};)BvONEjr)9_kj5|6p!;3X)`Cum}15HzNZB$UmU;4O0Jy5fY~$agcc+ zJ}6Is><7vJgUW;SfZQv{1gZxidLYI^NKjaU(lAJm5QxLTzyMMUG84oH=4N!}uWmpt2j84=TGsd`AXW22ftx&&U8S>p*$LmVpIa#vEW|U;v53)EtDW z0p%;0nnR2X3?T6x46NY%aF~&S0i+Kk{+)rD;TR~Q85kITF|aUzGA;S1vbWMBfD3DOTT2d4iNR6nxYPBStvfW%??L40KW zAh*H%afXorYzNG|vrzLuWdcmiIYtHskT^`ud8istIRaC2fsug$Bn}IYi;N5mAblWl zPu>(KB3v0-Lkg_@0Q|20Mi29P+&KcMsl;)DDHG7}UYAah{)Z$R}U zyX__;0|Q7LrXR#d)(E|IM0|Q7ONE{SiPmsb3B=?w+f#Eqcyg+Q2*-xQn zBisLsk%0ju4stIjzk&E5|ANc}g%`*inEn?~{m5>6$;iL}5{Kyr@sag|%z=g1D@Fze zkR34dUPH}84*xfd3=AM~n3}gxHOO)Nj*)=@Bn}IY_lyh-AblWlPoe}n2rcH4JG1_qEgOh1T^ ztRG|!EIfWNGBAMbfTf?Gj0_B*Fan8#^n=PnkQm55P#y4}kpbMU1=WKfGeG4us1OE; z*Ff6`jG%I#iGcy8hlvTI2UKRm^e{6qFo48idRUkk7(ikmcYx|&Rwf1pkU1c6kUK!- zEJzHbA0*Gl!~kw<=R@08?4bIHiGcy67F-51F))DCg33-VCI)c39V7=Tk3oEx91jx% z1IUjcIZ)XQ;#)GXgUeN3CI)c78Dt(vA4q-+H2w22F))DSLG?H&z6C&jhL)coIet*x z#RMroL2QtHpn6b*i2>Y>2k}Av5QX|9nSq4?BnRSy>cqbcjNq~tWG+mtBvdWT&r(bb z3?MO(+d=gPh;PZj3T|h}Ffo9~20-ET@IWL49sy zKB&))%m?+kk@=uLH!>g8=SJp(`rOETP@fyd2em6eeQp>Z)D{HwuaWtn{xvcm)W1gN zgZkLWd{93d#s~Qi)YnGlgZkLWd{F-y#0QlDpmvKq1Ek#o>N|ka2&iog>Q{ioVQN7A zEl}SB)PI1f0rmGl;>`@8UO58;sP6~T2NLIDU<2nY$iI3F3=AOmfaF1W z2b69>VlewaV@e=DBG-RL3=9k)ahMttsGE@MI8z1&29P++O=b)X3?O|VahRLTk=z85 zhnZ&yH4nLtv0`9g0ExrY*g(}F*CVzJ3=AM~n4NYE3=ANBAaR(T_DFVuyRk1BrvwgYq*-45r>4JWdO#Q$Xp-1L|Jn zF!BVC6(H1jLDe9KwKoF;14ta!?(=0}U;ya@iG#}VKu}=>EyqD}0SpWbAqMLFou22TH#nu?%SX1(o?A zd5~S8x&tH*G8?2Oi-7?=HrohItJw?;3?MPkI0HyMsO|uX!PJAs^FU_6_@K!GPdyzwAuup7fW%>H3K$r`1ypx5L(PS?AwlW6g@J(qBo2xf5Wf`~FQ9e- zL=IB^w?W5tK=L57LG>(13}$vG)NEK=6J%Bw0|Ns{9Aw%gLk_VX$ zsH1<4$c z9LO9{8v!H^3UiQqr!g=vfZPbEsO-+;!_U}mjkU|<0G55xzJldWT5U;vGWfx;Zr z{@K95zyM0Ku($(_i*00JU;v53)NF#PfyE0*&1MD$29P+YTmiM$w=gg;fb@aHQy5sl z<8fOV7#KkI#Y4yVwlOd;fW$y*LEQpSd!`TCF5k|;zyOj5xfkSCP#bs$0|R)D1XR9& z_&XUG7(ikmwJ^Ve#6apn@w$tFfdLd3AU?>iyV3l*hk=0sl;@EBx|e~00VEFd>pm=g z-Os?l01}7!^#B6{14ti89Ol=93=9k)`(S=O#K6D+5(BA4_v>K>1_qEk$h{!9g8X&_ z&96rp7#KieAhj^Rg2X`TL4G~PzyO|K0`Wn9J&xwr6HtF3`}HIP0|Q7L=GRlu@Ia2& z(+ms@AaR&q&oD4Bfb@aHVSYW!z`y{q59Zf%3=9k)F_2nxzn*7cU;xR3+zWCm$gdaB z{CbgrfdM22QVa7dNDQPNS?Bo6cI4F(1VkUo$&%&#{Y7#KkI!TfrQfq?-e22zXe*V_yX3?O-sdqHjm`SlK( zU+*$7Fo48BYGHl_iNV}?AL>qIzdT@IU;v53)I7uzw~rVY7(n7p46NX}iN_2K3?O|V zaZtY)6pl|A7#Kj}uyXww0|Ns{3^e`?s-Hml`Z)sw14ta?Hc;OP#E04Y66!YOFnq

O-u|RHpo0soPri%!0I|s-e6>el?K-BnDE8p1*||85ltFAh&_s z3G$Z+n!iLD85lrfAhj@mfy6**=m#_nfoPC^Q2dBN{Q*l4;*1OoATi`L3K9e92c=C= z{{WOWVQEehOMFT(GBAL|VP;A*GBAL|U}l2GFhF7;Gi9J-uOJ$f?&TO6800~5$H2e< zl2?GrgJ_tW6&V>AKxV+)poGN@%8U#QAaR%;70_5TxE%y3pHvwc7(i-3;;?kD#>l_` zG8-fZO7|eXEF%L0$W0)BgV-SRKZB1DZZyZqkIh3AsGbVq{4}C@k29P|+Z6G^9>B9gm zeHbz_Fo48BYGLUEBnDCs@|O`Kq^$|!gZyQT<}VXyI!E@GDI)^|NF1aGe3=AM~m>y5i{0&k%^*r% zor3sIj0_AQH-XYAhz&9i6n{R@_yh4lZt{h?3C8zBiz|Od1_qECkT@)^0vH(>K<2>W zDv*(Z0VD=e3yUj|d7yX+LW`GRMg|6u7)TE+UO-}?Fa?Dl$h{yMq#xwI5UBfLaT3bN zzyK0MjuVg=NIxhpK>9)c2Ju1V28a)$VdjNF{RcBIoRNV6BnC4NRE~heK>9&t5J*2L zzk~FH@pbXNdm2t0)-WHt~r?zwa=LXt-oOAr!q1y zfYgA*Vg1N7(3&Ym1_qEE%${^;ISI2TgOPy&BnFZP`8N}(+yn8mpydV3o@_=229O$% zILw|LMg|6uIUqThx?DyE29W(AIgo!rK$iM&+gViY@zqd0oFo48iYC525VC^fAnodRr29P)`&bvT) zfRTX#B>sF+)PFWh6T|248(?+JsWB^vi)-y85ls~AT^+{nF}fp7#SFl!(tw&JY)oo zBSOXs=QA=efb@aHLH-1VFGvhj27}xJiVu+eFf$ioF>?{9`~=nC49pB5{UAQFevmn^ zFj~yW09FGtZwZolAp1dlkQ$IYD4dr<{fRtgzKoH90VEDnvmB}hd0c!2BLf3S95!aZ z5>&=8GBAL|LE*dxDV#xas~H&>wn4)g#D9%9Abp_t*#z}Fays11$iM&+2jy2#n%ct1zyQ(<5{HRxWn^FgiG$XGfx--w z2S8$=FbDYy6lWm6!0g$M#hx9E3=AM~n0^o+SwF}eSe)%-WMBZ<0W)tGl6fHgAU;S9 SNIfX;?1svN@;!(R%J%@h;?7?H literal 13560 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 CvqqQz