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 3a1e4bc924..a9707a15c7 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -1017,9 +1017,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 2a3116bcdf..8022d8216e 100644
--- a/src/common/settings_enums.h
+++ b/src/common/settings_enums.h
@@ -143,7 +143,7 @@ ENUM(ConfirmStop, Ask_Always, Ask_Based_On_Game, Ask_Never);
ENUM(FullscreenMode, Borderless, Exclusive);
ENUM(NvdecEmulation, Off, Cpu, Gpu);
ENUM(ResolutionSetup, Res1_4X, Res1_2X, Res3_4X, Res1X, Res3_2X, Res2X, Res3X, Res4X, Res5X, Res6X, 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);
ENUM(AspectRatio, R16_9, R4_3, R21_9, R16_10, Stretch);
ENUM(ConsoleMode, Handheld, Docked);
diff --git a/src/qt_common/shared_translation.cpp b/src/qt_common/shared_translation.cpp
index ac65e94303..2cac28a937 100644
--- a/src/qt_common/shared_translation.cpp
+++ b/src/qt_common/shared_translation.cpp
@@ -554,15 +554,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: