GPU/TextureCache: Add texture scaling feature

This commit is contained in:
Stenzek 2024-11-20 20:50:45 +10:00
parent c21ea3c85b
commit de77b764a7
No known key found for this signature in database
14 changed files with 827 additions and 35 deletions

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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();
}

View File

@ -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
}

View File

@ -4591,6 +4591,11 @@ void FullscreenUI::DrawGraphicsSettingsPage()
"Hacks", "UseOldMDECRoutines", false); "Hacks", "UseOldMDECRoutines", false);
const bool texture_cache_enabled = GetEffectiveBoolSetting(bsi, "GPU", "EnableTextureCache", 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"), DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_FILE_IMPORT, "Enable Texture Replacements"),
FSUI_CSTR("Enables loading of replacement textures. Not compatible with all games."), FSUI_CSTR("Enables loading of replacement textures. Not compatible with all games."),
"TextureReplacements", "EnableTextureReplacements", false, texture_cache_enabled); "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", "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 deleting empty game settings:\n{}");
TRANSLATE_NOOP("FullscreenUI", "An error occurred while saving 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", "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", "Are you sure you want to clear the current post-processing chain? All configuration will be lost.");
TRANSLATE_NOOP("FullscreenUI", "Aspect Ratio"); 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", "Log To System Console");
TRANSLATE_NOOP("FullscreenUI", "Logging"); TRANSLATE_NOOP("FullscreenUI", "Logging");
TRANSLATE_NOOP("FullscreenUI", "Logging Settings"); TRANSLATE_NOOP("FullscreenUI", "Logging Settings");
TRANSLATE_NOOP("FullscreenUI", "Logging in to RetroAchievements...");
TRANSLATE_NOOP("FullscreenUI", "Login"); 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", "Login token generated on {}");
TRANSLATE_NOOP("FullscreenUI", "Logout"); TRANSLATE_NOOP("FullscreenUI", "Logout");
TRANSLATE_NOOP("FullscreenUI", "Logs BIOS calls to printf(). Not all games contain debugging messages."); 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 Depth Buffer");
TRANSLATE_NOOP("FullscreenUI", "PGXP Geometry Correction"); TRANSLATE_NOOP("FullscreenUI", "PGXP Geometry Correction");
TRANSLATE_NOOP("FullscreenUI", "Parent Directory"); TRANSLATE_NOOP("FullscreenUI", "Parent Directory");
TRANSLATE_NOOP("FullscreenUI", "Password: ");
TRANSLATE_NOOP("FullscreenUI", "Patches"); TRANSLATE_NOOP("FullscreenUI", "Patches");
TRANSLATE_NOOP("FullscreenUI", "Patches the BIOS to skip the boot animation. Safe to enable."); TRANSLATE_NOOP("FullscreenUI", "Patches the BIOS to skip the boot animation. Safe to enable.");
TRANSLATE_NOOP("FullscreenUI", "Path"); 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 Colors");
TRANSLATE_NOOP("FullscreenUI", "Perspective Correct Textures"); TRANSLATE_NOOP("FullscreenUI", "Perspective Correct Textures");
TRANSLATE_NOOP("FullscreenUI", "Plays sound effects for events such as achievement unlocks and leaderboard submissions."); 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", "Port {} Controller Type");
TRANSLATE_NOOP("FullscreenUI", "Post-Processing Settings"); TRANSLATE_NOOP("FullscreenUI", "Post-Processing Settings");
TRANSLATE_NOOP("FullscreenUI", "Post-processing chain cleared."); 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", "Restores the state of the system prior to the last state loaded.");
TRANSLATE_NOOP("FullscreenUI", "Resume Game"); TRANSLATE_NOOP("FullscreenUI", "Resume Game");
TRANSLATE_NOOP("FullscreenUI", "Resume Last Session"); TRANSLATE_NOOP("FullscreenUI", "Resume Last Session");
TRANSLATE_NOOP("FullscreenUI", "RetroAchievements Login");
TRANSLATE_NOOP("FullscreenUI", "Return To Game"); TRANSLATE_NOOP("FullscreenUI", "Return To Game");
TRANSLATE_NOOP("FullscreenUI", "Return to desktop mode, or exit the application."); TRANSLATE_NOOP("FullscreenUI", "Return to desktop mode, or exit the application.");
TRANSLATE_NOOP("FullscreenUI", "Return to the previous menu."); 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", "Test Unofficial Achievements");
TRANSLATE_NOOP("FullscreenUI", "Texture Filtering"); TRANSLATE_NOOP("FullscreenUI", "Texture Filtering");
TRANSLATE_NOOP("FullscreenUI", "Texture Replacements"); TRANSLATE_NOOP("FullscreenUI", "Texture Replacements");
TRANSLATE_NOOP("FullscreenUI", "Texture Scaling");
TRANSLATE_NOOP("FullscreenUI", "Textures Directory"); TRANSLATE_NOOP("FullscreenUI", "Textures Directory");
TRANSLATE_NOOP("FullscreenUI", "The SDL input source supports most controllers."); 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."); 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 Old MDEC Routines");
TRANSLATE_NOOP("FullscreenUI", "Use Single Card For Multi-Disc Games"); TRANSLATE_NOOP("FullscreenUI", "Use Single Card For Multi-Disc Games");
TRANSLATE_NOOP("FullscreenUI", "Use Software Renderer For Readbacks"); TRANSLATE_NOOP("FullscreenUI", "Use Software Renderer For Readbacks");
TRANSLATE_NOOP("FullscreenUI", "User Name: ");
TRANSLATE_NOOP("FullscreenUI", "Username: {}"); TRANSLATE_NOOP("FullscreenUI", "Username: {}");
TRANSLATE_NOOP("FullscreenUI", "Uses PGXP for all instructions, not just memory operations."); 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."); TRANSLATE_NOOP("FullscreenUI", "Uses a blit presentation model instead of flipping. This may be needed on some systems.");

View File

@ -240,7 +240,8 @@ static bool ShouldTrackVRAMWrites();
static bool IsDumpingVRAMWriteTextures(); static bool IsDumpingVRAMWriteTextures();
static void UpdateVRAMTrackingState(); static void UpdateVRAMTrackingState();
static bool CompilePipelines(); static bool CompileReplacementPipelines();
static bool CompileTextureScalingPipeline();
static void DestroyPipelines(); static void DestroyPipelines();
static const Source* ReturnSource(Source* source, const GSVector4i uv_rect, PaletteRecordFlags flags); 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 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 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 void DecodeTexture(u8 page, GPUTexturePaletteReg palette, GPUTextureMode mode, GPUTexture* texture);
static std::unique_ptr<GPUTexture> ScaleTexture(std::unique_ptr<GPUTexture> texture);
static std::optional<TextureReplacementType> GetTextureReplacementTypeFromFileTitle(const std::string_view file_title); static std::optional<TextureReplacementType> GetTextureReplacementTypeFromFileTitle(const std::string_view file_title);
static bool HasValidReplacementExtension(const std::string_view path); static bool HasValidReplacementExtension(const std::string_view path);
@ -501,6 +503,20 @@ ALWAYS_INLINE static float RectDistance(const GSVector4i& lhs, const GSVector4i&
} }
namespace { 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 struct GPUTextureCacheState
{ {
Settings::TextureReplacementSettings::Configuration config; Settings::TextureReplacementSettings::Configuration config;
@ -509,6 +525,7 @@ struct GPUTextureCacheState
VRAMWrite* last_vram_write = nullptr; VRAMWrite* last_vram_write = nullptr;
bool track_vram_writes = false; bool track_vram_writes = false;
const TextureScalerInfo* texture_scaler;
HashCache hash_cache; HashCache hash_cache;
/// List of candidates for purging when the hash cache gets too large. /// 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. /// List of VRAM writes collected when saving state.
std::vector<VRAMWrite*> temp_vram_write_list; std::vector<VRAMWrite*> temp_vram_write_list;
std::unique_ptr<GPUPipeline> texture_scaler_pipeline;
std::unique_ptr<GPUTexture> replacement_texture_render_target; std::unique_ptr<GPUTexture> replacement_texture_render_target;
std::unique_ptr<GPUPipeline> replacement_draw_pipeline; // copies alpha as-is std::unique_ptr<GPUPipeline> replacement_draw_pipeline; // copies alpha as-is
std::unique_ptr<GPUPipeline> replacement_semitransparent_draw_pipeline; // inverts alpha (i.e. semitransparent) std::unique_ptr<GPUPipeline> replacement_semitransparent_draw_pipeline; // inverts alpha (i.e. semitransparent)
@ -540,6 +559,15 @@ struct GPUTextureCacheState
ALIGN_TO_CACHE_LINE GPUTextureCacheState s_state; 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 } // namespace GPUTextureCache
bool GPUTextureCache::ShouldTrackVRAMWrites() bool GPUTextureCache::ShouldTrackVRAMWrites()
@ -562,11 +590,18 @@ bool GPUTextureCache::IsDumpingVRAMWriteTextures()
bool GPUTextureCache::Initialize() bool GPUTextureCache::Initialize()
{ {
s_state.texture_scaler = (g_settings.gpu_texture_scaling == GPUTextureScaling::Disabled) ?
nullptr :
&s_texture_scalers[static_cast<size_t>(g_settings.gpu_texture_scaling) - 1];
LoadLocalConfiguration(false, false); LoadLocalConfiguration(false, false);
UpdateVRAMTrackingState(); UpdateVRAMTrackingState();
if (!CompilePipelines()) if (!CompileReplacementPipelines())
return false; return false;
if (s_state.texture_scaler && !CompileTextureScalingPipeline()) [[unlikely]]
s_state.texture_scaler = nullptr;
return true; return true;
} }
@ -582,11 +617,16 @@ void GPUTextureCache::UpdateSettings(bool use_texture_cache, const Settings& old
Invalidate(); Invalidate();
DestroyPipelines(); DestroyPipelines();
if (!CompilePipelines()) [[unlikely]] if (!CompileReplacementPipelines()) [[unlikely]]
Panic("Failed to compile pipelines on TC settings change"); 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<size_t>(g_settings.gpu_texture_scaling) - 1];
// Reload textures if configuration changes. // Reload textures if configuration changes.
const bool old_replacement_scale_linear_filter = s_state.config.replacement_scale_linear_filter; const bool old_replacement_scale_linear_filter = s_state.config.replacement_scale_linear_filter;
if (LoadLocalConfiguration(false, false) || 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 (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"); Panic("Failed to compile pipelines on TC replacement settings change");
} }
} }
ReloadTextureReplacements(false); 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) bool GPUTextureCache::DoState(StateWrapper& sw, bool skip)
@ -756,7 +804,7 @@ void GPUTextureCache::Shutdown()
s_state.game_id = {}; s_state.game_id = {};
} }
bool GPUTextureCache::CompilePipelines() bool GPUTextureCache::CompileReplacementPipelines()
{ {
if (!g_settings.texture_replacements.enable_texture_replacements) if (!g_settings.texture_replacements.enable_texture_replacements)
return true; return true;
@ -807,6 +855,7 @@ bool GPUTextureCache::CompilePipelines()
void GPUTextureCache::DestroyPipelines() void GPUTextureCache::DestroyPipelines()
{ {
s_state.texture_scaler_pipeline.reset();
s_state.replacement_draw_pipeline.reset(); s_state.replacement_draw_pipeline.reset();
s_state.replacement_semitransparent_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()); 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) if (g_settings.texture_replacements.enable_texture_replacements)
ApplyTextureReplacements(key, tex_hash, pal_hash, &entry); 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); entry->texture = std::move(replacement_tex);
g_gpu->RestoreDeviceContext(); g_gpu->RestoreDeviceContext();
} }
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<std::string> 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<GPUShader> 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<GPUShader> 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<GPUShader> 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<GPUTexture> GPUTextureCache::ScaleTexture(std::unique_ptr<GPUTexture> 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<GPUTexture> 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;
}

