From de77b764a7ed226075de16524528f30862c7fe05 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Wed, 20 Nov 2024 20:50:45 +1000 Subject: [PATCH] GPU/TextureCache: Add texture scaling feature --- .../shaders/system/texscale-mmpx.comp | 124 +++++++++ .../shaders/system/texscale-scale2x.comp | 55 ++++ .../shaders/system/texscale-xbr.frag | 248 +++++++++++++++++ data/resources/shaders/system/texscale.vert | 14 + src/core/fullscreen_ui.cpp | 14 + src/core/gpu_hw_texture_cache.cpp | 256 +++++++++++++++++- src/core/settings.cpp | 49 ++++ src/core/settings.h | 5 + src/core/system.cpp | 3 +- src/core/types.h | 14 +- src/duckstation-qt/graphicssettingswidget.cpp | 7 + src/duckstation-qt/graphicssettingswidget.ui | 44 +-- src/util/vulkan_device.cpp | 27 +- src/util/vulkan_device.h | 2 + 14 files changed, 827 insertions(+), 35 deletions(-) create mode 100644 data/resources/shaders/system/texscale-mmpx.comp create mode 100644 data/resources/shaders/system/texscale-scale2x.comp create mode 100644 data/resources/shaders/system/texscale-xbr.frag create mode 100644 data/resources/shaders/system/texscale.vert diff --git a/data/resources/shaders/system/texscale-mmpx.comp b/data/resources/shaders/system/texscale-mmpx.comp new file mode 100644 index 000000000..9eff48f3e --- /dev/null +++ b/data/resources/shaders/system/texscale-mmpx.comp @@ -0,0 +1,124 @@ +#version 460 core + +// EPX.glc +// Copyright 2020 Morgan McGuire & Mara Gagiu, +// provided under the Open Source MIT license https://opensource.org/licenses/MIT + +// Implementation of Eric Johnston and Andrea Mazzoleni's +// EPX aka Scale2X algorithm based on https://www.scale2x.it/algorithm + +#define ABGR8 uint + +UNIFORM_BLOCK_LAYOUT uniform UBOBlock { + ivec2 src_size; + ivec2 dst_size; +}; + +TEXTURE_LAYOUT(0) uniform sampler2D samp0; +IMAGE_LAYOUT(0, rgba8) uniform restrict writeonly image2D dst_image; + +ABGR8 src(int x, int y) { + return packUnorm4x8(texelFetch(samp0, ivec2(x, y), 0)); +} + +void dst(int x, int y, ABGR8 value) { + imageStore(dst_image, ivec2(x, y), unpackUnorm4x8(value)); +} + +uint luma(ABGR8 C) { + uint alpha = (C & 0xFF000000u) >> 24; + return (((C & 0x00FF0000u) >> 16) + ((C & 0x0000FF00u) >> 8) + (C & 0x000000FFu) + 1u) * (256u - alpha); +} + +bool all_eq2(ABGR8 B, ABGR8 A0, ABGR8 A1) { + return ((B ^ A0) | (B ^ A1)) == 0u; +} + +bool all_eq3(ABGR8 B, ABGR8 A0, ABGR8 A1, ABGR8 A2) { + return ((B ^ A0) | (B ^ A1) | (B ^ A2)) == 0u; +} + +bool all_eq4(ABGR8 B, ABGR8 A0, ABGR8 A1, ABGR8 A2, ABGR8 A3) { + return ((B ^ A0) | (B ^ A1) | (B ^ A2) | (B ^ A3)) == 0u; +} + +bool any_eq3(ABGR8 B, ABGR8 A0, ABGR8 A1, ABGR8 A2) { + return B == A0 || B == A1 || B == A2; +} + +bool none_eq2(ABGR8 B, ABGR8 A0, ABGR8 A1) { + return (B != A0) && (B != A1); +} + +bool none_eq4(ABGR8 B, ABGR8 A0, ABGR8 A1, ABGR8 A2, ABGR8 A3) { + return B != A0 && B != A1 && B != A2 && B != A3; +} + +layout(local_size_x = 8, local_size_y = 8) in; + +void main () { + // EPX first falls back to Nearest Neighbour + int srcX = int(gl_GlobalInvocationID.x); + int srcY = int(gl_GlobalInvocationID.y); + if (srcX >= src_size.x || srcY >= src_size.y) + return; + + ABGR8 A = src(srcX - 1, srcY - 1), B = src(srcX, srcY - 1), C = src(srcX + 1, srcY - 1); + ABGR8 D = src(srcX - 1, srcY + 0), E = src(srcX, srcY + 0), F = src(srcX + 1, srcY + 0); + ABGR8 G = src(srcX - 1, srcY + 1), H = src(srcX, srcY + 1), I = src(srcX + 1, srcY + 1); + + ABGR8 J = E, K = E, L = E, M = E; + + if (((A ^ E) | (B ^ E) | (C ^ E) | (D ^ E) | (F ^ E) | (G ^ E) | (H ^ E) | (I ^ E)) != 0u) { + ABGR8 P = src(srcX, srcY - 2), S = src(srcX, srcY + 2); + ABGR8 Q = src(srcX - 2, srcY), R = src(srcX + 2, srcY); + ABGR8 Bl = luma(B), Dl = luma(D), El = luma(E), Fl = luma(F), Hl = luma(H); + + // 1:1 slope rules + if ((D == B && D != H && D != F) && (El >= Dl || E == A) && any_eq3(E, A, C, G) && ((El < Dl) || A != D || E != P || E != Q)) J = D; + if ((B == F && B != D && B != H) && (El >= Bl || E == C) && any_eq3(E, A, C, I) && ((El < Bl) || C != B || E != P || E != R)) K = B; + if ((H == D && H != F && H != B) && (El >= Hl || E == G) && any_eq3(E, A, G, I) && ((El < Hl) || G != H || E != S || E != Q)) L = H; + if ((F == H && F != B && F != D) && (El >= Fl || E == I) && any_eq3(E, C, G, I) && ((El < Fl) || I != H || E != R || E != S)) M = F; + + // Intersection rules + if ((E != F && all_eq4(E, C, I, D, Q) && all_eq2(F, B, H)) && (F != src(srcX + 3, srcY))) K = M = F; + if ((E != D && all_eq4(E, A, G, F, R) && all_eq2(D, B, H)) && (D != src(srcX - 3, srcY))) J = L = D; + if ((E != H && all_eq4(E, G, I, B, P) && all_eq2(H, D, F)) && (H != src(srcX, srcY + 3))) L = M = H; + if ((E != B && all_eq4(E, A, C, H, S) && all_eq2(B, D, F)) && (B != src(srcX, srcY - 3))) J = K = B; + if (Bl < El && all_eq4(E, G, H, I, S) && none_eq4(E, A, D, C, F)) J = K = B; + if (Hl < El && all_eq4(E, A, B, C, P) && none_eq4(E, D, G, I, F)) L = M = H; + if (Fl < El && all_eq4(E, A, D, G, Q) && none_eq4(E, B, C, I, H)) K = M = F; + if (Dl < El && all_eq4(E, C, F, I, R) && none_eq4(E, B, A, G, H)) J = L = D; + + // 2:1 slope rules + if (H != B) { + if (H != A && H != E && H != C) { + if (all_eq3(H, G, F, R) && none_eq2(H, D, src(srcX + 2, srcY - 1))) L = M; + if (all_eq3(H, I, D, Q) && none_eq2(H, F, src(srcX - 2, srcY - 1))) M = L; + } + + if (B != I && B != G && B != E) { + if (all_eq3(B, A, F, R) && none_eq2(B, D, src(srcX + 2, srcY + 1))) J = K; + if (all_eq3(B, C, D, Q) && none_eq2(B, F, src(srcX - 2, srcY + 1))) K = J; + } + } // H !== B + + if (F != D) { + if (D != I && D != E && D != C) { + if (all_eq3(D, A, H, S) && none_eq2(D, B, src(srcX + 1, srcY + 2))) J = L; + if (all_eq3(D, G, B, P) && none_eq2(D, H, src(srcX + 1, srcY - 2))) L = J; + } + + if (F != E && F != A && F != G) { + if (all_eq3(F, C, H, S) && none_eq2(F, B, src(srcX - 1, srcY + 2))) K = M; + if (all_eq3(F, I, B, P) && none_eq2(F, H, src(srcX - 1, srcY - 2))) M = K; + } + } // F !== D + } // not constant + + // Write four pixels at once + dst(srcX * 2, srcY * 2, J); + dst(srcX * 2 + 1, srcY * 2, K); + dst(srcX * 2, srcY * 2 + 1, L); + dst(srcX * 2 + 1, srcY * 2 + 1, M); +} diff --git a/data/resources/shaders/system/texscale-scale2x.comp b/data/resources/shaders/system/texscale-scale2x.comp new file mode 100644 index 000000000..bf6cb2021 --- /dev/null +++ b/data/resources/shaders/system/texscale-scale2x.comp @@ -0,0 +1,55 @@ +#version 460 core + +// EPX.glc +// Copyright 2020 Morgan McGuire & Mara Gagiu, +// provided under the Open Source MIT license https://opensource.org/licenses/MIT + +// Implementation of Eric Johnston and Andrea Mazzoleni's +// EPX aka Scale2X algorithm based on https://www.scale2x.it/algorithm + +#define ABGR8 uint + +UNIFORM_BLOCK_LAYOUT uniform UBOBlock { + ivec2 src_size; + ivec2 dst_size; +}; + +TEXTURE_LAYOUT(0) uniform sampler2D samp0; +IMAGE_LAYOUT(0, rgba8) uniform restrict writeonly image2D dst_image; + +ABGR8 src(int x, int y) { + return packUnorm4x8(texelFetch(samp0, ivec2(x, y), 0)); +} + +void dst(int x, int y, ABGR8 value) { + imageStore(dst_image, ivec2(x, y), unpackUnorm4x8(value)); +} + +layout(local_size_x = 8, local_size_y = 8) in; + +void main () { + // EPX first falls back to Nearest Neighbour + int srcX = int(gl_GlobalInvocationID.x); + int srcY = int(gl_GlobalInvocationID.y); + if (srcX >= src_size.x || srcY >= src_size.y) + return; + + ABGR8 E = src(srcX, srcY); + ABGR8 J = E, K = E, L = E, M = E; + + ABGR8 B = src(srcX + 0, srcY - 1); + ABGR8 D = src(srcX - 1, srcY + 0); + ABGR8 F = src(srcX + 1, srcY + 0); + ABGR8 H = src(srcX + 0, srcY + 1); + + if (D == B && B != F && D != H) J = D; + if (B == F && D != F && H != F) K = F; + if (H == D && F != D && B != D) L = D; + if (H == F && D != H && B != F) M = F; + + // Write four pixels at once + dst(srcX * 2, srcY * 2, J); + dst(srcX * 2 + 1, srcY * 2, K); + dst(srcX * 2, srcY * 2 + 1, L); + dst(srcX * 2 + 1, srcY * 2 + 1, M); +} diff --git a/data/resources/shaders/system/texscale-xbr.frag b/data/resources/shaders/system/texscale-xbr.frag new file mode 100644 index 000000000..ed85faea4 --- /dev/null +++ b/data/resources/shaders/system/texscale-xbr.frag @@ -0,0 +1,248 @@ +#version 460 core + +layout(location = 0) in VertexData { + vec2 v_tex0; +}; + +layout(location = 0) out vec4 dest; + +TEXTURE_LAYOUT(0) uniform sampler2D samp0; + +vec4 SrcGet(vec2 uv) +{ + return texelFetch(samp0, ivec2(uv), 0); +} + +// XBR.pix +// Copyright 2020 Morgan McGuire & Mara Gagiu, +// provided under the Open Source MIT license https://opensource.org/licenses/MIT + +#define XBR_Y_WEIGHT 48.0 +#define XBR_EQ_THRESHOLD 15.0 +#define XBR_LV1_COEFFICIENT 0.5 +#define XBR_LV2_COEFFICIENT 2.0 +// END PARAMETERS // + + +// XBR GLSL implementation source: +// https://github.com/libretro/glsl-shaders/blob/master/xbr/shaders/xbr-lv2.glsl +/* + Hyllian's xBR-lv2 Shader + Copyright (C) 2011-2015 Hyllian - sergiogdb@gmail.com + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + + Incorporates some of the ideas from SABR shader. Thanks to Joshua Street. + +*/ + +// Uncomment just one of the three params below to choose the corner detection +#define CORNER_A +//#define CORNER_B +//#define CORNER_C +//#define CORNER_D + +#ifndef CORNER_A + #define SMOOTH_TIPS +#endif + +#define XBR_SCALE 2.0 +#define lv2_cf XBR_LV2_COEFFICIENT +//================================================================================= +// XBR Helper Functions +//================================================================================= +const float coef = 2.0; +const vec3 rgbw = vec3(14.352, 28.176, 5.472); +const vec4 eq_threshold = vec4(15.0, 15.0, 15.0, 15.0); + +const vec4 delta = vec4(1.0/XBR_SCALE, 1.0/XBR_SCALE, 1.0/XBR_SCALE, 1.0/XBR_SCALE); +const vec4 delta_l = vec4(0.5/XBR_SCALE, 1.0/XBR_SCALE, 0.5/XBR_SCALE, 1.0/XBR_SCALE); +const vec4 delta_u = delta_l.yxwz; + +const vec4 Ao = vec4( 1.0, -1.0, -1.0, 1.0 ); +const vec4 Bo = vec4( 1.0, 1.0, -1.0,-1.0 ); +const vec4 Co = vec4( 1.5, 0.5, -0.5, 0.5 ); +const vec4 Ax = vec4( 1.0, -1.0, -1.0, 1.0 ); +const vec4 Bx = vec4( 0.5, 2.0, -0.5,-2.0 ); +const vec4 Cx = vec4( 1.0, 1.0, -0.5, 0.0 ); +const vec4 Ay = vec4( 1.0, -1.0, -1.0, 1.0 ); +const vec4 By = vec4( 2.0, 0.5, -2.0,-0.5 ); +const vec4 Cy = vec4( 2.0, 0.0, -1.0, 0.5 ); +const vec4 Ci = vec4(0.25, 0.25, 0.25, 0.25); + +// Difference between vector components. +vec4 df(vec4 A, vec4 B) +{ + return vec4(abs(A-B)); +} + +// Compare two vectors and return their components are different. +vec4 diff(vec4 A, vec4 B) +{ + return vec4(notEqual(A, B)); +} + +// Determine if two vector components are equal based on a threshold. +vec4 eq(vec4 A, vec4 B) +{ + return (step(df(A, B), vec4(XBR_EQ_THRESHOLD))); +} + +// Determine if two vector components are NOT equal based on a threshold. +vec4 neq(vec4 A, vec4 B) +{ + return (vec4(1.0, 1.0, 1.0, 1.0) - eq(A, B)); +} + +// Weighted distance. +vec4 wd(vec4 a, vec4 b, vec4 c, vec4 d, vec4 e, vec4 f, vec4 g, vec4 h) +{ + return (df(a,b) + df(a,c) + df(d,e) + df(d,f) + 4.0*df(g,h)); +} + +float c_df(vec3 c1, vec3 c2) +{ + vec3 df = abs(c1 - c2); + return df.r + df.g + df.b; +} + +vec4 XBR() +{ + vec4 proxy_dest = vec4(0, 0, 0, 1); + ivec2 tex_fetch_coords = ivec2(gl_FragCoord.xy / 2.0); + ivec2 tex_coords = ivec2(gl_FragCoord.xy); + + + + vec4 edri, edr, edr_l, edr_u, px; // px = pixel, edr = edge detection rule + vec4 irlv0, irlv1, irlv2l, irlv2u, block_3d; + vec4 fx, fx_l, fx_u; // inequations of straight lines. + + vec2 fp = fract(gl_FragCoord.xy / 2.0); + + vec3 A1 = SrcGet(tex_fetch_coords + ivec2(-1, -2)).xyz; + vec3 B1 = SrcGet(tex_fetch_coords + ivec2( 0, -2)).xyz; + vec3 C1 = SrcGet(tex_fetch_coords + ivec2(+1, -2)).xyz; + vec3 A = SrcGet(tex_fetch_coords + ivec2(-1, -1)).xyz; + vec3 B = SrcGet(tex_fetch_coords + ivec2( 0, -1)).xyz; + vec3 C = SrcGet(tex_fetch_coords + ivec2(+1, -1)).xyz; + vec3 D = SrcGet(tex_fetch_coords + ivec2(-1, 0)).xyz; + vec4 Eo = SrcGet(tex_fetch_coords); + vec3 E = Eo.xyz; + vec3 F = SrcGet(tex_fetch_coords + ivec2(+1, 0)).xyz; + vec3 G = SrcGet(tex_fetch_coords + ivec2(-1, +1)).xyz; + vec3 H = SrcGet(tex_fetch_coords + ivec2( 0, +1)).xyz; + vec3 I = SrcGet(tex_fetch_coords + ivec2(+1, +1)).xyz; + vec3 G5 = SrcGet(tex_fetch_coords + ivec2(-1, +2)).xyz; + vec3 H5 = SrcGet(tex_fetch_coords + ivec2( 0, +2) ).xyz; + vec3 I5 = SrcGet(tex_fetch_coords + ivec2(+1, +2)).xyz; + vec3 A0 = SrcGet(tex_fetch_coords + ivec2(-2, -1)).xyz; + vec3 D0 = SrcGet(tex_fetch_coords + ivec2(-2, 0)).xyz; + vec3 G0 = SrcGet(tex_fetch_coords + ivec2(-2, +1)).xyz; + vec3 C4 = SrcGet(tex_fetch_coords + ivec2(+2, -1)).xyz; + vec3 F4 = SrcGet(tex_fetch_coords + ivec2(+2, 0)).xyz; + vec3 I4 = SrcGet(tex_fetch_coords + ivec2(+2, +1)).xyz; + + vec4 b = vec4(dot(B ,rgbw), dot(D ,rgbw), dot(H ,rgbw), dot(F ,rgbw)); + vec4 c = vec4(dot(C ,rgbw), dot(A ,rgbw), dot(G ,rgbw), dot(I ,rgbw)); + vec4 d = b.yzwx; + vec4 e = vec4(dot(E,rgbw)); + vec4 f = b.wxyz; + vec4 g = c.zwxy; + vec4 h = b.zwxy; + vec4 i = c.wxyz; + vec4 i4 = vec4(dot(I4,rgbw), dot(C1,rgbw), dot(A0,rgbw), dot(G5,rgbw)); + vec4 i5 = vec4(dot(I5,rgbw), dot(C4,rgbw), dot(A1,rgbw), dot(G0,rgbw)); + vec4 h5 = vec4(dot(H5,rgbw), dot(F4,rgbw), dot(B1,rgbw), dot(D0,rgbw)); + vec4 f4 = h5.yzwx; + + // These inequations define the line below which interpolation occurs. + fx = (Ao*fp.y+Bo*fp.x); + fx_l = (Ax*fp.y+Bx*fp.x); + fx_u = (Ay*fp.y+By*fp.x); + + irlv1 = irlv0 = diff(e,f) * diff(e,h); + +#ifdef CORNER_B + + // E1/K case (X odd, Y even) + irlv1 = (irlv0 * ( neq(f,b) * neq(h,d) + eq(e,i) * neq(f,i4) * neq(h,i5) + eq(e,g) + eq(e,c) ) ); + +#endif +#ifdef CORNER_D + + // E3/M case (X odd, Y odd) + vec4 c1 = i4.yzwx; + vec4 g0 = i5.wxyz; + irlv1 = (irlv0 * ( neq(f,b) * neq(h,d) + eq(e,i) * neq(f,i4) * neq(h,i5) + eq(e,g) + eq(e,c) ) * (diff(f,f4) * diff(f,i) + diff(h,h5) * diff(h,i) + diff(h,g) + diff(f,c) + eq(b,c1) * eq(d,g0))); + +#endif +#ifdef CORNER_C + + irlv1 = (irlv0 * ( neq(f,b) * neq(f,c) + neq(h,d) * neq(h,g) + eq(e,i) * (neq(f,f4) * neq(f,i4) + neq(h,h5) * neq(h,i5)) + eq(e,g) + eq(e,c)) ); + +#endif + + irlv2l = diff(e,g) * diff(d,g); + irlv2u = diff(e,c) * diff(b,c); + + vec4 fx45i = clamp((fx + delta -Co - Ci)/(2.0*delta ), 0.0, 1.0); + vec4 fx45 = clamp((fx + delta -Co )/(2.0*delta ), 0.0, 1.0); + vec4 fx30 = clamp((fx_l + delta_l -Cx )/(2.0*delta_l), 0.0, 1.0); + vec4 fx60 = clamp((fx_u + delta_u -Cy )/(2.0*delta_u), 0.0, 1.0); + + vec4 wd1 = wd( e, c, g, i, h5, f4, h, f); + vec4 wd2 = wd( h, d, i5, f, i4, b, e, i); + + edri = step(wd1, wd2) * irlv0; + edr = step(wd1 + vec4(0.1, 0.1, 0.1, 0.1), wd2) * step(vec4(0.5, 0.5, 0.5, 0.5), irlv1); + edr_l = step( lv2_cf*df(f,g), df(h,c) ) * irlv2l * edr; + edr_u = step( lv2_cf*df(h,c), df(f,g) ) * irlv2u * edr; + + fx45 = edr * fx45; + fx30 = edr_l * fx30; + fx60 = edr_u * fx60; + fx45i = edri * fx45i; + + px = step(df(e,f), df(e,h)); + +#ifdef SMOOTH_TIPS + //vec4 maximos = max(max(fx30, fx60), max(fx45, fx45i)); +#endif +#ifndef SMOOTH_TIPS + vec4 maximos = max(max(fx30, fx60), fx45); +#endif + + vec3 res1 = E; + res1 = mix(res1, mix(H, F, px.x), maximos.x); + res1 = mix(res1, mix(B, D, px.z), maximos.z); + + vec3 res2 = E; + res2 = mix(res2, mix(F, B, px.y), maximos.y); + res2 = mix(res2, mix(D, H, px.w), maximos.w); + + vec3 res = mix(res1, res2, step(c_df(E, res1), c_df(E, res2))); + + proxy_dest.rgb = res; + proxy_dest.a = Eo.a; + return proxy_dest; +} + +void main () { + dest = XBR(); +} diff --git a/data/resources/shaders/system/texscale.vert b/data/resources/shaders/system/texscale.vert new file mode 100644 index 000000000..020014b7d --- /dev/null +++ b/data/resources/shaders/system/texscale.vert @@ -0,0 +1,14 @@ +#version 460 core + +layout(location = 0) out VertexData { + vec2 v_tex0; +}; + +void main() +{ + v_tex0 = vec2(float((gl_VertexIndex << 1) & 2), float(gl_VertexIndex & 2u)); + gl_Position = vec4(v_tex0 * vec2(2.0f, -2.0f) + vec2(-1.0f, 1.0f), 0.0f, 1.0f); + #if API_OPENGL || API_OPENGL_ES || API_VULKAN + gl_Position.y = -gl_Position.y; + #endif +} diff --git a/src/core/fullscreen_ui.cpp b/src/core/fullscreen_ui.cpp index 16e189aac..3360d00ac 100644 --- a/src/core/fullscreen_ui.cpp +++ b/src/core/fullscreen_ui.cpp @@ -4591,6 +4591,11 @@ void FullscreenUI::DrawGraphicsSettingsPage() "Hacks", "UseOldMDECRoutines", false); const bool texture_cache_enabled = GetEffectiveBoolSetting(bsi, "GPU", "EnableTextureCache", false); + DrawEnumSetting(bsi, FSUI_ICONSTR(ICON_FA_EXPAND_ALT, "Texture Scaling"), + FSUI_CSTR("Applies a texture scaling filter to textures as a pre-processing step."), "GPU", + "TextureScaling", GPUTextureScaling::Disabled, &Settings::ParseGPUTextureScalingName, + &Settings::GetGPUTextureScalingName, &Settings::GetGPUTextureScalingDisplayName, + GPUTextureScaling::MaxCount, texture_cache_enabled); DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_FILE_IMPORT, "Enable Texture Replacements"), FSUI_CSTR("Enables loading of replacement textures. Not compatible with all games."), "TextureReplacements", "EnableTextureReplacements", false, texture_cache_enabled); @@ -7686,6 +7691,7 @@ TRANSLATE_NOOP("FullscreenUI", "Allow Booting Without SBI File"); TRANSLATE_NOOP("FullscreenUI", "Allows loading protected games without subchannel information."); TRANSLATE_NOOP("FullscreenUI", "An error occurred while deleting empty game settings:\n{}"); TRANSLATE_NOOP("FullscreenUI", "An error occurred while saving game settings:\n{}"); +TRANSLATE_NOOP("FullscreenUI", "Applies a texture scaling filter to textures as a pre-processing step."); TRANSLATE_NOOP("FullscreenUI", "Apply Image Patches"); TRANSLATE_NOOP("FullscreenUI", "Are you sure you want to clear the current post-processing chain? All configuration will be lost."); TRANSLATE_NOOP("FullscreenUI", "Aspect Ratio"); @@ -7969,7 +7975,10 @@ TRANSLATE_NOOP("FullscreenUI", "Log To File"); TRANSLATE_NOOP("FullscreenUI", "Log To System Console"); TRANSLATE_NOOP("FullscreenUI", "Logging"); TRANSLATE_NOOP("FullscreenUI", "Logging Settings"); +TRANSLATE_NOOP("FullscreenUI", "Logging in to RetroAchievements..."); TRANSLATE_NOOP("FullscreenUI", "Login"); +TRANSLATE_NOOP("FullscreenUI", "Login Error"); +TRANSLATE_NOOP("FullscreenUI", "Login Failed.\nError: {}\nPlease check your username and password, and try again."); TRANSLATE_NOOP("FullscreenUI", "Login token generated on {}"); TRANSLATE_NOOP("FullscreenUI", "Logout"); TRANSLATE_NOOP("FullscreenUI", "Logs BIOS calls to printf(). Not all games contain debugging messages."); @@ -8026,6 +8035,7 @@ TRANSLATE_NOOP("FullscreenUI", "PGXP (Precision Geometry Transform Pipeline)"); TRANSLATE_NOOP("FullscreenUI", "PGXP Depth Buffer"); TRANSLATE_NOOP("FullscreenUI", "PGXP Geometry Correction"); TRANSLATE_NOOP("FullscreenUI", "Parent Directory"); +TRANSLATE_NOOP("FullscreenUI", "Password: "); TRANSLATE_NOOP("FullscreenUI", "Patches"); TRANSLATE_NOOP("FullscreenUI", "Patches the BIOS to skip the boot animation. Safe to enable."); TRANSLATE_NOOP("FullscreenUI", "Path"); @@ -8041,6 +8051,7 @@ TRANSLATE_NOOP("FullscreenUI", "Performance enhancement - jumps directly between TRANSLATE_NOOP("FullscreenUI", "Perspective Correct Colors"); TRANSLATE_NOOP("FullscreenUI", "Perspective Correct Textures"); TRANSLATE_NOOP("FullscreenUI", "Plays sound effects for events such as achievement unlocks and leaderboard submissions."); +TRANSLATE_NOOP("FullscreenUI", "Please enter your user name and password for retroachievements.org below. Your password will not be saved in DuckStation, an access token will be generated and used instead."); TRANSLATE_NOOP("FullscreenUI", "Port {} Controller Type"); TRANSLATE_NOOP("FullscreenUI", "Post-Processing Settings"); TRANSLATE_NOOP("FullscreenUI", "Post-processing chain cleared."); @@ -8088,6 +8099,7 @@ TRANSLATE_NOOP("FullscreenUI", "Resolution change will be applied after restarti TRANSLATE_NOOP("FullscreenUI", "Restores the state of the system prior to the last state loaded."); TRANSLATE_NOOP("FullscreenUI", "Resume Game"); TRANSLATE_NOOP("FullscreenUI", "Resume Last Session"); +TRANSLATE_NOOP("FullscreenUI", "RetroAchievements Login"); TRANSLATE_NOOP("FullscreenUI", "Return To Game"); TRANSLATE_NOOP("FullscreenUI", "Return to desktop mode, or exit the application."); TRANSLATE_NOOP("FullscreenUI", "Return to the previous menu."); @@ -8222,6 +8234,7 @@ TRANSLATE_NOOP("FullscreenUI", "Temporarily disables all enhancements, useful wh TRANSLATE_NOOP("FullscreenUI", "Test Unofficial Achievements"); TRANSLATE_NOOP("FullscreenUI", "Texture Filtering"); TRANSLATE_NOOP("FullscreenUI", "Texture Replacements"); +TRANSLATE_NOOP("FullscreenUI", "Texture Scaling"); TRANSLATE_NOOP("FullscreenUI", "Textures Directory"); TRANSLATE_NOOP("FullscreenUI", "The SDL input source supports most controllers."); TRANSLATE_NOOP("FullscreenUI", "The XInput source provides support for XBox 360/XBox One/XBox Series controllers."); @@ -8259,6 +8272,7 @@ TRANSLATE_NOOP("FullscreenUI", "Use Light Theme"); TRANSLATE_NOOP("FullscreenUI", "Use Old MDEC Routines"); TRANSLATE_NOOP("FullscreenUI", "Use Single Card For Multi-Disc Games"); TRANSLATE_NOOP("FullscreenUI", "Use Software Renderer For Readbacks"); +TRANSLATE_NOOP("FullscreenUI", "User Name: "); TRANSLATE_NOOP("FullscreenUI", "Username: {}"); TRANSLATE_NOOP("FullscreenUI", "Uses PGXP for all instructions, not just memory operations."); TRANSLATE_NOOP("FullscreenUI", "Uses a blit presentation model instead of flipping. This may be needed on some systems."); diff --git a/src/core/gpu_hw_texture_cache.cpp b/src/core/gpu_hw_texture_cache.cpp index 1efc0988d..cc09f8093 100644 --- a/src/core/gpu_hw_texture_cache.cpp +++ b/src/core/gpu_hw_texture_cache.cpp @@ -240,7 +240,8 @@ static bool ShouldTrackVRAMWrites(); static bool IsDumpingVRAMWriteTextures(); static void UpdateVRAMTrackingState(); -static bool CompilePipelines(); +static bool CompileReplacementPipelines(); +static bool CompileTextureScalingPipeline(); static void DestroyPipelines(); static const Source* ReturnSource(Source* source, const GSVector4i uv_rect, PaletteRecordFlags flags); @@ -284,6 +285,7 @@ static void DecodeTexture4(const u16* page, const u16* palette, u32 width, u32 h static void DecodeTexture8(const u16* page, const u16* palette, u32 width, u32 height, u32* dest, u32 dest_stride); static void DecodeTexture16(const u16* page, u32 width, u32 height, u32* dest, u32 dest_stride); static void DecodeTexture(u8 page, GPUTexturePaletteReg palette, GPUTextureMode mode, GPUTexture* texture); +static std::unique_ptr ScaleTexture(std::unique_ptr texture); static std::optional GetTextureReplacementTypeFromFileTitle(const std::string_view file_title); static bool HasValidReplacementExtension(const std::string_view path); @@ -501,6 +503,20 @@ ALWAYS_INLINE static float RectDistance(const GSVector4i& lhs, const GSVector4i& } namespace { +enum TextureScaler +{ + None, + XBR, +}; +struct TextureScalerInfo +{ + u32 scale; + u32 compute_local_size; + const char* vertex_shader_path; + const char* fragment_shader_path; + + ALWAYS_INLINE bool IsComputeShader() const { return (compute_local_size > 0); } +}; struct GPUTextureCacheState { Settings::TextureReplacementSettings::Configuration config; @@ -509,6 +525,7 @@ struct GPUTextureCacheState VRAMWrite* last_vram_write = nullptr; bool track_vram_writes = false; + const TextureScalerInfo* texture_scaler; HashCache hash_cache; /// List of candidates for purging when the hash cache gets too large. @@ -517,6 +534,8 @@ struct GPUTextureCacheState /// List of VRAM writes collected when saving state. std::vector temp_vram_write_list; + std::unique_ptr texture_scaler_pipeline; + std::unique_ptr replacement_texture_render_target; std::unique_ptr replacement_draw_pipeline; // copies alpha as-is std::unique_ptr replacement_semitransparent_draw_pipeline; // inverts alpha (i.e. semitransparent) @@ -540,6 +559,15 @@ struct GPUTextureCacheState ALIGN_TO_CACHE_LINE GPUTextureCacheState s_state; +static constexpr const TextureScalerInfo s_texture_scalers[] = { + {2, 0, "shaders/system/texscale.vert", "shaders/system/texscale-hq2x.frag"}, // HQ2x + {3, 0, "shaders/system/texscale.vert", "shaders/system/texscale-hq3x.frag"}, // HQ3x + {4, 0, "shaders/system/texscale.vert", "shaders/system/texscale-hq4x.frag"}, // HQ4x + {2, 8, nullptr, "shaders/system/texscale-mmpx.comp"}, // MMPX + {2, 8, "shaders/system/texscale.vert", "shaders/system/texscale-scale2x.comp"}, // Scale2x + {2, 0, "shaders/system/texscale.vert", "shaders/system/texscale-xbr.frag"}, // XBR +}; + } // namespace GPUTextureCache bool GPUTextureCache::ShouldTrackVRAMWrites() @@ -562,11 +590,18 @@ bool GPUTextureCache::IsDumpingVRAMWriteTextures() bool GPUTextureCache::Initialize() { + s_state.texture_scaler = (g_settings.gpu_texture_scaling == GPUTextureScaling::Disabled) ? + nullptr : + &s_texture_scalers[static_cast(g_settings.gpu_texture_scaling) - 1]; + LoadLocalConfiguration(false, false); UpdateVRAMTrackingState(); - if (!CompilePipelines()) + if (!CompileReplacementPipelines()) return false; + if (s_state.texture_scaler && !CompileTextureScalingPipeline()) [[unlikely]] + s_state.texture_scaler = nullptr; + return true; } @@ -582,11 +617,16 @@ void GPUTextureCache::UpdateSettings(bool use_texture_cache, const Settings& old Invalidate(); DestroyPipelines(); - if (!CompilePipelines()) [[unlikely]] + if (!CompileReplacementPipelines()) [[unlikely]] Panic("Failed to compile pipelines on TC settings change"); } } + const TextureScalerInfo* old_scaler = s_state.texture_scaler; + s_state.texture_scaler = (!use_texture_cache || g_settings.gpu_texture_scaling == GPUTextureScaling::Disabled) ? + nullptr : + &s_texture_scalers[static_cast(g_settings.gpu_texture_scaling) - 1]; + // Reload textures if configuration changes. const bool old_replacement_scale_linear_filter = s_state.config.replacement_scale_linear_filter; if (LoadLocalConfiguration(false, false) || @@ -599,13 +639,21 @@ void GPUTextureCache::UpdateSettings(bool use_texture_cache, const Settings& old { if (s_state.config.replacement_scale_linear_filter != old_replacement_scale_linear_filter) { - if (!CompilePipelines()) [[unlikely]] + if (!CompileReplacementPipelines()) [[unlikely]] Panic("Failed to compile pipelines on TC replacement settings change"); } } ReloadTextureReplacements(false); } + + if (use_texture_cache && s_state.texture_scaler != old_scaler) + { + if (!CompileTextureScalingPipeline()) [[unlikely]] + s_state.texture_scaler = nullptr; + + Invalidate(); + } } bool GPUTextureCache::DoState(StateWrapper& sw, bool skip) @@ -756,7 +804,7 @@ void GPUTextureCache::Shutdown() s_state.game_id = {}; } -bool GPUTextureCache::CompilePipelines() +bool GPUTextureCache::CompileReplacementPipelines() { if (!g_settings.texture_replacements.enable_texture_replacements) return true; @@ -807,6 +855,7 @@ bool GPUTextureCache::CompilePipelines() void GPUTextureCache::DestroyPipelines() { + s_state.texture_scaler_pipeline.reset(); s_state.replacement_draw_pipeline.reset(); s_state.replacement_semitransparent_draw_pipeline.reset(); } @@ -2057,6 +2106,8 @@ GPUTextureCache::HashCacheEntry* GPUTextureCache::LookupHashCache(SourceKey key, } DecodeTexture(key.page, key.palette, key.mode, entry.texture.get()); + if (s_state.texture_scaler) + entry.texture = ScaleTexture(std::move(entry.texture)); if (g_settings.texture_replacements.enable_texture_replacements) ApplyTextureReplacements(key, tex_hash, pal_hash, &entry); @@ -3340,4 +3391,197 @@ void GPUTextureCache::ApplyTextureReplacements(SourceKey key, HashType tex_hash, entry->texture = std::move(replacement_tex); g_gpu->RestoreDeviceContext(); -} \ No newline at end of file +} + +bool GPUTextureCache::CompileTextureScalingPipeline() +{ + s_state.texture_scaler_pipeline.reset(); + + const TextureScalerInfo* const info = s_state.texture_scaler; + if (!info) + return true; + + static constexpr auto add_defines = [](std::string& source) { + std::string::size_type pos = source.find("#version "); + if (pos == std::string::npos) + return; + + pos = source.find('\n', pos); + if (pos == std::string::npos) + return; + + const RenderAPI render_api = g_gpu_device->GetRenderAPI(); + const bool vulkan = (render_api == RenderAPI::Vulkan); + const std::string macros = + fmt::format("#define API_D3D11 {}\n" + "#define API_D3D12 {}\n" + "#define API_OPENGL {}\n" + "#define API_OPENGL_ES {}\n" + "#define API_VULKAN {}\n" + "#define API_METAL {}\n" + "#define UNIFORM_BLOCK_LAYOUT layout(push_constant)\n" + "#define TEXTURE_LAYOUT(index) layout(set = {}, binding = index)\n" + "#define IMAGE_LAYOUT(index, format) layout(set = {}, binding = index, format)\n", + BoolToUInt32(render_api == RenderAPI::D3D11), BoolToUInt32(render_api == RenderAPI::D3D12), + BoolToUInt32(render_api == RenderAPI::OpenGL), BoolToUInt32(render_api == RenderAPI::OpenGLES), + BoolToUInt32(render_api == RenderAPI::Vulkan), BoolToUInt32(render_api == RenderAPI::Metal), + vulkan ? 0 : 1, vulkan ? 1 : 2); + + source.insert(pos + 1, macros); + }; + + Error error; + std::optional source; + if (!info->IsComputeShader()) + { + source = Host::ReadResourceFileToString(info->vertex_shader_path, true, &error); + if (!source.has_value()) + { + ERROR_LOG("Failed to read scaling vertex shader '{}': {}", info->vertex_shader_path, error.GetDescription()); + return false; + } + + add_defines(source.value()); + + std::unique_ptr vertex_shader = + g_gpu_device->CreateShader(GPUShaderStage::Vertex, GPUShaderLanguage::GLSLVK, source.value(), &error); + if (!vertex_shader) + { + ERROR_LOG("Failed to compile scaling vertex shader '{}': {}", info->vertex_shader_path, error.GetDescription()); + return false; + } + + source = Host::ReadResourceFileToString(info->fragment_shader_path, true, &error); + if (!source.has_value()) + { + ERROR_LOG("Failed to read scaling fragment shader '{}': {}", info->fragment_shader_path, error.GetDescription()); + return false; + } + + add_defines(source.value()); + + std::unique_ptr fragment_shader = + g_gpu_device->CreateShader(GPUShaderStage::Fragment, GPUShaderLanguage::GLSLVK, source.value(), &error); + if (!fragment_shader) + { + ERROR_LOG("Failed to compile scaling fragment shader '{}': {}", info->fragment_shader_path, + error.GetDescription()); + return false; + } + + GPUPipeline::GraphicsConfig config; + config.layout = GPUPipeline::Layout::SingleTextureAndPushConstants; + config.primitive = GPUPipeline::Primitive::Triangles; + config.input_layout = {}; + config.rasterization = GPUPipeline::RasterizationState::GetNoCullState(); + config.depth = GPUPipeline::DepthState::GetNoTestsState(); + config.blend = GPUPipeline::BlendState::GetNoBlendingState(); + config.vertex_shader = vertex_shader.get(); + config.fragment_shader = fragment_shader.get(); + config.geometry_shader = nullptr; + config.SetTargetFormats(GPUTexture::Format::RGBA8); + config.samples = 1; + config.per_sample_shading = false; + config.render_pass_flags = GPUPipeline::NoRenderPassFlags; + + s_state.texture_scaler_pipeline = g_gpu_device->CreatePipeline(config, &error); + if (!s_state.texture_scaler_pipeline) + { + ERROR_LOG("Failed to compile scaling pipeline {}", error.GetDescription()); + return false; + } + } + else + { + source = Host::ReadResourceFileToString(info->fragment_shader_path, true, &error); + if (!source.has_value()) + { + ERROR_LOG("Failed to read scaling compute shader '{}': {}", info->fragment_shader_path, error.GetDescription()); + return false; + } + + add_defines(source.value()); + + std::unique_ptr compute_shader = + g_gpu_device->CreateShader(GPUShaderStage::Compute, GPUShaderLanguage::GLSLVK, source.value(), &error); + if (!compute_shader) + { + ERROR_LOG("Failed to compile scaling compute shader '{}': {}", info->fragment_shader_path, + error.GetDescription()); + return false; + } + + GPUPipeline::ComputeConfig config; + config.layout = GPUPipeline::Layout::ComputeSingleTextureAndPushConstants; + config.compute_shader = compute_shader.get(); + + s_state.texture_scaler_pipeline = g_gpu_device->CreatePipeline(config, &error); + if (!s_state.texture_scaler_pipeline) + { + ERROR_LOG("Failed to compile scaling pipeline {}", error.GetDescription()); + return false; + } + } + + return true; +} + +std::unique_ptr GPUTextureCache::ScaleTexture(std::unique_ptr texture) +{ + const TextureScalerInfo* const info = s_state.texture_scaler; + + // TODO: rounds + const u32 new_width = texture->GetWidth() * info->scale; + const u32 new_height = texture->GetHeight() * info->scale; + const GPUTexture::Type rt_type = + info->IsComputeShader() ? GPUTexture::Type::RWTexture : GPUTexture::Type::RenderTarget; + auto rt = g_gpu_device->FetchAutoRecycleTexture(new_width, new_height, 1, 1, 1, rt_type, texture->GetFormat()); + if (!rt) [[unlikely]] + { + WARNING_LOG("Failed to create {}x{} RT for scaling", new_width, new_height); + return texture; + } + + g_gpu_device->SetPipeline(s_state.texture_scaler_pipeline.get()); + g_gpu_device->SetRenderTarget(rt.get(), nullptr, + info->IsComputeShader() ? GPUPipeline::BindRenderTargetsAsImages : + GPUPipeline::NoRenderPassFlags); + g_gpu_device->SetTextureSampler(0, texture.get(), g_gpu_device->GetNearestSampler()); + if (!info->IsComputeShader()) + { + g_gpu_device->InvalidateRenderTarget(rt.get()); + g_gpu_device->SetViewportAndScissor(rt->GetRect()); + g_gpu_device->Draw(3, 0); + } + else + { + struct ComputeUBO + { + u32 src_size[2]; + u32 dst_size[2]; + }; + + const ComputeUBO uniforms = {.src_size = {texture->GetWidth(), texture->GetHeight()}, + .dst_size = {new_width, new_height}}; + const auto& [dispatch_x, dispatch_y, dispatch_z] = GPUDevice::GetDispatchCount( + texture->GetWidth(), texture->GetHeight(), 1, info->compute_local_size, info->compute_local_size, 1); + g_gpu_device->PushUniformBuffer(&uniforms, sizeof(uniforms)); + g_gpu_device->Dispatch(dispatch_x, dispatch_y, dispatch_z); + } + + std::unique_ptr new_texture = + g_gpu_device->CreateTexture(new_width, new_height, 1, 1, 1, GPUTexture::Type::Texture, rt->GetFormat()); + if (!new_texture) + { + WARNING_LOG("Failed to create {}x{} texture for scaling", new_width, new_height); + return texture; + } + + rt->MakeReadyForSampling(); + g_gpu_device->CopyTextureRegion(new_texture.get(), 0, 0, 0, 0, rt.get(), 0, 0, 0, 0, new_width, new_height); + g_gpu_device->RecycleTexture(std::move(texture)); + + g_gpu->RestoreDeviceContext(); + + return new_texture; +} diff --git a/src/core/settings.cpp b/src/core/settings.cpp index c0c3a1641..c49f999c7 100644 --- a/src/core/settings.cpp +++ b/src/core/settings.cpp @@ -217,6 +217,10 @@ void Settings::Load(const SettingsInterface& si, const SettingsInterface& contro ParseTextureFilterName( si.GetStringValue("GPU", "SpriteTextureFilter", GetTextureFilterName(gpu_texture_filter)).c_str()) .value_or(gpu_texture_filter); + gpu_texture_scaling = + ParseGPUTextureScalingName( + si.GetStringValue("GPU", "TextureScaling", GetGPUTextureScalingName(gpu_texture_scaling)).c_str()) + .value_or(gpu_texture_scaling); gpu_line_detect_mode = ParseLineDetectModeName( si.GetStringValue("GPU", "LineDetectMode", GetLineDetectModeName(DEFAULT_GPU_LINE_DETECT_MODE)).c_str()) @@ -542,6 +546,7 @@ void Settings::Save(SettingsInterface& si, bool ignore_base) const si.SetStringValue( "GPU", "SpriteTextureFilter", (gpu_sprite_texture_filter != gpu_texture_filter) ? GetTextureFilterName(gpu_sprite_texture_filter) : ""); + si.SetStringValue("GPU", "TextureScaling", GetGPUTextureScalingName(gpu_texture_scaling)); si.SetStringValue("GPU", "LineDetectMode", GetLineDetectModeName(gpu_line_detect_mode)); si.SetStringValue("GPU", "DownsampleMode", GetDownsampleModeName(gpu_downsample_mode)); si.SetUIntValue("GPU", "DownsampleScale", gpu_downsample_scale); @@ -992,6 +997,10 @@ void Settings::FixIncompatibleSettings(bool display_osd_messages) } } + // scaling depends on TC + g_settings.gpu_texture_scaling = + g_settings.gpu_texture_cache ? g_settings.gpu_texture_scaling : GPUTextureScaling::Disabled; + // if challenge mode is enabled, disable things like rewind since they use save states if (Achievements::IsHardcoreModeActive()) { @@ -1580,6 +1589,46 @@ const char* Settings::GetGPUDumpCompressionModeDisplayName(GPUDumpCompressionMod "GPUDumpCompressionMode"); } +static constexpr const std::array s_texture_scaling_names = { + "Disabled", "HQ2x", "HQ3x", "HQ4x", "MMPX", "Scale2x", "xBR", +}; +static constexpr const std::array s_texture_scaling_display_names = { + TRANSLATE_DISAMBIG_NOOP("Settings", "Disabled", "GPUTextureScaling"), + TRANSLATE_DISAMBIG_NOOP("Settings", "HQ2x", "GPUTextureScaling"), + TRANSLATE_DISAMBIG_NOOP("Settings", "HQ3x", "GPUTextureScaling"), + TRANSLATE_DISAMBIG_NOOP("Settings", "HQ4x", "GPUTextureScaling"), + TRANSLATE_DISAMBIG_NOOP("Settings", "MMPX", "GPUTextureScaling"), + TRANSLATE_DISAMBIG_NOOP("Settings", "Scale2x", "GPUTextureScaling"), + TRANSLATE_DISAMBIG_NOOP("Settings", "xBR", "GPUTextureScaling"), +}; +static_assert(s_texture_scaling_names.size() == static_cast(GPUTextureScaling::MaxCount)); +static_assert(s_texture_scaling_display_names.size() == static_cast(GPUTextureScaling::MaxCount)); + +std::optional Settings::ParseGPUTextureScalingName(const char* str) +{ + int index = 0; + for (const char* name : s_texture_scaling_names) + { + if (StringUtil::Strcasecmp(name, str) == 0) + return static_cast(index); + + index++; + } + + return std::nullopt; +} + +const char* Settings::GetGPUTextureScalingName(GPUTextureScaling scaler) +{ + return s_texture_scaling_names[static_cast(scaler)]; +} + +const char* Settings::GetGPUTextureScalingDisplayName(GPUTextureScaling scaler) +{ + return Host::TranslateToCString("Settings", s_texture_scaling_display_names[static_cast(scaler)], + "GPUTextureScaling"); +} + static constexpr const std::array s_display_deinterlacing_mode_names = { "Disabled", "Weave", "Blend", "Adaptive", "Progressive", }; diff --git a/src/core/settings.h b/src/core/settings.h index ef4ff3002..5e22a2220 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -127,6 +127,7 @@ struct Settings ForceVideoTimingMode gpu_force_video_timing = DEFAULT_FORCE_VIDEO_TIMING_MODE; GPUTextureFilter gpu_texture_filter = DEFAULT_GPU_TEXTURE_FILTER; GPUTextureFilter gpu_sprite_texture_filter = DEFAULT_GPU_TEXTURE_FILTER; + GPUTextureScaling gpu_texture_scaling = GPUTextureScaling::Disabled; GPULineDetectMode gpu_line_detect_mode = DEFAULT_GPU_LINE_DETECT_MODE; GPUDownsampleMode gpu_downsample_mode = DEFAULT_GPU_DOWNSAMPLE_MODE; u8 gpu_downsample_scale = 1; @@ -419,6 +420,10 @@ struct Settings static const char* GetGPUDumpCompressionModeName(GPUDumpCompressionMode mode); static const char* GetGPUDumpCompressionModeDisplayName(GPUDumpCompressionMode mode); + static std::optional ParseGPUTextureScalingName(const char* str); + static const char* GetGPUTextureScalingName(GPUTextureScaling scaler); + static const char* GetGPUTextureScalingDisplayName(GPUTextureScaling scaler); + static std::optional ParseDisplayDeinterlacingMode(const char* str); static const char* GetDisplayDeinterlacingModeName(DisplayDeinterlacingMode mode); static const char* GetDisplayDeinterlacingModeDisplayName(DisplayDeinterlacingMode mode); diff --git a/src/core/system.cpp b/src/core/system.cpp index 985d21e89..5bfecc281 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -4360,6 +4360,7 @@ void System::CheckForSettingsChanges(const Settings& old_settings) g_settings.gpu_downsample_scale != old_settings.gpu_downsample_scale || g_settings.gpu_wireframe_mode != old_settings.gpu_wireframe_mode || g_settings.gpu_texture_cache != old_settings.gpu_texture_cache || + (g_settings.gpu_texture_cache && g_settings.gpu_texture_scaling != old_settings.gpu_texture_scaling) || g_settings.display_deinterlacing_mode != old_settings.display_deinterlacing_mode || g_settings.display_24bit_chroma_smoothing != old_settings.display_24bit_chroma_smoothing || g_settings.display_crop_mode != old_settings.display_crop_mode || @@ -4648,7 +4649,7 @@ void System::WarnAboutUnsafeSettings() if (g_settings.gpu_texture_cache) { append( - ICON_FA_PAINT_ROLLER, + ICON_EMOJI_WARNING, TRANSLATE_SV("System", "Texture cache is enabled. This feature is experimental, some games may not render correctly.")); } diff --git a/src/core/types.h b/src/core/types.h index 49e517ce2..d1df6d47c 100644 --- a/src/core/types.h +++ b/src/core/types.h @@ -138,6 +138,18 @@ enum class GPUDumpCompressionMode : u8 MaxCount }; +enum class GPUTextureScaling : u8 +{ + Disabled, + HQ2X, + HQ3X, + HQ4X, + MMPX, + Scale2X, + XBR, + MaxCount +}; + enum class DisplayCropMode : u8 { None, @@ -298,6 +310,6 @@ enum class ForceVideoTimingMode : u8 Disabled, NTSC, PAL, - + Count, }; diff --git a/src/duckstation-qt/graphicssettingswidget.cpp b/src/duckstation-qt/graphicssettingswidget.cpp index c0b52fd92..274eb80d2 100644 --- a/src/duckstation-qt/graphicssettingswidget.cpp +++ b/src/duckstation-qt/graphicssettingswidget.cpp @@ -245,6 +245,10 @@ GraphicsSettingsWidget::GraphicsSettingsWidget(SettingsWindow* dialog, QWidget* SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.enableTextureCache, "GPU", "EnableTextureCache", false); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.useOldMDECRoutines, "Hacks", "UseOldMDECRoutines", false); + SettingWidgetBinder::BindWidgetToEnumSetting( + sif, m_ui.textureScaling, "GPU", "TextureScaling", &Settings::ParseGPUTextureScalingName, + &Settings::GetGPUTextureScalingName, &Settings::GetGPUTextureScalingDisplayName, GPUTextureScaling::Disabled, + GPUTextureScaling::MaxCount); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.enableTextureReplacements, "TextureReplacements", "EnableTextureReplacements", false); @@ -582,6 +586,8 @@ GraphicsSettingsWidget::GraphicsSettingsWidget(SettingsWindow* dialog, QWidget* dialog->registerWidgetHelp(m_ui.useOldMDECRoutines, tr("Use Old MDEC Routines"), tr("Unchecked"), tr("Enables the older, less accurate MDEC decoding routines. May be required for old " "replacement backgrounds to match/load.")); + dialog->registerWidgetHelp(m_ui.textureScaling, tr("Texture Scaling"), tr("Disabled"), + tr("Applies a texture scaling filter to textures as a pre-processing step.")); dialog->registerWidgetHelp(m_ui.enableTextureReplacements, tr("Enable Texture Replacements"), tr("Unchecked"), tr("Enables loading of replacement textures. Not compatible with all games.")); @@ -1146,6 +1152,7 @@ void GraphicsSettingsWidget::onMediaCaptureAudioEnabledChanged() void GraphicsSettingsWidget::onEnableTextureCacheChanged() { const bool tc_enabled = m_dialog->getEffectiveBoolValue("GPU", "EnableTextureCache", false); + m_ui.textureScaling->setEnabled(tc_enabled); m_ui.enableTextureReplacements->setEnabled(tc_enabled); m_ui.enableTextureDumping->setEnabled(tc_enabled); onEnableTextureDumpingChanged(); diff --git a/src/duckstation-qt/graphicssettingswidget.ui b/src/duckstation-qt/graphicssettingswidget.ui index e5e7c2f0e..537dbaa26 100644 --- a/src/duckstation-qt/graphicssettingswidget.ui +++ b/src/duckstation-qt/graphicssettingswidget.ui @@ -7,7 +7,7 @@ 0 0 584 - 477 + 474 @@ -1088,30 +1088,34 @@ General Settings - - - - - Enable Texture Cache - - - + - + + + + + Enable Texture Cache (Experimental) + + + + + + + Use Old MDEC Routines + + + + + + + - The texture cache is currently experimental, and may cause rendering errors in some games. - - - true + Texture Scaling: - - - Use Old MDEC Routines - - + @@ -1246,7 +1250,7 @@ 20 - 40 + 0 diff --git a/src/util/vulkan_device.cpp b/src/util/vulkan_device.cpp index 97042cbec..911a000f7 100644 --- a/src/util/vulkan_device.cpp +++ b/src/util/vulkan_device.cpp @@ -2736,7 +2736,8 @@ void VulkanDevice::PushUniformBuffer(const void* data, u32 data_size) { DebugAssert(data_size < UNIFORM_PUSH_CONSTANTS_SIZE); s_stats.buffer_streamed += data_size; - vkCmdPushConstants(GetCurrentCommandBuffer(), GetCurrentVkPipelineLayout(), UNIFORM_PUSH_CONSTANTS_STAGES, 0, + vkCmdPushConstants(GetCurrentCommandBuffer(), GetCurrentVkPipelineLayout(), + IsCurrentPipelineCompute() ? VK_SHADER_STAGE_COMPUTE_BIT : UNIFORM_PUSH_CONSTANTS_STAGES, 0, data_size, data); } @@ -3472,13 +3473,13 @@ void VulkanDevice::SetPipeline(GPUPipeline* pipeline) m_current_pipeline = static_cast(pipeline); - vkCmdBindPipeline(m_current_command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, m_current_pipeline->GetPipeline()); - if (m_current_pipeline_layout != m_current_pipeline->GetLayout()) { m_current_pipeline_layout = m_current_pipeline->GetLayout(); m_dirty_flags |= DIRTY_FLAG_PIPELINE_LAYOUT; } + + vkCmdBindPipeline(m_current_command_buffer, GetCurrentVkPipelineBindPoint(), m_current_pipeline->GetPipeline()); } void VulkanDevice::UnbindPipeline(VulkanPipeline* pl) @@ -3516,12 +3517,24 @@ VulkanDevice::PipelineLayoutType VulkanDevice::GetPipelineLayoutType(GPUPipeline PipelineLayoutType::Normal); } +bool VulkanDevice::IsCurrentPipelineCompute() const +{ + return (m_current_pipeline_layout >= GPUPipeline::Layout::ComputeSingleTextureAndPushConstants); +} + VkPipelineLayout VulkanDevice::GetCurrentVkPipelineLayout() const { - return m_pipeline_layouts[static_cast(GetPipelineLayoutType(m_current_render_pass_flags))] + return m_pipeline_layouts[IsCurrentPipelineCompute() ? + 0 : + static_cast(GetPipelineLayoutType(m_current_render_pass_flags))] [static_cast(m_current_pipeline_layout)]; } +VkPipelineBindPoint VulkanDevice::GetCurrentVkPipelineBindPoint() const +{ + return IsCurrentPipelineCompute() ? VK_PIPELINE_BIND_POINT_COMPUTE : VK_PIPELINE_BIND_POINT_GRAPHICS; +} + void VulkanDevice::SetInitialPipelineState() { DebugAssert(m_current_pipeline); @@ -3533,7 +3546,7 @@ void VulkanDevice::SetInitialPipelineState() vkCmdBindIndexBuffer(cmdbuf, m_index_buffer.GetBuffer(), 0, VK_INDEX_TYPE_UINT16); m_current_pipeline_layout = m_current_pipeline->GetLayout(); - vkCmdBindPipeline(cmdbuf, VK_PIPELINE_BIND_POINT_GRAPHICS, m_current_pipeline->GetPipeline()); + vkCmdBindPipeline(cmdbuf, GetCurrentVkPipelineBindPoint(), m_current_pipeline->GetPipeline()); const VkViewport vp = {static_cast(m_current_viewport.left), static_cast(m_current_viewport.top), @@ -3733,9 +3746,9 @@ bool VulkanDevice::UpdateDescriptorSetsForLayout(u32 dirty) { [[maybe_unused]] bool new_dynamic_offsets = false; + constexpr bool is_compute = (layout >= GPUPipeline::Layout::ComputeSingleTextureAndPushConstants); constexpr VkPipelineBindPoint vk_bind_point = - ((layout < GPUPipeline::Layout::ComputeSingleTextureAndPushConstants) ? VK_PIPELINE_BIND_POINT_GRAPHICS : - VK_PIPELINE_BIND_POINT_COMPUTE); + (is_compute ? VK_PIPELINE_BIND_POINT_COMPUTE : VK_PIPELINE_BIND_POINT_GRAPHICS); const VkPipelineLayout vk_pipeline_layout = GetCurrentVkPipelineLayout(); std::array ds; u32 first_ds = 0; diff --git a/src/util/vulkan_device.h b/src/util/vulkan_device.h index 6ac6c7ff5..ec93330f8 100644 --- a/src/util/vulkan_device.h +++ b/src/util/vulkan_device.h @@ -372,7 +372,9 @@ private: /// Applies any changed state. static PipelineLayoutType GetPipelineLayoutType(GPUPipeline::RenderPassFlag flags); + bool IsCurrentPipelineCompute() const; VkPipelineLayout GetCurrentVkPipelineLayout() const; + VkPipelineBindPoint GetCurrentVkPipelineBindPoint() const; void SetInitialPipelineState(); void PreDrawCheck(); void PreDispatchCheck();