diff --git a/src/citron/configuration/shared_translation.cpp b/src/citron/configuration/shared_translation.cpp index 20e92d1d2..819a6a3bc 100644 --- a/src/citron/configuration/shared_translation.cpp +++ b/src/citron/configuration/shared_translation.cpp @@ -406,6 +406,7 @@ std::unique_ptr ComboboxEnumeration(QWidget* parent) { PAIR(ScalingFilter, Lanczos, tr("Lanczos")), PAIR(ScalingFilter, Gaussian, tr("Gaussian")), PAIR(ScalingFilter, ScaleForce, tr("ScaleForce")), + PAIR(ScalingFilter, ScaleFx, tr("ScaleFX")), PAIR(ScalingFilter, Fsr, tr("AMD FidelityFX™️ Super Resolution")), PAIR(ScalingFilter, Fsr2, tr("AMD FidelityFX™️ Super Resolution 2.0")), }}); diff --git a/src/citron/configuration/shared_translation.h b/src/citron/configuration/shared_translation.h index a2c3bdd98..fad796d8d 100644 --- a/src/citron/configuration/shared_translation.h +++ b/src/citron/configuration/shared_translation.h @@ -42,6 +42,8 @@ static const std::map scaling_filter_texts_map {Settings::ScalingFilter::Lanczos, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "Lanczos"))}, {Settings::ScalingFilter::ScaleForce, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "ScaleForce"))}, + {Settings::ScalingFilter::ScaleFx, + QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "ScaleFX"))}, {Settings::ScalingFilter::Fsr, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "FSR"))}, {Settings::ScalingFilter::Fsr2, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "FSR 2.0"))}, }; diff --git a/src/video_core/host_shaders/CMakeLists.txt b/src/video_core/host_shaders/CMakeLists.txt index 01f2fc72a..fbc661065 100644 --- a/src/video_core/host_shaders/CMakeLists.txt +++ b/src/video_core/host_shaders/CMakeLists.txt @@ -40,6 +40,7 @@ set(SHADER_FILES opengl_present.frag opengl_present.vert opengl_present_scaleforce.frag + opengl_present_scalefx.frag opengl_smaa.glsl pitch_unswizzle.comp present_bicubic.frag @@ -70,6 +71,8 @@ set(SHADER_FILES vulkan_present.vert vulkan_present_scaleforce_fp16.frag vulkan_present_scaleforce_fp32.frag + vulkan_present_scalefx_fp16.frag + vulkan_present_scalefx_fp32.frag vulkan_quad_indexed.comp vulkan_taa.frag vulkan_taa.vert diff --git a/src/video_core/host_shaders/opengl_present_scalefx.frag b/src/video_core/host_shaders/opengl_present_scalefx.frag new file mode 100644 index 000000000..d679a9ddb --- /dev/null +++ b/src/video_core/host_shaders/opengl_present_scalefx.frag @@ -0,0 +1,143 @@ +// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +// ScaleFX shader adapted from libretro's ScaleFX implementation +// Original: https://github.com/libretro/slang-shaders/tree/master/scalefx +// ScaleFX is a pixel art scaling algorithm designed to preserve sharp edges + +#ifndef VERSION +#version 460 +#endif + +#extension GL_ARB_separate_shader_objects : enable + +#ifdef CITRON_USE_FP16 +#extension GL_AMD_gpu_shader_half_float : enable +#extension GL_NV_gpu_shader5 : enable +#define lfloat float16_t +#define lvec2 f16vec2 +#define lvec3 f16vec3 +#define lvec4 f16vec4 +#else +#define lfloat float +#define lvec2 vec2 +#define lvec3 vec3 +#define lvec4 vec4 +#endif + +layout (location = 0) in vec2 tex_coord; +layout (location = 0) out vec4 frag_color; + +layout (binding = 0) uniform sampler2D input_texture; + +// ScaleFX configuration +const lfloat SFX_CLR = lfloat(0.35); // Color threshold - lower = more edge detection +const lfloat SFX_SAA = lfloat(1.0); // Sub-pixel anti-aliasing strength + +// Helper function to calculate color difference using weighted RGB +lfloat df(lvec3 A, lvec3 B) { + lvec3 diff = A - B; + return dot(abs(diff), lvec3(1.0, 1.0, 1.0)); +} + +// Check if two colors are similar within threshold +bool eq(lvec3 A, lvec3 B) { + return df(A, B) < SFX_CLR; +} + +// ScaleFX main algorithm +lvec4 scalefx(sampler2D tex, vec2 tex_coord) { + vec2 ps = vec2(1.0) / vec2(textureSize(tex, 0)); + vec2 pos = fract(tex_coord * vec2(textureSize(tex, 0)) - vec2(0.5)); + + // Sample 3x3 grid around current pixel + // A B C + // D E F + // G H I + + lvec3 A = lvec3(texture(tex, tex_coord + vec2(-1, -1) * ps).rgb); + lvec3 B = lvec3(texture(tex, tex_coord + vec2( 0, -1) * ps).rgb); + lvec3 C = lvec3(texture(tex, tex_coord + vec2( 1, -1) * ps).rgb); + lvec3 D = lvec3(texture(tex, tex_coord + vec2(-1, 0) * ps).rgb); + lvec3 E = lvec3(texture(tex, tex_coord).rgb); + lvec3 F = lvec3(texture(tex, tex_coord + vec2( 1, 0) * ps).rgb); + lvec3 G = lvec3(texture(tex, tex_coord + vec2(-1, 1) * ps).rgb); + lvec3 H = lvec3(texture(tex, tex_coord + vec2( 0, 1) * ps).rgb); + lvec3 I = lvec3(texture(tex, tex_coord + vec2( 1, 1) * ps).rgb); + + // Edge detection patterns - ScaleFX algorithm + // Pattern 1: Horizontal edge D-E-F + bool P1 = (eq(D, E) && !eq(D, F) && !eq(D, B) && !eq(D, H)); + // Pattern 2: Horizontal edge E-F-? + bool P2 = (eq(E, F) && !eq(E, D) && !eq(E, B) && !eq(E, H)); + // Pattern 3: Vertical edge B-E-H + bool P3 = (eq(B, E) && !eq(B, H) && !eq(B, D) && !eq(B, F)); + // Pattern 4: Vertical edge E-H-? + bool P4 = (eq(E, H) && !eq(E, B) && !eq(E, D) && !eq(E, F)); + // Pattern 5: Diagonal edge A-E-I + bool P5 = (eq(A, E) && !eq(A, I) && !eq(A, C) && !eq(A, G)); + // Pattern 6: Diagonal edge C-E-G + bool P6 = (eq(C, E) && !eq(C, G) && !eq(C, A) && !eq(C, I)); + + lvec3 res = E; + lfloat fx = lfloat(pos.x); + lfloat fy = lfloat(pos.y); + + // Apply ScaleFX blending based on detected patterns + // Horizontal patterns + if (P1) { + // Blend towards D + lfloat dist_x = lfloat(1.0) - fx; + res = mix(res, D, dist_x * SFX_SAA * lfloat(0.5)); + } + if (P2) { + // Blend towards F + res = mix(res, F, fx * SFX_SAA * lfloat(0.5)); + } + + // Vertical patterns + if (P3) { + // Blend towards B + lfloat dist_y = lfloat(1.0) - fy; + res = mix(res, B, dist_y * SFX_SAA * lfloat(0.5)); + } + if (P4) { + // Blend towards H + res = mix(res, H, fy * SFX_SAA * lfloat(0.5)); + } + + // Diagonal patterns (more subtle) + if (P5) { + // Blend towards A (top-left) + lfloat dist = lfloat(1.0) - (fx + fy) * lfloat(0.5); + if (dist > lfloat(0.0)) { + res = mix(res, A, dist * SFX_SAA * lfloat(0.3)); + } + } + if (P6) { + // Blend towards C (top-right) + lfloat dist = lfloat(1.0) - (lfloat(1.0) - fx + fy) * lfloat(0.5); + if (dist > lfloat(0.0)) { + res = mix(res, C, dist * SFX_SAA * lfloat(0.3)); + } + } + + // Additional corner handling for better quality + // Bottom-right corner blending + if (eq(E, I) && !eq(E, G) && fx > lfloat(0.5) && fy > lfloat(0.5)) { + lfloat dist = (fx - lfloat(0.5)) * (fy - lfloat(0.5)) * lfloat(4.0); + res = mix(res, I, dist * SFX_SAA * lfloat(0.3)); + } + + // Bottom-left corner blending + if (eq(E, G) && !eq(E, I) && fx < lfloat(0.5) && fy > lfloat(0.5)) { + lfloat dist = (lfloat(0.5) - fx) * (fy - lfloat(0.5)) * lfloat(4.0); + res = mix(res, G, dist * SFX_SAA * lfloat(0.3)); + } + + return lvec4(res, lfloat(1.0)); +} + +void main() { + frag_color = vec4(scalefx(input_texture, tex_coord)); +} diff --git a/src/video_core/host_shaders/vulkan_present_scalefx_fp16.frag b/src/video_core/host_shaders/vulkan_present_scalefx_fp16.frag new file mode 100644 index 000000000..9f25b39db --- /dev/null +++ b/src/video_core/host_shaders/vulkan_present_scalefx_fp16.frag @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#version 460 + +#extension GL_GOOGLE_include_directive : enable +#extension GL_AMD_gpu_shader_half_float : enable +#extension GL_NV_gpu_shader5 : enable + +#define VERSION 2 + +#include "opengl_present_scalefx.frag" diff --git a/src/video_core/host_shaders/vulkan_present_scalefx_fp32.frag b/src/video_core/host_shaders/vulkan_present_scalefx_fp32.frag new file mode 100644 index 000000000..6fc9ea8e0 --- /dev/null +++ b/src/video_core/host_shaders/vulkan_present_scalefx_fp32.frag @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#version 460 + +#extension GL_GOOGLE_include_directive : enable + +#define VERSION 2 + +#include "opengl_present_scalefx.frag" diff --git a/src/video_core/renderer_opengl/gl_blit_screen.cpp b/src/video_core/renderer_opengl/gl_blit_screen.cpp index e7d249392..d0d03835c 100644 --- a/src/video_core/renderer_opengl/gl_blit_screen.cpp +++ b/src/video_core/renderer_opengl/gl_blit_screen.cpp @@ -90,6 +90,9 @@ void BlitScreen::CreateWindowAdapt() { case Settings::ScalingFilter::ScaleForce: window_adapt = MakeScaleForce(device); break; + case Settings::ScalingFilter::ScaleFx: + window_adapt = MakeScaleFx(device); + break; case Settings::ScalingFilter::Fsr: case Settings::ScalingFilter::Fsr2: case Settings::ScalingFilter::Bilinear: diff --git a/src/video_core/renderer_opengl/present/filters.cpp b/src/video_core/renderer_opengl/present/filters.cpp index 619583300..2f20bb388 100644 --- a/src/video_core/renderer_opengl/present/filters.cpp +++ b/src/video_core/renderer_opengl/present/filters.cpp @@ -4,6 +4,7 @@ #include "video_core/host_shaders/opengl_present_frag.h" #include "video_core/host_shaders/opengl_present_scaleforce_frag.h" +#include "video_core/host_shaders/opengl_present_scalefx_frag.h" #include "video_core/host_shaders/present_bicubic_frag.h" #include "video_core/host_shaders/present_lanczos_frag.h" #include "video_core/host_shaders/present_gaussian_frag.h" @@ -38,6 +39,12 @@ std::unique_ptr MakeScaleForce(const Device& device) { fmt::format("#version 460\n{}", HostShaders::OPENGL_PRESENT_SCALEFORCE_FRAG)); } +std::unique_ptr MakeScaleFx(const Device& device) { + return std::make_unique( + device, CreateBilinearSampler(), + fmt::format("#version 460\n{}", HostShaders::OPENGL_PRESENT_SCALEFX_FRAG)); +} + std::unique_ptr MakeLanczos(const Device& device) { return std::make_unique(device, CreateNearestNeighborSampler(), HostShaders::PRESENT_LANCZOS_FRAG); diff --git a/src/video_core/renderer_opengl/present/filters.h b/src/video_core/renderer_opengl/present/filters.h index 182a764c6..4e525dedc 100644 --- a/src/video_core/renderer_opengl/present/filters.h +++ b/src/video_core/renderer_opengl/present/filters.h @@ -15,5 +15,6 @@ std::unique_ptr MakeBicubic(const Device& device); std::unique_ptr MakeLanczos(const Device& device); std::unique_ptr MakeGaussian(const Device& device); std::unique_ptr MakeScaleForce(const Device& device); +std::unique_ptr MakeScaleFx(const Device& device); } // namespace OpenGL diff --git a/src/video_core/renderer_vulkan/present/filters.cpp b/src/video_core/renderer_vulkan/present/filters.cpp index d57665b96..e9dc2be22 100644 --- a/src/video_core/renderer_vulkan/present/filters.cpp +++ b/src/video_core/renderer_vulkan/present/filters.cpp @@ -10,6 +10,8 @@ #include "video_core/host_shaders/vulkan_present_frag_spv.h" #include "video_core/host_shaders/vulkan_present_scaleforce_fp16_frag_spv.h" #include "video_core/host_shaders/vulkan_present_scaleforce_fp32_frag_spv.h" +#include "video_core/host_shaders/vulkan_present_scalefx_fp16_frag_spv.h" +#include "video_core/host_shaders/vulkan_present_scalefx_fp32_frag_spv.h" #include "video_core/renderer_vulkan/present/filters.h" #include "video_core/renderer_vulkan/present/util.h" #include "video_core/renderer_vulkan/vk_shader_util.h" @@ -27,6 +29,14 @@ vk::ShaderModule SelectScaleForceShader(const Device& device) { } } +vk::ShaderModule SelectScaleFxShader(const Device& device) { + if (device.IsFloat16Supported()) { + return BuildShader(device, VULKAN_PRESENT_SCALEFX_FP16_FRAG_SPV); + } else { + return BuildShader(device, VULKAN_PRESENT_SCALEFX_FP32_FRAG_SPV); + } +} + } // Anonymous namespace std::unique_ptr MakeNearestNeighbor(const Device& device, VkFormat frame_format) { @@ -55,6 +65,11 @@ std::unique_ptr MakeScaleForce(const Device& device, VkFormat f SelectScaleForceShader(device)); } +std::unique_ptr MakeScaleFx(const Device& device, VkFormat frame_format) { + return std::make_unique(device, frame_format, CreateBilinearSampler(device), + SelectScaleFxShader(device)); +} + std::unique_ptr MakeLanczos(const Device& device, VkFormat frame_format) { return std::make_unique(device, frame_format, CreateNearestNeighborSampler(device), BuildShader(device, PRESENT_LANCZOS_FRAG_SPV)); diff --git a/src/video_core/renderer_vulkan/present/filters.h b/src/video_core/renderer_vulkan/present/filters.h index eb9888f07..a69abf209 100644 --- a/src/video_core/renderer_vulkan/present/filters.h +++ b/src/video_core/renderer_vulkan/present/filters.h @@ -16,5 +16,6 @@ std::unique_ptr MakeBicubic(const Device& device, VkFormat fram std::unique_ptr MakeLanczos(const Device& device, VkFormat frame_format); std::unique_ptr MakeGaussian(const Device& device, VkFormat frame_format); std::unique_ptr MakeScaleForce(const Device& device, VkFormat frame_format); +std::unique_ptr MakeScaleFx(const Device& device, VkFormat frame_format); } // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/vk_blit_screen.cpp b/src/video_core/renderer_vulkan/vk_blit_screen.cpp index a1622b077..07d329872 100644 --- a/src/video_core/renderer_vulkan/vk_blit_screen.cpp +++ b/src/video_core/renderer_vulkan/vk_blit_screen.cpp @@ -47,6 +47,9 @@ void BlitScreen::SetWindowAdaptPass() { case Settings::ScalingFilter::ScaleForce: window_adapt = MakeScaleForce(device, swapchain_view_format); break; + case Settings::ScalingFilter::ScaleFx: + window_adapt = MakeScaleFx(device, swapchain_view_format); + break; case Settings::ScalingFilter::Fsr: case Settings::ScalingFilter::Fsr2: case Settings::ScalingFilter::Bilinear: