Merge branch 'feat_renderer_Lanczos' into 'main'

feat(renderer): Add configurable Lanczos upscaling filter

See merge request citron/emulator!84
This commit is contained in:
Zephyron
2025-10-06 12:22:40 +10:00
14 changed files with 160 additions and 9 deletions

View File

@@ -127,6 +127,7 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QWidget* parent) {
INSERT(Settings, scaling_filter, tr("Window Adapting Filter:"), QStringLiteral());
INSERT(Settings, fsr_sharpening_slider, tr("FSR Sharpness:"),
tr("Determines how sharpened the image will look while using FSR's dynamic contrast."));
INSERT(Settings, lanczos_quality, tr("Lanczos Quality:"), tr("The quality of the Lanczos filter. Higher is sharper but more expensive."));
INSERT(Settings, fsr2_quality_mode, tr("FSR 2.0 Quality Mode:"),
tr("Selects the quality mode for FSR 2.0 upscaling. Quality provides better image quality, Performance provides better performance."));
@@ -401,6 +402,7 @@ std::unique_ptr<ComboboxTranslationMap> ComboboxEnumeration(QWidget* parent) {
PAIR(ScalingFilter, NearestNeighbor, tr("Nearest Neighbor")),
PAIR(ScalingFilter, Bilinear, tr("Bilinear")),
PAIR(ScalingFilter, Bicubic, tr("Bicubic")),
PAIR(ScalingFilter, Lanczos, tr("Lanczos")),
PAIR(ScalingFilter, Gaussian, tr("Gaussian")),
PAIR(ScalingFilter, ScaleForce, tr("ScaleForce")),
PAIR(ScalingFilter, Fsr, tr("AMD FidelityFX™ Super Resolution")),

View File

@@ -39,6 +39,7 @@ static const std::map<Settings::ScalingFilter, QString> scaling_filter_texts_map
{Settings::ScalingFilter::Bicubic, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "Bicubic"))},
{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"))},

View File

@@ -348,6 +348,17 @@ struct Values {
true,
true};
SwitchableSetting<int, true> lanczos_quality{linkage,
3, // Default value
2, // Minimum value
4, // Maximum value
"lanczos_quality",
Category::Renderer,
Specialization::Scalar,
true,
true};
SwitchableSetting<FSR2QualityMode, true> fsr2_quality_mode{linkage,
FSR2QualityMode::Quality, // Quality by default
FSR2QualityMode::Quality, // Min value

View File

@@ -147,7 +147,7 @@ ENUM(NvdecEmulation, Off, Cpu, Gpu);
ENUM(ResolutionSetup, Res1_2X, Res3_4X, Res1X, Res3_2X, Res2X, Res3X, Res4X, Res5X, Res6X, Res7X,
Res8X);
ENUM(ScalingFilter, NearestNeighbor, Bilinear, Bicubic, Gaussian, ScaleForce, Fsr, Fsr2, MaxEnum);
ENUM(ScalingFilter, NearestNeighbor, Bilinear, Bicubic, Gaussian, ScaleForce, Lanczos, Fsr, Fsr2, MaxEnum);
ENUM(AntiAliasing, None, Fxaa, Smaa, Taa, MaxEnum);

View File

@@ -1,4 +1,5 @@
# SPDX-FileCopyrightText: 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
set(FIDELITYFX_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/externals/FidelityFX-FSR/ffx-fsr)
@@ -42,6 +43,7 @@ set(SHADER_FILES
opengl_smaa.glsl
pitch_unswizzle.comp
present_bicubic.frag
present_lanczos.frag
present_gaussian.frag
queries_prefix_scan_sum.comp
queries_prefix_scan_sum_nosubgroups.comp

View File

@@ -0,0 +1,86 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#version 460 core
layout(location = 0) in vec2 frag_tex_coord;
layout(location = 0) out vec4 color;
layout(binding = 0) uniform sampler2D color_texture;
// This block defines the variable that will hold our setting value.
// It uses a preprocessor directive to switch between the OpenGL and Vulkan way of doing things.
#ifdef VULKAN
layout(push_constant) uniform LanczosPushConstant {
layout(offset = 128) int u_lanczos_a;
} lanczos_pc;
#else // OpenGL
layout(location = 0) uniform int u_lanczos_a;
#endif
const float PI = 3.14159265359;
float sinc(float x) {
if (x == 0.0) {
return 1.0;
}
return sin(PI * x) / (PI * x);
}
float lanczos_weight(float x, float a) {
if (abs(x) < a) {
return sinc(x) * sinc(x / a);
}
return 0.0;
}
vec4 textureLanczos(sampler2D ts, vec2 tc) {
// Get the 'a' value from the correct uniform based on the renderer
#ifdef VULKAN
const int a_val = lanczos_pc.u_lanczos_a;
#else
const int a_val = u_lanczos_a;
#endif
const float a = float(a_val);
// If 'a' is 0 (which it will be by default since we aren't sending a value yet),
// just do a basic sample to avoid errors.
if (a_val == 0) {
return texture(ts, tc);
}
vec2 tex_size = vec2(textureSize(ts, 0));
vec2 inv_tex_size = 1.0 / tex_size;
vec2 p = tc * tex_size;
vec2 f = fract(p);
vec2 p_int = p - f;
vec4 sum = vec4(0.0);
float weight_sum = 0.0;
const int MAX_A = 4;
for (int y = -MAX_A + 1; y <= MAX_A; ++y) {
if (abs(y) >= a_val) continue;
for (int x = -MAX_A + 1; x <= MAX_A; ++x) {
if (abs(x) >= a_val) continue;
vec2 offset = vec2(float(x), float(y));
float w = lanczos_weight(f.x - offset.x, a) * lanczos_weight(f.y - offset.y, a);
if (w != 0.0) {
sum += texture(ts, (p_int + offset) * inv_tex_size) * w;
weight_sum += w;
}
}
}
if (weight_sum == 0.0) {
return texture(ts, tc);
}
return sum / weight_sum;
}
void main() {
color = textureLanczos(color_texture, frag_tex_coord);
}

View File

@@ -81,6 +81,9 @@ void BlitScreen::CreateWindowAdapt() {
case Settings::ScalingFilter::Bicubic:
window_adapt = MakeBicubic(device);
break;
case Settings::ScalingFilter::Lanczos:
window_adapt = MakeLanczos(device);
break;
case Settings::ScalingFilter::Gaussian:
window_adapt = MakeGaussian(device);
break;

View File

@@ -1,9 +1,11 @@
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "video_core/host_shaders/opengl_present_frag.h"
#include "video_core/host_shaders/opengl_present_scaleforce_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"
#include "video_core/renderer_opengl/present/filters.h"
#include "video_core/renderer_opengl/present/util.h"
@@ -36,4 +38,9 @@ std::unique_ptr<WindowAdaptPass> MakeScaleForce(const Device& device) {
fmt::format("#version 460\n{}", HostShaders::OPENGL_PRESENT_SCALEFORCE_FRAG));
}
std::unique_ptr<WindowAdaptPass> MakeLanczos(const Device& device) {
return std::make_unique<WindowAdaptPass>(device, CreateNearestNeighborSampler(),
HostShaders::PRESENT_LANCZOS_FRAG);
}
} // namespace OpenGL

View File

@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@@ -11,6 +12,7 @@ namespace OpenGL {
std::unique_ptr<WindowAdaptPass> MakeNearestNeighbor(const Device& device);
std::unique_ptr<WindowAdaptPass> MakeBilinear(const Device& device);
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);

View File

@@ -64,10 +64,10 @@ void WindowAdaptPass::DrawToFramebuffer(ProgramManager& program_manager, std::li
glViewportIndexedf(0, 0.0f, 0.0f, static_cast<GLfloat>(layout.width),
static_cast<GLfloat>(layout.height));
glEnableVertexAttribArray(PositionLocation);
glEnableVertexAttribArray(TexCoordLocation);
glVertexAttribDivisor(PositionLocation, 0);
glVertexAttribDivisor(TexCoordLocation, 0);
glEnableVertexAttribArray(PositionLocation);
glEnableVertexAttribArray(TexCoordLocation);
glVertexAttribFormat(PositionLocation, 2, GL_FLOAT, GL_FALSE,
offsetof(ScreenRectVertex, position));
glVertexAttribFormat(TexCoordLocation, 2, GL_FLOAT, GL_FALSE,
@@ -84,7 +84,6 @@ void WindowAdaptPass::DrawToFramebuffer(ProgramManager& program_manager, std::li
glBindSampler(0, sampler.handle);
// Update background color before drawing
glClearColor(Settings::values.bg_red.GetValue() / 255.0f,
Settings::values.bg_green.GetValue() / 255.0f,
Settings::values.bg_blue.GetValue() / 255.0f, 1.0f);
@@ -107,6 +106,11 @@ void WindowAdaptPass::DrawToFramebuffer(ProgramManager& program_manager, std::li
break;
}
if (Settings::values.scaling_filter.GetValue() == Settings::ScalingFilter::Lanczos) {
glProgramUniform1i(frag.handle, 0, Settings::values.lanczos_quality.GetValue());
}
glBindTextureUnit(0, textures[i]);
glProgramUniformMatrix3x2fv(vert.handle, ModelViewMatrixLocation, 1, GL_FALSE,
matrices[i].data());

View File

@@ -1,9 +1,11 @@
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/common_types.h"
#include "video_core/host_shaders/present_bicubic_frag_spv.h"
#include "video_core/host_shaders/present_lanczos_frag_spv.h"
#include "video_core/host_shaders/present_gaussian_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"
@@ -53,4 +55,9 @@ std::unique_ptr<WindowAdaptPass> MakeScaleForce(const Device& device, VkFormat f
SelectScaleForceShader(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));
}
} // namespace Vulkan

View File

@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@@ -12,6 +13,7 @@ class MemoryAllocator;
std::unique_ptr<WindowAdaptPass> MakeNearestNeighbor(const Device& device, VkFormat frame_format);
std::unique_ptr<WindowAdaptPass> MakeBilinear(const Device& device, VkFormat frame_format);
std::unique_ptr<WindowAdaptPass> MakeBicubic(const Device& device, VkFormat frame_format);
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);

View File

@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "core/frontend/framebuffer_layout.h"
@@ -12,6 +13,7 @@
#include "video_core/renderer_vulkan/vk_shader_util.h"
#include "video_core/vulkan_common/vulkan_device.h"
#include "video_core/vulkan_common/vulkan_memory_allocator.h"
#include "common/settings.h"
namespace Vulkan {
@@ -92,8 +94,16 @@ void WindowAdaptPass::Draw(RasterizerVulkan& rasterizer, Scheduler& scheduler, s
for (size_t i = 0; i < layer_count; i++) {
cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, graphics_pipelines[i]);
cmdbuf.PushConstants(graphics_pipeline_layout, VK_SHADER_STAGE_VERTEX_BIT,
push_constants[i]);
cmdbuf.PushConstants(graphics_pipeline_layout, VK_SHADER_STAGE_VERTEX_BIT, 0,
sizeof(PresentPushConstants), &push_constants[i]);
if (Settings::values.scaling_filter.GetValue() == Settings::ScalingFilter::Lanczos) {
const s32 lanczos_a = Settings::values.lanczos_quality.GetValue();
cmdbuf.PushConstants(graphics_pipeline_layout, VK_SHADER_STAGE_FRAGMENT_BIT,
sizeof(PresentPushConstants), sizeof(s32), &lanczos_a);
}
cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_GRAPHICS, graphics_pipeline_layout, 0,
descriptor_sets[i], {});
cmdbuf.Draw(4, 1, 0, 0);
@@ -117,20 +127,31 @@ void WindowAdaptPass::CreateDescriptorSetLayout() {
}
void WindowAdaptPass::CreatePipelineLayout() {
const VkPushConstantRange range{
std::array<VkPushConstantRange, 2> ranges{};
// Range 0: The existing constants for the Vertex Shader
ranges[0] = {
.stageFlags = VK_SHADER_STAGE_VERTEX_BIT,
.offset = 0,
.size = sizeof(PresentPushConstants),
};
// Range 1: Our new constant for the Fragment Shader
ranges[1] = {
.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT,
.offset = sizeof(PresentPushConstants),
.size = sizeof(s32),
};
pipeline_layout = device.GetLogical().CreatePipelineLayout(VkPipelineLayoutCreateInfo{
.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
.pNext = nullptr,
.flags = 0,
.setLayoutCount = 1,
.pSetLayouts = descriptor_set_layout.address(),
.pushConstantRangeCount = 1,
.pPushConstantRanges = &range,
.pushConstantRangeCount = static_cast<u32>(ranges.size()),
.pPushConstantRanges = ranges.data(),
});
}

View File

@@ -38,6 +38,9 @@ void BlitScreen::SetWindowAdaptPass() {
case Settings::ScalingFilter::Bicubic:
window_adapt = MakeBicubic(device, swapchain_view_format);
break;
case Settings::ScalingFilter::Lanczos:
window_adapt = MakeLanczos(device, swapchain_view_format);
break;
case Settings::ScalingFilter::Gaussian:
window_adapt = MakeGaussian(device, swapchain_view_format);
break;