diff --git a/src/common/settings_enums.h b/src/common/settings_enums.h index 41133a7819..ebfa4ceb9e 100644 --- a/src/common/settings_enums.h +++ b/src/common/settings_enums.h @@ -166,7 +166,7 @@ ENUM(ResolutionSetup, Res7X, Res8X); -ENUM(ScalingFilter, NearestNeighbor, Bilinear, Bicubic, Gaussian, ScaleForce, Fsr, Area, MaxEnum); +ENUM(ScalingFilter, NearestNeighbor, Bilinear, Bicubic, Spline1, Gaussian, Lanczos, ScaleForce, Fsr, Area, MaxEnum); ENUM(AntiAliasing, None, Fxaa, Smaa, MaxEnum); diff --git a/src/qt_common/shared_translation.cpp b/src/qt_common/shared_translation.cpp index eb413f28e9..4254253c2f 100644 --- a/src/qt_common/shared_translation.cpp +++ b/src/qt_common/shared_translation.cpp @@ -572,7 +572,9 @@ std::unique_ptr ComboboxEnumeration(QObject* parent) PAIR(ScalingFilter, NearestNeighbor, tr("Nearest Neighbor")), PAIR(ScalingFilter, Bilinear, tr("Bilinear")), PAIR(ScalingFilter, Bicubic, tr("Bicubic")), + PAIR(ScalingFilter, Spline1, tr("Spline-1")), PAIR(ScalingFilter, Gaussian, tr("Gaussian")), + PAIR(ScalingFilter, Lanczos, tr("Lanczos")), PAIR(ScalingFilter, ScaleForce, tr("ScaleForce")), PAIR(ScalingFilter, Fsr, tr("AMD FidelityFX™️ Super Resolution")), PAIR(ScalingFilter, Area, tr("Area")), diff --git a/src/qt_common/shared_translation.h b/src/qt_common/shared_translation.h index 48a2cb5205..c9216c2daa 100644 --- a/src/qt_common/shared_translation.h +++ b/src/qt_common/shared_translation.h @@ -38,8 +38,12 @@ static const std::map scaling_filter_texts_map {Settings::ScalingFilter::Bilinear, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "Bilinear"))}, {Settings::ScalingFilter::Bicubic, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "Bicubic"))}, + {Settings::ScalingFilter::Spline1, + QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "Spline-1"))}, {Settings::ScalingFilter::Gaussian, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "Gaussian"))}, + {Settings::ScalingFilter::Lanczos, + QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "Lanczos"))}, {Settings::ScalingFilter::ScaleForce, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "ScaleForce"))}, {Settings::ScalingFilter::Fsr, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "FSR"))}, diff --git a/src/video_core/host_shaders/CMakeLists.txt b/src/video_core/host_shaders/CMakeLists.txt index 688e10d2e4..c14b44a45a 100644 --- a/src/video_core/host_shaders/CMakeLists.txt +++ b/src/video_core/host_shaders/CMakeLists.txt @@ -1,5 +1,5 @@ -# SPDX-FileCopyrightText: 2018 yuzu Emulator Project -# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +# SPDX-License-Identifier: GPL-3.0-or-later set(FIDELITYFX_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/externals/FidelityFX-FSR/ffx-fsr) @@ -45,6 +45,8 @@ set(SHADER_FILES present_area.frag present_bicubic.frag present_gaussian.frag + present_lanczos.frag + present_spline1.frag queries_prefix_scan_sum.comp queries_prefix_scan_sum_nosubgroups.comp resolve_conditional_render.comp diff --git a/src/video_core/host_shaders/present_lanczos.frag b/src/video_core/host_shaders/present_lanczos.frag new file mode 100644 index 0000000000..b69b329c1b --- /dev/null +++ b/src/video_core/host_shaders/present_lanczos.frag @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// https://en.wikipedia.org/wiki/Lanczos_resampling + +#version 460 core + +layout (location = 0) in vec2 frag_tex_coord; +layout (location = 0) out vec4 color; +layout (binding = 0) uniform sampler2D color_texture; + +#define PI 3.1415926535897932384626433 +float sinc(float x) { + return x == 0.0f ? 1.0f : sin(PI * x) / (PI * x); +} +float lanczos(vec2 v, float a) { + float d = length(v); + return sinc(d) / sinc(d / a); +} +vec4 textureLanczos(sampler2D textureSampler, vec2 p) { + vec3 c_sum = vec3(0.0f); + float w_sum = 0.0f; + vec2 res = vec2(textureSize(textureSampler, 0)); + vec2 cc = floor(p * res) / res; + // kernel size = (2r + 1)^2 + const int r = 3; //radius (1 = 3 steps) + for (int x = -r; x <= r; x++) + for (int y = -r; y <= r; y++) { + vec2 kp = 0.5f * (vec2(x, y) / res); // 0.5 = half-pixel level resampling + vec2 uv = cc + kp; + float w = lanczos(kp, float(r)); + c_sum += w * texture(textureSampler, p + kp).rgb; + w_sum += w; + } + return vec4(c_sum / w_sum, 1.0f); +} + +void main() { + color = textureLanczos(color_texture, frag_tex_coord); +} diff --git a/src/video_core/host_shaders/present_spline1.frag b/src/video_core/host_shaders/present_spline1.frag new file mode 100644 index 0000000000..871b47586b --- /dev/null +++ b/src/video_core/host_shaders/present_spline1.frag @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// Spline (smooth linear inerpolation) with 1 texel fetch (needs bilinear to work) +// Emulates bicubic without actually doing bicubic +// See https://iquilezles.org/articles/texture, unfortunely there are issues with the original +// where smoothstep "expansion" actually results in worse codegen (SPIRV-Opt does a direct conv to smoothstep) +// TODO: Numerical analysis - fract is sawtooth func and floor, reuse params? Perhaps - no need for precision + +#version 460 core + +layout (location = 0) in vec2 frag_tex_coord; +layout (location = 0) out vec4 color; +layout (binding = 0) uniform sampler2D color_texture; + +vec4 textureSpline1(sampler2D sam, vec2 uv) { + float r = float(textureSize(sam, 0).x); + vec2 x = fract(uv * r + 0.5); + return texture(sam, (floor(uv * r + 0.5) + smoothstep(0.0, 1.0, x) - 0.5) / r); +} + +void main() { + color = textureSpline1(color_texture, frag_tex_coord); +} diff --git a/src/video_core/renderer_opengl/gl_blit_screen.cpp b/src/video_core/renderer_opengl/gl_blit_screen.cpp index 2071fe8d15..65670fcad8 100644 --- a/src/video_core/renderer_opengl/gl_blit_screen.cpp +++ b/src/video_core/renderer_opengl/gl_blit_screen.cpp @@ -89,6 +89,12 @@ void BlitScreen::CreateWindowAdapt() { case Settings::ScalingFilter::Gaussian: window_adapt = MakeGaussian(device); break; + case Settings::ScalingFilter::Spline1: + window_adapt = MakeSpline1(device); + break; + case Settings::ScalingFilter::Lanczos: + window_adapt = MakeLanczos(device); + break; case Settings::ScalingFilter::ScaleForce: window_adapt = MakeScaleForce(device); break; diff --git a/src/video_core/renderer_opengl/present/filters.cpp b/src/video_core/renderer_opengl/present/filters.cpp index c5ac8e7823..a840de304e 100644 --- a/src/video_core/renderer_opengl/present/filters.cpp +++ b/src/video_core/renderer_opengl/present/filters.cpp @@ -12,6 +12,7 @@ #include "video_core/host_shaders/present_area_frag.h" #include "video_core/host_shaders/present_bicubic_frag.h" #include "video_core/host_shaders/present_gaussian_frag.h" +#include "video_core/host_shaders/present_lanczos_frag.h" #include "video_core/renderer_opengl/present/filters.h" #include "video_core/renderer_opengl/present/util.h" @@ -27,6 +28,11 @@ std::unique_ptr MakeBilinear(const Device& device) { HostShaders::OPENGL_PRESENT_FRAG); } +std::unique_ptr MakeSpline1(const Device& device) { + return std::make_unique(device, CreateBilinearSampler(), + HostShaders::PRESENT_SPLINE1_FRAG); +} + std::unique_ptr MakeBicubic(const Device& device) { return std::make_unique(device, CreateBilinearSampler(), HostShaders::PRESENT_BICUBIC_FRAG); @@ -37,6 +43,11 @@ std::unique_ptr MakeGaussian(const Device& device) { HostShaders::PRESENT_GAUSSIAN_FRAG); } +std::unique_ptr MakeLanczos(const Device& device) { + return std::make_unique(device, CreateBilinearSampler(), + HostShaders::PRESENT_LANCZOS_FRAG); +} + std::unique_ptr MakeScaleForce(const Device& device) { return std::make_unique( device, CreateBilinearSampler(), diff --git a/src/video_core/renderer_opengl/present/filters.h b/src/video_core/renderer_opengl/present/filters.h index be2ce24842..7b38ac56bc 100644 --- a/src/video_core/renderer_opengl/present/filters.h +++ b/src/video_core/renderer_opengl/present/filters.h @@ -18,6 +18,8 @@ std::unique_ptr MakeNearestNeighbor(const Device& device); std::unique_ptr MakeBilinear(const Device& device); std::unique_ptr MakeBicubic(const Device& device); std::unique_ptr MakeGaussian(const Device& device); +std::unique_ptr MakeSpline1(const Device& device); +std::unique_ptr MakeLanczos(const Device& device); std::unique_ptr MakeScaleForce(const Device& device); std::unique_ptr MakeArea(const Device& device); diff --git a/src/video_core/renderer_vulkan/present/filters.cpp b/src/video_core/renderer_vulkan/present/filters.cpp index 7843f38d2c..6622b8daea 100644 --- a/src/video_core/renderer_vulkan/present/filters.cpp +++ b/src/video_core/renderer_vulkan/present/filters.cpp @@ -12,6 +12,7 @@ #include "video_core/host_shaders/present_area_frag_spv.h" #include "video_core/host_shaders/present_bicubic_frag_spv.h" #include "video_core/host_shaders/present_gaussian_frag_spv.h" +#include "video_core/host_shaders/present_lanczos_frag_spv.h" #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" @@ -45,6 +46,11 @@ std::unique_ptr MakeBilinear(const Device& device, VkFormat fra BuildShader(device, VULKAN_PRESENT_FRAG_SPV)); } +std::unique_ptr MakeSpline1(const Device& device, VkFormat frame_format) { + return std::make_unique(device, frame_format, CreateBilinearSampler(device), + BuildShader(device, PRESENT_SPLINE1_FRAG_SPV)); +} + std::unique_ptr MakeBicubic(const Device& device, VkFormat frame_format) { // No need for handrolled shader -- if the VK impl can do it for us ;) if (device.IsExtFilterCubicSupported()) @@ -59,6 +65,11 @@ std::unique_ptr MakeGaussian(const Device& device, VkFormat fra BuildShader(device, PRESENT_GAUSSIAN_FRAG_SPV)); } +std::unique_ptr MakeLanczos(const Device& device, VkFormat frame_format) { + return std::make_unique(device, frame_format, CreateBilinearSampler(device), + BuildShader(device, PRESENT_LANCZOS_FRAG_SPV)); +} + std::unique_ptr MakeScaleForce(const Device& device, VkFormat frame_format) { return std::make_unique(device, frame_format, CreateBilinearSampler(device), SelectScaleForceShader(device)); diff --git a/src/video_core/renderer_vulkan/present/filters.h b/src/video_core/renderer_vulkan/present/filters.h index c8259487f8..015bffc8a5 100644 --- a/src/video_core/renderer_vulkan/present/filters.h +++ b/src/video_core/renderer_vulkan/present/filters.h @@ -18,7 +18,9 @@ class MemoryAllocator; std::unique_ptr MakeNearestNeighbor(const Device& device, VkFormat frame_format); std::unique_ptr MakeBilinear(const Device& device, VkFormat frame_format); std::unique_ptr MakeBicubic(const Device& device, VkFormat frame_format); +std::unique_ptr MakeSpline1(const Device& device, VkFormat frame_format); std::unique_ptr MakeGaussian(const Device& device, VkFormat frame_format); +std::unique_ptr MakeLanczos(const Device& device, VkFormat frame_format); std::unique_ptr MakeScaleForce(const Device& device, VkFormat frame_format); std::unique_ptr MakeArea(const Device& device, VkFormat frame_format); diff --git a/src/video_core/renderer_vulkan/vk_blit_screen.cpp b/src/video_core/renderer_vulkan/vk_blit_screen.cpp index 39f07b966d..b720bcded3 100644 --- a/src/video_core/renderer_vulkan/vk_blit_screen.cpp +++ b/src/video_core/renderer_vulkan/vk_blit_screen.cpp @@ -43,9 +43,15 @@ void BlitScreen::SetWindowAdaptPass() { case Settings::ScalingFilter::Bicubic: window_adapt = MakeBicubic(device, swapchain_view_format); break; + case Settings::ScalingFilter::Spline1: + window_adapt = MakeSpline1(device, swapchain_view_format); + break; case Settings::ScalingFilter::Gaussian: window_adapt = MakeGaussian(device, swapchain_view_format); break; + case Settings::ScalingFilter::Lanczos: + window_adapt = MakeLanczos(device, swapchain_view_format); + break; case Settings::ScalingFilter::ScaleForce: window_adapt = MakeScaleForce(device, swapchain_view_format); break; diff --git a/tools/lanczos_gen.c b/tools/lanczos_gen.c new file mode 100644 index 0000000000..6d7be3cb0e --- /dev/null +++ b/tools/lanczos_gen.c @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// clang -lm tools/lanczos_gen.c -o tools/lanczos_gen && ./tools/lanczos_gen +#include +#include + +double sinc(double x) { + return x == 0.0f ? 1.0f : sin(M_PI * x) / (M_PI * x); +} + +typedef struct vec2 { + double x; + double y; +} vec2; + +double lanczos(vec2 v, float a) { + double d = sqrt(v.x * v.x + v.y * v.y); + return sinc(d) / sinc(d / a); +} + +int main(int argc, char* argv[]) { + const int r = 3; //radius (1 = 3 steps) + const int k_size = (r * 2 + 1) * (r * 2 + 1); + double w_sum = 0.0f; + // kernel size = (r * 2 + 1) ^ 2 + printf("const float w_kernel[%i] = float[] (\n ", k_size); + double factor = 1.0f / ((double)r + 1.0f); + for (int x = -r; x <= r; x++) + for (int y = -r; y <= r; y++) { + double w = lanczos((vec2){ .x = x, .y = y }, (double)r); + printf("%lff, ", w); + w_sum += w; + } + printf("\n);\n"); + printf("const vec2 w_pos[%i] = vec2[] (\n ", k_size); + for (int x = -r; x <= r; x++) + for (int y = -r; y <= r; y++) { + vec2 kp = (vec2){ + .x = x * factor, + .y = y * factor + }; + printf("vec2(%lff, %lff), ", kp.x, kp.y); + } + printf("\n);\n"); + printf("const float w_sum = %lff;\n", w_sum); + return 0; +}