mirror of
https://git.citron-emu.org/citron/emulator
synced 2025-12-19 10:43:33 +00:00
feat(renderer): Add ScaleFX pixel art upscaling filter
Implements ScaleFX algorithm for pixel art upscaling with edge preservation. Supports both OpenGL and Vulkan with FP16/FP32 variants for hardware optimization. ScaleFX is designed to reduce pixelation while preserving sharp edges, ideal for low-resolution and pixel art games. Signed-off-by: Zephyron <zephyron@citron-emu.org>
This commit is contained in:
@@ -406,6 +406,7 @@ std::unique_ptr<ComboboxTranslationMap> 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")),
|
||||
}});
|
||||
|
||||
@@ -42,6 +42,8 @@ static const std::map<Settings::ScalingFilter, QString> 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"))},
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
143
src/video_core/host_shaders/opengl_present_scalefx.frag
Normal file
143
src/video_core/host_shaders/opengl_present_scalefx.frag
Normal file
@@ -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));
|
||||
}
|
||||
12
src/video_core/host_shaders/vulkan_present_scalefx_fp16.frag
Normal file
12
src/video_core/host_shaders/vulkan_present_scalefx_fp16.frag
Normal file
@@ -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"
|
||||
10
src/video_core/host_shaders/vulkan_present_scalefx_fp32.frag
Normal file
10
src/video_core/host_shaders/vulkan_present_scalefx_fp32.frag
Normal file
@@ -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"
|
||||
@@ -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:
|
||||
|
||||
@@ -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<WindowAdaptPass> MakeScaleForce(const Device& device) {
|
||||
fmt::format("#version 460\n{}", HostShaders::OPENGL_PRESENT_SCALEFORCE_FRAG));
|
||||
}
|
||||
|
||||
std::unique_ptr<WindowAdaptPass> MakeScaleFx(const Device& device) {
|
||||
return std::make_unique<WindowAdaptPass>(
|
||||
device, CreateBilinearSampler(),
|
||||
fmt::format("#version 460\n{}", HostShaders::OPENGL_PRESENT_SCALEFX_FRAG));
|
||||
}
|
||||
|
||||
std::unique_ptr<WindowAdaptPass> MakeLanczos(const Device& device) {
|
||||
return std::make_unique<WindowAdaptPass>(device, CreateNearestNeighborSampler(),
|
||||
HostShaders::PRESENT_LANCZOS_FRAG);
|
||||
|
||||
@@ -15,5 +15,6 @@ std::unique_ptr<WindowAdaptPass> MakeBicubic(const Device& device);
|
||||
std::unique_ptr<WindowAdaptPass> MakeLanczos(const Device& device);
|
||||
std::unique_ptr<WindowAdaptPass> MakeGaussian(const Device& device);
|
||||
std::unique_ptr<WindowAdaptPass> MakeScaleForce(const Device& device);
|
||||
std::unique_ptr<WindowAdaptPass> MakeScaleFx(const Device& device);
|
||||
|
||||
} // namespace OpenGL
|
||||
|
||||
@@ -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<WindowAdaptPass> MakeNearestNeighbor(const Device& device, VkFormat frame_format) {
|
||||
@@ -55,6 +65,11 @@ std::unique_ptr<WindowAdaptPass> MakeScaleForce(const Device& device, VkFormat f
|
||||
SelectScaleForceShader(device));
|
||||
}
|
||||
|
||||
std::unique_ptr<WindowAdaptPass> MakeScaleFx(const Device& device, VkFormat frame_format) {
|
||||
return std::make_unique<WindowAdaptPass>(device, frame_format, CreateBilinearSampler(device),
|
||||
SelectScaleFxShader(device));
|
||||
}
|
||||
|
||||
std::unique_ptr<WindowAdaptPass> MakeLanczos(const Device& device, VkFormat frame_format) {
|
||||
return std::make_unique<WindowAdaptPass>(device, frame_format, CreateNearestNeighborSampler(device),
|
||||
BuildShader(device, PRESENT_LANCZOS_FRAG_SPV));
|
||||
|
||||
@@ -16,5 +16,6 @@ std::unique_ptr<WindowAdaptPass> MakeBicubic(const Device& device, VkFormat fram
|
||||
std::unique_ptr<WindowAdaptPass> MakeLanczos(const Device& device, VkFormat frame_format);
|
||||
std::unique_ptr<WindowAdaptPass> MakeGaussian(const Device& device, VkFormat frame_format);
|
||||
std::unique_ptr<WindowAdaptPass> MakeScaleForce(const Device& device, VkFormat frame_format);
|
||||
std::unique_ptr<WindowAdaptPass> MakeScaleFx(const Device& device, VkFormat frame_format);
|
||||
|
||||
} // namespace Vulkan
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user