View File

@ -217,6 +217,10 @@ void Settings::Load(const SettingsInterface& si, const SettingsInterface& contro
ParseTextureFilterName( ParseTextureFilterName(
si.GetStringValue("GPU", "SpriteTextureFilter", GetTextureFilterName(gpu_texture_filter)).c_str()) si.GetStringValue("GPU", "SpriteTextureFilter", GetTextureFilterName(gpu_texture_filter)).c_str())
.value_or(gpu_texture_filter); .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 = gpu_line_detect_mode =
ParseLineDetectModeName( ParseLineDetectModeName(
si.GetStringValue("GPU", "LineDetectMode", GetLineDetectModeName(DEFAULT_GPU_LINE_DETECT_MODE)).c_str()) 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( si.SetStringValue(
"GPU", "SpriteTextureFilter", "GPU", "SpriteTextureFilter",
(gpu_sprite_texture_filter != gpu_texture_filter) ? GetTextureFilterName(gpu_sprite_texture_filter) : ""); (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", "LineDetectMode", GetLineDetectModeName(gpu_line_detect_mode));
si.SetStringValue("GPU", "DownsampleMode", GetDownsampleModeName(gpu_downsample_mode)); si.SetStringValue("GPU", "DownsampleMode", GetDownsampleModeName(gpu_downsample_mode));
si.SetUIntValue("GPU", "DownsampleScale", gpu_downsample_scale); 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 challenge mode is enabled, disable things like rewind since they use save states
if (Achievements::IsHardcoreModeActive()) if (Achievements::IsHardcoreModeActive())
{ {
@ -1580,6 +1589,46 @@ const char* Settings::GetGPUDumpCompressionModeDisplayName(GPUDumpCompressionMod
"GPUDumpCompressionMode"); "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<size_t>(GPUTextureScaling::MaxCount));
static_assert(s_texture_scaling_display_names.size() == static_cast<size_t>(GPUTextureScaling::MaxCount));
std::optional<GPUTextureScaling> 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<GPUTextureScaling>(index);
index++;
}
return std::nullopt;
}
const char* Settings::GetGPUTextureScalingName(GPUTextureScaling scaler)
{
return s_texture_scaling_names[static_cast<size_t>(scaler)];
}
const char* Settings::GetGPUTextureScalingDisplayName(GPUTextureScaling scaler)
{
return Host::TranslateToCString("Settings", s_texture_scaling_display_names[static_cast<size_t>(scaler)],
"GPUTextureScaling");
}
static constexpr const std::array s_display_deinterlacing_mode_names = { static constexpr const std::array s_display_deinterlacing_mode_names = {
"Disabled", "Weave", "Blend", "Adaptive", "Progressive", "Disabled", "Weave", "Blend", "Adaptive", "Progressive",
}; };

View File

@ -127,6 +127,7 @@ struct Settings
ForceVideoTimingMode gpu_force_video_timing = DEFAULT_FORCE_VIDEO_TIMING_MODE; ForceVideoTimingMode gpu_force_video_timing = DEFAULT_FORCE_VIDEO_TIMING_MODE;
GPUTextureFilter gpu_texture_filter = DEFAULT_GPU_TEXTURE_FILTER; GPUTextureFilter gpu_texture_filter = DEFAULT_GPU_TEXTURE_FILTER;
GPUTextureFilter gpu_sprite_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; GPULineDetectMode gpu_line_detect_mode = DEFAULT_GPU_LINE_DETECT_MODE;
GPUDownsampleMode gpu_downsample_mode = DEFAULT_GPU_DOWNSAMPLE_MODE; GPUDownsampleMode gpu_downsample_mode = DEFAULT_GPU_DOWNSAMPLE_MODE;
u8 gpu_downsample_scale = 1; u8 gpu_downsample_scale = 1;
@ -419,6 +420,10 @@ struct Settings
static const char* GetGPUDumpCompressionModeName(GPUDumpCompressionMode mode); static const char* GetGPUDumpCompressionModeName(GPUDumpCompressionMode mode);
static const char* GetGPUDumpCompressionModeDisplayName(GPUDumpCompressionMode mode); static const char* GetGPUDumpCompressionModeDisplayName(GPUDumpCompressionMode mode);
static std::optional<GPUTextureScaling> ParseGPUTextureScalingName(const char* str);
static const char* GetGPUTextureScalingName(GPUTextureScaling scaler);
static const char* GetGPUTextureScalingDisplayName(GPUTextureScaling scaler);
static std::optional<DisplayDeinterlacingMode> ParseDisplayDeinterlacingMode(const char* str); static std::optional<DisplayDeinterlacingMode> ParseDisplayDeinterlacingMode(const char* str);
static const char* GetDisplayDeinterlacingModeName(DisplayDeinterlacingMode mode); static const char* GetDisplayDeinterlacingModeName(DisplayDeinterlacingMode mode);
static const char* GetDisplayDeinterlacingModeDisplayName(DisplayDeinterlacingMode mode); static const char* GetDisplayDeinterlacingModeDisplayName(DisplayDeinterlacingMode mode);

View File

@ -4360,6 +4360,7 @@ void System::CheckForSettingsChanges(const Settings& old_settings)
g_settings.gpu_downsample_scale != old_settings.gpu_downsample_scale || g_settings.gpu_downsample_scale != old_settings.gpu_downsample_scale ||
g_settings.gpu_wireframe_mode != old_settings.gpu_wireframe_mode || 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 != 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_deinterlacing_mode != old_settings.display_deinterlacing_mode ||
g_settings.display_24bit_chroma_smoothing != old_settings.display_24bit_chroma_smoothing || g_settings.display_24bit_chroma_smoothing != old_settings.display_24bit_chroma_smoothing ||
g_settings.display_crop_mode != old_settings.display_crop_mode || g_settings.display_crop_mode != old_settings.display_crop_mode ||
@ -4648,7 +4649,7 @@ void System::WarnAboutUnsafeSettings()
if (g_settings.gpu_texture_cache) if (g_settings.gpu_texture_cache)
{ {
append( append(
ICON_FA_PAINT_ROLLER, ICON_EMOJI_WARNING,
TRANSLATE_SV("System", TRANSLATE_SV("System",
"Texture cache is enabled. This feature is experimental, some games may not render correctly.")); "Texture cache is enabled. This feature is experimental, some games may not render correctly."));
} }

View File

@ -138,6 +138,18 @@ enum class GPUDumpCompressionMode : u8
MaxCount MaxCount
}; };
enum class GPUTextureScaling : u8
{
Disabled,
HQ2X,
HQ3X,
HQ4X,
MMPX,
Scale2X,
XBR,
MaxCount
};
enum class DisplayCropMode : u8 enum class DisplayCropMode : u8
{ {
None, None,
@ -298,6 +310,6 @@ enum class ForceVideoTimingMode : u8
Disabled, Disabled,
NTSC, NTSC,
PAL, PAL,
Count, Count,
}; };

View File

@ -245,6 +245,10 @@ GraphicsSettingsWidget::GraphicsSettingsWidget(SettingsWindow* dialog, QWidget*
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.enableTextureCache, "GPU", "EnableTextureCache", false); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.enableTextureCache, "GPU", "EnableTextureCache", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.useOldMDECRoutines, "Hacks", "UseOldMDECRoutines", 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", SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.enableTextureReplacements, "TextureReplacements",
"EnableTextureReplacements", false); "EnableTextureReplacements", false);
@ -582,6 +586,8 @@ GraphicsSettingsWidget::GraphicsSettingsWidget(SettingsWindow* dialog, QWidget*
dialog->registerWidgetHelp(m_ui.useOldMDECRoutines, tr("Use Old MDEC Routines"), tr("Unchecked"), 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 " tr("Enables the older, less accurate MDEC decoding routines. May be required for old "
"replacement backgrounds to match/load.")); "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"), dialog->registerWidgetHelp(m_ui.enableTextureReplacements, tr("Enable Texture Replacements"), tr("Unchecked"),
tr("Enables loading of replacement textures. Not compatible with all games.")); tr("Enables loading of replacement textures. Not compatible with all games."));
@ -1146,6 +1152,7 @@ void GraphicsSettingsWidget::onMediaCaptureAudioEnabledChanged()
void GraphicsSettingsWidget::onEnableTextureCacheChanged() void GraphicsSettingsWidget::onEnableTextureCacheChanged()
{ {
const bool tc_enabled = m_dialog->getEffectiveBoolValue("GPU", "EnableTextureCache", false); const bool tc_enabled = m_dialog->getEffectiveBoolValue("GPU", "EnableTextureCache", false);
m_ui.textureScaling->setEnabled(tc_enabled);
m_ui.enableTextureReplacements->setEnabled(tc_enabled); m_ui.enableTextureReplacements->setEnabled(tc_enabled);
m_ui.enableTextureDumping->setEnabled(tc_enabled); m_ui.enableTextureDumping->setEnabled(tc_enabled);
onEnableTextureDumpingChanged(); onEnableTextureDumpingChanged();

View File

@ -7,7 +7,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>584</width> <width>584</width>
<height>477</height> <height>474</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@ -1088,30 +1088,34 @@
<property name="title"> <property name="title">
<string>General Settings</string> <string>General Settings</string>
</property> </property>
<layout class="QGridLayout" name="gridLayout_9"> <layout class="QFormLayout" name="formLayout_13">
<item row="1" column="0">
<widget class="QCheckBox" name="enableTextureCache">
<property name="text">
<string>Enable Texture Cache</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="2"> <item row="0" column="0" colspan="2">
<widget class="QLabel" name="label_4"> <layout class="QGridLayout" name="gridLayout_11">
<item row="0" column="0">
<widget class="QCheckBox" name="enableTextureCache">
<property name="text">
<string>Enable Texture Cache (Experimental)</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="useOldMDECRoutines">
<property name="text">
<string>Use Old MDEC Routines</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_12">
<property name="text"> <property name="text">
<string>The texture cache is currently experimental, and may cause rendering errors in some games.</string> <string>Texture Scaling:</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="1"> <item row="1" column="1">
<widget class="QCheckBox" name="useOldMDECRoutines"> <widget class="QComboBox" name="textureScaling"/>
<property name="text">
<string>Use Old MDEC Routines</string>
</property>
</widget>
</item> </item>
</layout> </layout>
</widget> </widget>
@ -1246,7 +1250,7 @@
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
<width>20</width> <width>20</width>
<height>40</height> <height>0</height>
</size> </size>
</property> </property>
</spacer> </spacer>

View File

@ -2736,7 +2736,8 @@ void VulkanDevice::PushUniformBuffer(const void* data, u32 data_size)
{ {
DebugAssert(data_size < UNIFORM_PUSH_CONSTANTS_SIZE); DebugAssert(data_size < UNIFORM_PUSH_CONSTANTS_SIZE);
s_stats.buffer_streamed += data_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); data_size, data);
} }
@ -3472,13 +3473,13 @@ void VulkanDevice::SetPipeline(GPUPipeline* pipeline)
m_current_pipeline = static_cast<VulkanPipeline*>(pipeline); m_current_pipeline = static_cast<VulkanPipeline*>(pipeline);
vkCmdBindPipeline(m_current_command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, m_current_pipeline->GetPipeline());
if (m_current_pipeline_layout != m_current_pipeline->GetLayout()) if (m_current_pipeline_layout != m_current_pipeline->GetLayout())
{ {
m_current_pipeline_layout = m_current_pipeline->GetLayout(); m_current_pipeline_layout = m_current_pipeline->GetLayout();
m_dirty_flags |= DIRTY_FLAG_PIPELINE_LAYOUT; m_dirty_flags |= DIRTY_FLAG_PIPELINE_LAYOUT;
} }
vkCmdBindPipeline(m_current_command_buffer, GetCurrentVkPipelineBindPoint(), m_current_pipeline->GetPipeline());
} }
void VulkanDevice::UnbindPipeline(VulkanPipeline* pl) void VulkanDevice::UnbindPipeline(VulkanPipeline* pl)
@ -3516,12 +3517,24 @@ VulkanDevice::PipelineLayoutType VulkanDevice::GetPipelineLayoutType(GPUPipeline
PipelineLayoutType::Normal); PipelineLayoutType::Normal);
} }
bool VulkanDevice::IsCurrentPipelineCompute() const
{
return (m_current_pipeline_layout >= GPUPipeline::Layout::ComputeSingleTextureAndPushConstants);
}
VkPipelineLayout VulkanDevice::GetCurrentVkPipelineLayout() const VkPipelineLayout VulkanDevice::GetCurrentVkPipelineLayout() const
{ {
return m_pipeline_layouts[static_cast<size_t>(GetPipelineLayoutType(m_current_render_pass_flags))] return m_pipeline_layouts[IsCurrentPipelineCompute() ?
0 :
static_cast<size_t>(GetPipelineLayoutType(m_current_render_pass_flags))]
[static_cast<size_t>(m_current_pipeline_layout)]; [static_cast<size_t>(m_current_pipeline_layout)];
} }
VkPipelineBindPoint VulkanDevice::GetCurrentVkPipelineBindPoint() const
{
return IsCurrentPipelineCompute() ? VK_PIPELINE_BIND_POINT_COMPUTE : VK_PIPELINE_BIND_POINT_GRAPHICS;
}
void VulkanDevice::SetInitialPipelineState() void VulkanDevice::SetInitialPipelineState()
{ {
DebugAssert(m_current_pipeline); DebugAssert(m_current_pipeline);
@ -3533,7 +3546,7 @@ void VulkanDevice::SetInitialPipelineState()
vkCmdBindIndexBuffer(cmdbuf, m_index_buffer.GetBuffer(), 0, VK_INDEX_TYPE_UINT16); vkCmdBindIndexBuffer(cmdbuf, m_index_buffer.GetBuffer(), 0, VK_INDEX_TYPE_UINT16);
m_current_pipeline_layout = m_current_pipeline->GetLayout(); 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<float>(m_current_viewport.left), const VkViewport vp = {static_cast<float>(m_current_viewport.left),
static_cast<float>(m_current_viewport.top), static_cast<float>(m_current_viewport.top),
@ -3733,9 +3746,9 @@ bool VulkanDevice::UpdateDescriptorSetsForLayout(u32 dirty)
{ {
[[maybe_unused]] bool new_dynamic_offsets = false; [[maybe_unused]] bool new_dynamic_offsets = false;
constexpr bool is_compute = (layout >= GPUPipeline::Layout::ComputeSingleTextureAndPushConstants);
constexpr VkPipelineBindPoint vk_bind_point = constexpr VkPipelineBindPoint vk_bind_point =
((layout < GPUPipeline::Layout::ComputeSingleTextureAndPushConstants) ? VK_PIPELINE_BIND_POINT_GRAPHICS : (is_compute ? VK_PIPELINE_BIND_POINT_COMPUTE : VK_PIPELINE_BIND_POINT_GRAPHICS);
VK_PIPELINE_BIND_POINT_COMPUTE);
const VkPipelineLayout vk_pipeline_layout = GetCurrentVkPipelineLayout(); const VkPipelineLayout vk_pipeline_layout = GetCurrentVkPipelineLayout();
std::array<VkDescriptorSet, 3> ds; std::array<VkDescriptorSet, 3> ds;
u32 first_ds = 0; u32 first_ds = 0;

View File

@ -372,7 +372,9 @@ private:
/// Applies any changed state. /// Applies any changed state.
static PipelineLayoutType GetPipelineLayoutType(GPUPipeline::RenderPassFlag flags); static PipelineLayoutType GetPipelineLayoutType(GPUPipeline::RenderPassFlag flags);
bool IsCurrentPipelineCompute() const;
VkPipelineLayout GetCurrentVkPipelineLayout() const; VkPipelineLayout GetCurrentVkPipelineLayout() const;
VkPipelineBindPoint GetCurrentVkPipelineBindPoint() const;
void SetInitialPipelineState(); void SetInitialPipelineState();
void PreDrawCheck(); void PreDrawCheck();
void PreDispatchCheck(); void PreDispatchCheck();