From d16d86ee087c1080c2bfb4456a379f3780326ee7 Mon Sep 17 00:00:00 2001 From: lizzie Date: Thu, 25 Sep 2025 17:25:04 +0000 Subject: [PATCH] add MMPX filter Signed-off-by: lizzie --- docs/User.md | 18 +++ .../app/src/main/res/values/arrays.xml | 2 + .../app/src/main/res/values/strings.xml | 5 +- src/common/settings_enums.h | 2 +- src/qt_common/shared_translation.cpp | 5 +- src/qt_common/shared_translation.h | 5 +- src/video_core/host_shaders/CMakeLists.txt | 1 + .../host_shaders/present_bicubic.frag | 2 + .../host_shaders/present_bspline.frag | 4 +- .../host_shaders/present_mitchell.frag | 4 +- src/video_core/host_shaders/present_mmpx.frag | 131 ++++++++++++++++++ .../host_shaders/present_zero_tangent.frag | 4 +- .../renderer_opengl/gl_blit_screen.cpp | 3 + .../renderer_opengl/present/filters.cpp | 6 + .../renderer_opengl/present/filters.h | 1 + .../renderer_vulkan/present/filters.cpp | 6 + .../renderer_vulkan/present/filters.h | 1 + .../renderer_vulkan/vk_blit_screen.cpp | 3 + 18 files changed, 190 insertions(+), 13 deletions(-) create mode 100644 src/video_core/host_shaders/present_mmpx.frag diff --git a/docs/User.md b/docs/User.md index cfc81063f8..b8da9bc8d5 100644 --- a/docs/User.md +++ b/docs/User.md @@ -9,3 +9,21 @@ Eden will store configuration in the following directories: - **Linux, macOS, FreeBSD, Solaris, OpenBSD**: `$XDG_DATA_HOME`, `$XDG_CACHE_HOME`, `$XDG_CONFIG_HOME`. If a `user` directory is present in the current working directory, that will override all global configuration directories and the emulator will use that instead. + +# Enhancements + +## Filters + +Various graphical filters exist - each of them aimed at a specific target/image quality preset. + +- **Nearest**: Provides no filtering - useful for debugging. +- **Bilinear**: Provides the hardware default filtering of the Tegra X1. +- **Bicubic**: Provides a bicubic interpolation using a Catmull-Rom (or hardware-accelerated) implementation. +- **Zero-Tangent, B-Spline, Mitchell**: Provides bicubic interpolation using the respective matrix weights. They're normally not hardware accelerated unless the device supports the `VK_QCOM_filter_cubic_weights` extension. The matrix weights are those matching [the specification itself](https://registry.khronos.org/vulkan/specs/latest/html/vkspec.html#VkSamplerCubicWeightsCreateInfoQCOM). +- **Spline-1**: Bicubic interpolation (similar to Mitchell) but with a faster texel fetch method. Generally less blurry than bicubic. +- **Gaussian**: Whole-area blur, an applied gaussian blur is done to the entire frame. +- **Lanczos**: An implementation using `a = 3` (49 texel fetches). Provides sharper edges but blurrier artifacts. +- **ScaleForce**: Experimental texture upscale method, see [ScaleFish](https://github.com/BreadFish64/ScaleFish). +- **FSR**: Uses AMD FidelityFX Super Resolution to enhance image quality. +- **Area**: Area interpolation (high kernel count). +- **MMPX**: Nearest-neighbour filter aimed at providing higher pixel-art quality. diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml index fa9a02aead..86fae9ba3c 100644 --- a/src/android/app/src/main/res/values/arrays.xml +++ b/src/android/app/src/main/res/values/arrays.xml @@ -260,6 +260,7 @@ @string/scaling_filter_scale_force @string/scaling_filter_fsr @string/scaling_filter_area + @string/scaling_filter_mmpx @@ -275,6 +276,7 @@ 9 10 11 + 12 diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index e6b616128c..9872d69d18 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -991,9 +991,10 @@ ScaleForce AMD FidelityFX™ Super Resolution Area - Zero-Tangent-Cardinal + Zero-Tangent B-Spline - Mitchell-Netravali + Mitchell + MMPX None diff --git a/src/common/settings_enums.h b/src/common/settings_enums.h index 49fdaf686a..f7dc5dc6c8 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, ZeroTangent, BSpline, Mitchell, Spline1, Gaussian, Lanczos, ScaleForce, Fsr, Area, MaxEnum); +ENUM(ScalingFilter, NearestNeighbor, Bilinear, Bicubic, ZeroTangent, BSpline, Mitchell, Spline1, Gaussian, Lanczos, ScaleForce, Fsr, Area, Mmpx, MaxEnum); ENUM(AntiAliasing, None, Fxaa, Smaa, MaxEnum); diff --git a/src/qt_common/shared_translation.cpp b/src/qt_common/shared_translation.cpp index fea986cf0a..dc20fbf246 100644 --- a/src/qt_common/shared_translation.cpp +++ b/src/qt_common/shared_translation.cpp @@ -570,15 +570,16 @@ std::unique_ptr ComboboxEnumeration(QObject* parent) PAIR(ScalingFilter, NearestNeighbor, tr("Nearest Neighbor")), PAIR(ScalingFilter, Bilinear, tr("Bilinear")), PAIR(ScalingFilter, Bicubic, tr("Bicubic")), - PAIR(ScalingFilter, ZeroTangent, tr("Zero-Tangent-Cardinal")), + PAIR(ScalingFilter, ZeroTangent, tr("Zero-Tangent")), PAIR(ScalingFilter, BSpline, tr("B-Spline")), - PAIR(ScalingFilter, Mitchell, tr("Mitchell-Netravali")), + PAIR(ScalingFilter, Mitchell, tr("Mitchell")), 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")), + PAIR(ScalingFilter, Mmpx, tr("MMPX")), }}); translations->insert({Settings::EnumMetadata::Index(), { diff --git a/src/qt_common/shared_translation.h b/src/qt_common/shared_translation.h index 801d27c416..a25887bb87 100644 --- a/src/qt_common/shared_translation.h +++ b/src/qt_common/shared_translation.h @@ -38,9 +38,9 @@ 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::ZeroTangent, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "Zero-Tangent-Cardinal"))}, + {Settings::ScalingFilter::ZeroTangent, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "Zero-Tangent"))}, {Settings::ScalingFilter::BSpline, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "B-Spline"))}, - {Settings::ScalingFilter::Mitchell, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "Mitchell-Netravali"))}, + {Settings::ScalingFilter::Mitchell, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "Mitchell"))}, {Settings::ScalingFilter::Spline1, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "Spline-1"))}, {Settings::ScalingFilter::Gaussian, @@ -51,6 +51,7 @@ static const std::map scaling_filter_texts_map QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "ScaleForce"))}, {Settings::ScalingFilter::Fsr, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "FSR"))}, {Settings::ScalingFilter::Area, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "Area"))}, + {Settings::ScalingFilter::Mmpx, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "MMPX"))}, }; static const std::map use_docked_mode_texts_map = { diff --git a/src/video_core/host_shaders/CMakeLists.txt b/src/video_core/host_shaders/CMakeLists.txt index 4bbeb1e33f..9f7b9edd5a 100644 --- a/src/video_core/host_shaders/CMakeLists.txt +++ b/src/video_core/host_shaders/CMakeLists.txt @@ -50,6 +50,7 @@ set(SHADER_FILES present_gaussian.frag present_lanczos.frag present_spline1.frag + present_mmpx.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_bicubic.frag b/src/video_core/host_shaders/present_bicubic.frag index a03b330165..5347fd2ef7 100644 --- a/src/video_core/host_shaders/present_bicubic.frag +++ b/src/video_core/host_shaders/present_bicubic.frag @@ -1,3 +1,5 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #version 460 core diff --git a/src/video_core/host_shaders/present_bspline.frag b/src/video_core/host_shaders/present_bspline.frag index 92fd6f646b..f229de6030 100644 --- a/src/video_core/host_shaders/present_bspline.frag +++ b/src/video_core/host_shaders/present_bspline.frag @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: Copyright 2021 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 #version 460 core layout (location = 0) in vec2 frag_tex_coord; layout (location = 0) out vec4 color; diff --git a/src/video_core/host_shaders/present_mitchell.frag b/src/video_core/host_shaders/present_mitchell.frag index 7a3efa79a1..4ca65cd6f0 100644 --- a/src/video_core/host_shaders/present_mitchell.frag +++ b/src/video_core/host_shaders/present_mitchell.frag @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: Copyright 2021 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 #version 460 core layout (location = 0) in vec2 frag_tex_coord; layout (location = 0) out vec4 color; diff --git a/src/video_core/host_shaders/present_mmpx.frag b/src/video_core/host_shaders/present_mmpx.frag new file mode 100644 index 0000000000..6c2c05a63a --- /dev/null +++ b/src/video_core/host_shaders/present_mmpx.frag @@ -0,0 +1,131 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#version 460 core +layout(location = 0) in vec2 tex_coord; +layout(location = 0) out vec4 frag_color; +layout(binding = 0) uniform sampler2D tex; + +#define src(x, y) texture(tex, coord + vec2(x, y) * 1.0 / source_size) + +float luma(vec4 col) { + return dot(col.rgb, vec3(0.2126, 0.7152, 0.0722)) * (1.0 - col.a); +} + +bool same(vec4 B, vec4 A0) { + return all(equal(B, A0)); +} + +bool notsame(vec4 B, vec4 A0) { + return any(notEqual(B, A0)); +} + +bool all_eq2(vec4 B, vec4 A0, vec4 A1) { + return (same(B,A0) && same(B,A1)); +} + +bool all_eq3(vec4 B, vec4 A0, vec4 A1, vec4 A2) { + return (same(B,A0) && same(B,A1) && same(B,A2)); +} + +bool all_eq4(vec4 B, vec4 A0, vec4 A1, vec4 A2, vec4 A3) { + return (same(B,A0) && same(B,A1) && same(B,A2) && same(B,A3)); +} + +bool any_eq3(vec4 B, vec4 A0, vec4 A1, vec4 A2) { + return (same(B,A0) || same(B,A1) || same(B,A2)); +} + +bool none_eq2(vec4 B, vec4 A0, vec4 A1) { + return (notsame(B,A0) && notsame(B,A1)); +} + +bool none_eq4(vec4 B, vec4 A0, vec4 A1, vec4 A2, vec4 A3) { + return (notsame(B,A0) && notsame(B,A1) && notsame(B,A2) && notsame(B,A3)); +} + +void main() +{ + vec2 source_size = vec2(textureSize(tex, 0)); + vec2 pos = fract(tex_coord * source_size) - vec2(0.5, 0.5); + vec2 coord = tex_coord - pos / source_size; + + vec4 E = src(0.0,0.0); + + vec4 A = src(-1.0,-1.0); + vec4 B = src(0.0,-1.0); + vec4 C = src(1.0,-1.0); + + vec4 D = src(-1.0,0.0); + vec4 F = src(1.0,0.0); + + vec4 G = src(-1.0,1.0); + vec4 H = src(0.0,1.0); + vec4 I = src(1.0,1.0); + + vec4 J = E; + vec4 K = E; + vec4 L = E; + vec4 M = E; + + frag_color = E; + + if(same(E,A) && same(E,B) && same(E,C) && same(E,D) && same(E,F) && same(E,G) && same(E,H) && same(E,I)) return; + + vec4 P = src(0.0,2.0); + vec4 Q = src(-2.0,0.0); + vec4 R = src(2.0,0.0); + vec4 S = src(0.0,2.0); + + float Bl = luma(B); + float Dl = luma(D); + float El = luma(E); + float Fl = luma(F); + float Hl = luma(H); + + if (((same(D,B) && notsame(D,H) && notsame(D,F))) && ((El>=Dl) || same(E,A)) && any_eq3(E,A,C,G) && ((El=Bl) || same(E,C)) && any_eq3(E,A,C,I) && ((El=Hl) || same(E,G)) && any_eq3(E,A,G,I) && ((El=Fl) || same(E,I)) && any_eq3(E,C,G,I) && ((El MakeArea(const Device& device) { HostShaders::PRESENT_AREA_FRAG); } +std::unique_ptr MakeMmpx(const Device& device) { + return std::make_unique(device, CreateNearestNeighborSampler(), + HostShaders::PRESENT_MMPX_FRAG); +} + } // namespace OpenGL diff --git a/src/video_core/renderer_opengl/present/filters.h b/src/video_core/renderer_opengl/present/filters.h index 0fba4408dc..187d0f1298 100644 --- a/src/video_core/renderer_opengl/present/filters.h +++ b/src/video_core/renderer_opengl/present/filters.h @@ -25,5 +25,6 @@ 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); +std::unique_ptr MakeMmpx(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 e3c457b44c..0a28ea6349 100644 --- a/src/video_core/renderer_vulkan/present/filters.cpp +++ b/src/video_core/renderer_vulkan/present/filters.cpp @@ -19,6 +19,7 @@ #include "video_core/host_shaders/present_mitchell_frag_spv.h" #include "video_core/host_shaders/present_bspline_frag_spv.h" #include "video_core/host_shaders/present_zero_tangent_frag_spv.h" +#include "video_core/host_shaders/present_mmpx_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" @@ -101,4 +102,9 @@ std::unique_ptr MakeArea(const Device& device, VkFormat frame_f BuildShader(device, PRESENT_AREA_FRAG_SPV)); } +std::unique_ptr MakeMmpx(const Device& device, VkFormat frame_format) { + return std::make_unique(device, frame_format, CreateNearestNeighborSampler(device), + BuildShader(device, PRESENT_MMPX_FRAG_SPV)); +} + } // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/filters.h b/src/video_core/renderer_vulkan/present/filters.h index 2f02003235..afc3ba29a0 100644 --- a/src/video_core/renderer_vulkan/present/filters.h +++ b/src/video_core/renderer_vulkan/present/filters.h @@ -23,5 +23,6 @@ std::unique_ptr MakeGaussian(const Device& device, VkFormat fra 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); +std::unique_ptr MakeMmpx(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 14a914c0b3..0f54dd5ade 100644 --- a/src/video_core/renderer_vulkan/vk_blit_screen.cpp +++ b/src/video_core/renderer_vulkan/vk_blit_screen.cpp @@ -68,6 +68,9 @@ void BlitScreen::SetWindowAdaptPass() { case Settings::ScalingFilter::Area: window_adapt = MakeArea(device, swapchain_view_format); break; + case Settings::ScalingFilter::Mmpx: + window_adapt = MakeMmpx(device, swapchain_view_format); + break; case Settings::ScalingFilter::Fsr: case Settings::ScalingFilter::Bilinear: default: