Merge pull request 'feat(renderer): add CRT shader filter with configurable effects' (#82) from feature/renderer/crt-shader into main

Reviewed-on: https://git.citron-emu.org/Citron/Emulator/pulls/82
This commit is contained in:
Zephyron
2026-01-01 08:23:31 +00:00
12 changed files with 320 additions and 16 deletions

View File

@@ -163,8 +163,12 @@
<item>@string/scaling_filter_bicubic</item>
<item>@string/scaling_filter_gaussian</item>
<item>@string/scaling_filter_scale_force</item>
<item>@string/scaling_filter_scale_fx</item>
<item>@string/scaling_filter_lanczos</item>
<item>@string/scaling_filter_fsr</item>
<item>@string/scaling_filter_fsr2</item>
<item>@string/scaling_filter_crt_easymode</item>
<item>@string/scaling_filter_crt_royale</item>
</string-array>
<integer-array name="rendererScalingFilterValues">
@@ -175,6 +179,10 @@
<item>4</item>
<item>5</item>
<item>6</item>
<item>7</item>
<item>8</item>
<item>9</item>
<item>10</item>
</integer-array>
<string-array name="fsr2QualityModeNames">

View File

@@ -654,8 +654,12 @@
<string name="scaling_filter_bicubic">Bicubic</string>
<string name="scaling_filter_gaussian">Gaussian</string>
<string name="scaling_filter_scale_force">ScaleForce</string>
<string name="scaling_filter_scale_fx">ScaleFX</string>
<string name="scaling_filter_lanczos">Lanczos</string>
<string name="scaling_filter_fsr">AMD FidelityFX™ Super Resolution</string>
<string name="scaling_filter_fsr2">AMD FidelityFX™ Super Resolution 2.0</string>
<string name="scaling_filter_crt_easymode">CRT EasyMode</string>
<string name="scaling_filter_crt_royale">CRT Royale</string>
<!-- Anti-Aliasing -->
<string name="anti_aliasing_none">None</string>

View File

@@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "citron/configuration/shared_translation.h"
@@ -130,6 +130,20 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QWidget* parent) {
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."));
INSERT(Settings, crt_scanline_strength, tr("CRT Scanline Strength:"),
tr("Controls the intensity of scanlines. Higher values create more pronounced horizontal lines."));
INSERT(Settings, crt_curvature, tr("CRT Curvature:"),
tr("Applies barrel distortion to simulate the curved screen of a CRT monitor."));
INSERT(Settings, crt_gamma, tr("CRT Gamma:"),
tr("Adjusts the gamma correction curve. Higher values brighten the image, lower values darken it."));
INSERT(Settings, crt_bloom, tr("CRT Bloom:"),
tr("Controls the glow effect around bright areas, simulating phosphor persistence."));
INSERT(Settings, crt_mask_type, tr("CRT Mask Type:"),
tr("Selects the phosphor mask pattern: None, Aperture Grille (vertical stripes), or Shadow Mask (triangular pattern)."));
INSERT(Settings, crt_brightness, tr("CRT Brightness:"),
tr("Adjusts overall brightness of the CRT effect. Use to compensate for darkening from other effects."));
INSERT(Settings, crt_alpha, tr("CRT Alpha:"),
tr("Controls transparency of the CRT effect. Lower values make the effect more transparent."));
INSERT(Settings, frame_skipping, tr("Frame Skipping:"),
tr("Skips frames to maintain performance when the system cannot keep up with the target frame rate."));
@@ -426,6 +440,8 @@ std::unique_ptr<ComboboxTranslationMap> ComboboxEnumeration(QWidget* parent) {
PAIR(ScalingFilter, ScaleFx, tr("ScaleFX")),
PAIR(ScalingFilter, Fsr, tr("AMD FidelityFX™ Super Resolution")),
PAIR(ScalingFilter, Fsr2, tr("AMD FidelityFX™ Super Resolution 2.0")),
PAIR(ScalingFilter, CRTEasyMode, tr("CRT EasyMode")),
PAIR(ScalingFilter, CRTRoyale, tr("CRT Royale")),
}});
translations->insert({Settings::EnumMetadata<Settings::AntiAliasing>::Index(),
{

View File

@@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@@ -46,6 +46,10 @@ static const std::map<Settings::ScalingFilter, QString> scaling_filter_texts_map
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"))},
{Settings::ScalingFilter::CRTEasyMode,
QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "CRT EasyMode"))},
{Settings::ScalingFilter::CRTRoyale,
QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "CRT Royale"))},
};
static const std::map<Settings::ConsoleMode, QString> use_docked_mode_texts_map = {

View File

@@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@@ -350,6 +350,71 @@ struct Values {
true,
true};
// CRT Shader Settings (only active when CRT filter is selected)
SwitchableSetting<float, true> crt_scanline_strength{linkage,
1.0f, // 100/100 = 1.0 (range 0-200, actual 0.0-2.0)
0.0f,
2.0f,
"crt_scanline_strength",
Category::Renderer,
Specialization::Scalar,
true,
true};
SwitchableSetting<float, true> crt_curvature{linkage,
0.0f,
0.0f,
1.0f,
"crt_curvature",
Category::Renderer,
Specialization::Scalar,
true,
true};
SwitchableSetting<float, true> crt_gamma{linkage,
1.0f, // 100 maps to 1.0 (range 1-300, actual 1.0-3.0)
1.0f,
3.0f,
"crt_gamma",
Category::Renderer,
Specialization::Scalar,
true,
true};
SwitchableSetting<float, true> crt_bloom{linkage,
0.33f, // 33/100 = 0.33 (range 0-100, actual 0.0-1.0)
0.0f,
1.0f,
"crt_bloom",
Category::Renderer,
Specialization::Scalar,
true,
true};
SwitchableSetting<int, true> crt_mask_type{linkage,
1, // Already correct
0,
2,
"crt_mask_type",
Category::Renderer,
Specialization::Scalar,
true,
true}; // 0=none, 1=aperture, 2=shadow
SwitchableSetting<float, true> crt_brightness{linkage,
1.0f, // Default brightness (1.0 = no change)
0.0f,
2.0f,
"crt_brightness",
Category::Renderer,
Specialization::Scalar,
true,
true};
SwitchableSetting<float, true> crt_alpha{linkage,
1.0f, // Default alpha (1.0 = fully opaque)
0.0f,
1.0f,
"crt_alpha",
Category::Renderer,
Specialization::Scalar,
true,
true};
SwitchableSetting<int, true> lanczos_quality{linkage,
3, // Default value

View File

@@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@@ -673,7 +673,9 @@ enum class ScalingFilter : u32 {
Lanczos = 6,
Fsr = 7,
Fsr2 = 8,
MaxEnum = 9,
CRTEasyMode = 9,
CRTRoyale = 10,
MaxEnum = 11,
};
template <>
@@ -689,6 +691,8 @@ EnumMetadata<ScalingFilter>::Canonicalizations() {
{"Lanczos", ScalingFilter::Lanczos},
{"Fsr", ScalingFilter::Fsr},
{"Fsr2", ScalingFilter::Fsr2},
{"CRTEasyMode", ScalingFilter::CRTEasyMode},
{"CRTRoyale", ScalingFilter::CRTRoyale},
{"MaxEnum", ScalingFilter::MaxEnum},
};
}
@@ -876,6 +880,7 @@ inline u32 EnumMetadata<ExtendedDynamicState>::Index() {
return 26;
}
template <typename Type>
inline std::string CanonicalizeEnum(Type id) {
const auto group = EnumMetadata<Type>::Canonicalizations();

View File

@@ -1,5 +1,5 @@
# SPDX-FileCopyrightText: 2018 yuzu Emulator Project
# SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
# SPDX-FileCopyrightText: 2026 citron Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
set(FIDELITYFX_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/externals/FidelityFX-FSR/ffx-fsr)
@@ -74,6 +74,7 @@ set(SHADER_FILES
vulkan_present_scaleforce_fp32.frag
vulkan_present_scalefx_fp16.frag
vulkan_present_scalefx_fp32.frag
vulkan_crt_easymode.frag
vulkan_quad_indexed.comp
vulkan_taa.frag
vulkan_taa.vert

View File

@@ -0,0 +1,133 @@
// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
//
// CRT EasyMode shader - Single-pass CRT effects
// Based on Libretro's crt-easymode shader
// https://github.com/libretro/common-shaders/blob/master/crt/shaders/crt-easymode.cg
#version 460 core
layout(location = 0) in vec2 frag_tex_coord;
layout(location = 0) out vec4 color;
layout(binding = 0) uniform sampler2D color_texture;
layout(push_constant) uniform CRTPushConstants {
layout(offset = 132) float scanline_strength;
layout(offset = 136) float curvature;
layout(offset = 140) float gamma;
layout(offset = 144) float bloom;
layout(offset = 148) int mask_type;
layout(offset = 152) float brightness;
layout(offset = 156) float alpha;
layout(offset = 160) float screen_width;
layout(offset = 164) float screen_height;
} crt_params;
const float PI = 3.141592653589793;
// Apply barrel distortion (curvature)
vec2 applyCurvature(vec2 coord) {
if (crt_params.curvature <= 0.0) {
return coord;
}
vec2 centered = coord - 0.5;
float dist = length(centered);
float distortion = 1.0 + crt_params.curvature * dist * dist;
vec2 curved = centered * distortion + 0.5;
// Clamp to valid texture coordinates
return clamp(curved, vec2(0.0), vec2(1.0));
}
// Generate scanlines
float scanline(float y) {
if (crt_params.scanline_strength <= 0.0) {
return 1.0;
}
float scanline_pos = y * crt_params.screen_height;
float scanline_factor = abs(sin(scanline_pos * PI));
// Make scanlines more subtle
return 1.0 - crt_params.scanline_strength * scanline_factor * 0.5;
}
// Apply phosphor mask (aperture grille or shadow mask)
vec3 applyMask(vec2 coord) {
if (crt_params.mask_type == 0) {
return vec3(1.0); // No mask
}
vec2 screen_pos = coord * vec2(crt_params.screen_width, crt_params.screen_height);
if (crt_params.mask_type == 1) {
// Aperture grille (vertical RGB stripes)
float mask = sin(screen_pos.x * PI * 3.0) * 0.5 + 0.5;
return vec3(
1.0 - mask * 0.2,
1.0 - mask * 0.15,
1.0 - mask * 0.2
);
} else if (crt_params.mask_type == 2) {
// Shadow mask (triangular pattern)
float x = screen_pos.x * 3.0;
float y = screen_pos.y * 3.0;
float mask = sin(x * PI) * sin(y * PI) * 0.5 + 0.5;
return vec3(1.0 - mask * 0.15);
}
return vec3(1.0);
}
// Simple bloom effect (multi-tap blur approximation)
vec3 applyBloom(vec2 coord, vec3 original) {
if (crt_params.bloom <= 0.0) {
return original;
}
vec2 texel_size = 1.0 / vec2(crt_params.screen_width, crt_params.screen_height);
vec3 bloom_color = original;
// Simple 5-tap horizontal blur
for (int i = -2; i <= 2; i++) {
vec2 offset = vec2(float(i) * texel_size.x, 0.0);
vec3 sample_color = texture(color_texture, clamp(coord + offset, vec2(0.0), vec2(1.0))).rgb;
bloom_color += sample_color;
}
bloom_color /= 6.0; // Average of 5 taps + original
// Mix original with bloom
return mix(original, bloom_color, crt_params.bloom * 0.3);
}
void main() {
// Apply curvature distortion first
vec2 curved_coord = applyCurvature(frag_tex_coord);
// Sample the texture
vec3 rgb = texture(color_texture, curved_coord).rgb;
// Apply bloom
rgb = applyBloom(curved_coord, rgb);
// Apply phosphor mask
rgb *= applyMask(curved_coord);
// Apply scanlines
float scan = scanline(curved_coord.y);
rgb *= scan;
// Gamma correction
if (crt_params.gamma > 0.0 && crt_params.gamma != 1.0) {
rgb = pow(clamp(rgb, vec3(0.0), vec3(1.0)), vec3(1.0 / crt_params.gamma));
}
// Apply brightness adjustment
rgb *= crt_params.brightness;
// Clamp to valid range and apply alpha
color = vec4(clamp(rgb, vec3(0.0), vec3(1.0)), crt_params.alpha);
}

View File

@@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/common_types.h"
@@ -12,6 +12,7 @@
#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/host_shaders/vulkan_crt_easymode_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"
@@ -75,4 +76,9 @@ std::unique_ptr<WindowAdaptPass> MakeLanczos(const Device& device, VkFormat fram
BuildShader(device, PRESENT_LANCZOS_FRAG_SPV));
}
std::unique_ptr<WindowAdaptPass> MakeCRT(const Device& device, VkFormat frame_format) {
return std::make_unique<WindowAdaptPass>(device, frame_format, CreateBilinearSampler(device),
BuildShader(device, VULKAN_CRT_EASYMODE_FRAG_SPV));
}
} // namespace Vulkan

View File

@@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@@ -17,5 +17,6 @@ std::unique_ptr<WindowAdaptPass> MakeLanczos(const Device& device, VkFormat fram
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);
std::unique_ptr<WindowAdaptPass> MakeCRT(const Device& device, VkFormat frame_format);
} // namespace Vulkan

View File

@@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "core/frontend/framebuffer_layout.h"
@@ -13,7 +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"
#include "common/settings.h"
namespace Vulkan {
@@ -92,18 +92,51 @@ void WindowAdaptPass::Draw(RasterizerVulkan& rasterizer, Scheduler& scheduler, s
BeginRenderPass(cmdbuf, renderpass, host_framebuffer, render_area);
cmdbuf.ClearAttachments({clear_attachment}, {clear_rect});
const auto current_scaling_filter = Settings::values.scaling_filter.GetValue();
const bool is_crt_enabled = current_scaling_filter == Settings::ScalingFilter::CRTEasyMode ||
current_scaling_filter == Settings::ScalingFilter::CRTRoyale;
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, 0,
sizeof(PresentPushConstants), &push_constants[i]);
if (Settings::values.scaling_filter.GetValue() == Settings::ScalingFilter::Lanczos) {
// Push Lanczos quality if using Lanczos filter
if (current_scaling_filter == Settings::ScalingFilter::Lanczos && !is_crt_enabled) {
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);
}
// Push CRT parameters if CRT filter is enabled
if (is_crt_enabled) {
struct CRTPushConstants {
float scanline_strength;
float curvature;
float gamma;
float bloom;
int mask_type;
float brightness;
float alpha;
float screen_width;
float screen_height;
} crt_constants;
crt_constants.scanline_strength = Settings::values.crt_scanline_strength.GetValue();
crt_constants.curvature = Settings::values.crt_curvature.GetValue();
crt_constants.gamma = Settings::values.crt_gamma.GetValue();
crt_constants.bloom = Settings::values.crt_bloom.GetValue();
crt_constants.mask_type = Settings::values.crt_mask_type.GetValue();
crt_constants.brightness = Settings::values.crt_brightness.GetValue();
crt_constants.alpha = Settings::values.crt_alpha.GetValue();
crt_constants.screen_width = static_cast<float>(render_area.width);
crt_constants.screen_height = static_cast<float>(render_area.height);
cmdbuf.PushConstants(graphics_pipeline_layout, VK_SHADER_STAGE_FRAGMENT_BIT,
sizeof(PresentPushConstants) + sizeof(s32),
sizeof(CRTPushConstants), &crt_constants);
}
cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_GRAPHICS, graphics_pipeline_layout, 0,
descriptor_sets[i], {});
cmdbuf.Draw(4, 1, 0, 0);
@@ -127,8 +160,11 @@ void WindowAdaptPass::CreateDescriptorSetLayout() {
}
void WindowAdaptPass::CreatePipelineLayout() {
std::array<VkPushConstantRange, 2> ranges{};
// Support up to 3 push constant ranges:
// 0: PresentPushConstants (vertex shader)
// 1: Lanczos quality (fragment shader) - optional
// 2: CRT parameters (fragment shader) - optional
std::array<VkPushConstantRange, 3> ranges{};
// Range 0: The existing constants for the Vertex Shader
ranges[0] = {
@@ -137,13 +173,33 @@ void WindowAdaptPass::CreatePipelineLayout() {
.size = sizeof(PresentPushConstants),
};
// Range 1: Our new constant for the Fragment Shader
// Range 1: Lanczos quality for the Fragment Shader
ranges[1] = {
.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT,
.offset = sizeof(PresentPushConstants),
.size = sizeof(s32),
};
// Range 2: CRT parameters for the Fragment Shader
// Offset after PresentPushConstants + Lanczos (if used)
// CRT constants: 8 floats + 1 int = 36 bytes
struct CRTPushConstants {
float scanline_strength;
float curvature;
float gamma;
float bloom;
int mask_type;
float brightness;
float alpha;
float screen_width;
float screen_height;
};
ranges[2] = {
.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT,
.offset = sizeof(PresentPushConstants) + sizeof(s32),
.size = sizeof(CRTPushConstants),
};
pipeline_layout = device.GetLogical().CreatePipelineLayout(VkPipelineLayoutCreateInfo{
.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
.pNext = nullptr,

View File

@@ -1,7 +1,8 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/settings.h"
#include "video_core/framebuffer_config.h"
#include "video_core/present.h"
#include "video_core/renderer_vulkan/present/filters.h"
@@ -50,6 +51,10 @@ void BlitScreen::SetWindowAdaptPass() {
case Settings::ScalingFilter::ScaleFx:
window_adapt = MakeScaleFx(device, swapchain_view_format);
break;
case Settings::ScalingFilter::CRTEasyMode:
case Settings::ScalingFilter::CRTRoyale:
window_adapt = MakeCRT(device, swapchain_view_format);
break;
case Settings::ScalingFilter::Fsr:
case Settings::ScalingFilter::Fsr2:
case Settings::ScalingFilter::Bilinear: