feat(renderer): add CRT shader filter with configurable effects

Add CRT (Cathode Ray Tube) shader implementation as scaling filter
options (CRT EasyMode and CRT Royale) in the Window Adapting Filter
dropdown. Provides classic TV effects including scanlines, phosphor
masks, curvature distortion, gamma correction, bloom, brightness, and
alpha transparency.

- Add CRTEasyMode and CRTRoyale to ScalingFilter enum
- Implement vulkan_crt_easymode.frag shader with single-pass effects
- Integrate CRT filter into WindowAdaptPass rendering pipeline
- Add configurable CRT parameters to settings with user-friendly labels
- Add UI translations for desktop and Android platforms
- Support CRT push constants in present pipeline

The CRT filter appears alongside other scaling filters like FSR and
FSR 2.0. CRT parameter settings are only active when a CRT filter
is selected.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
This commit is contained in:
Zephyron
2026-01-01 18:18:05 +10:00
parent d8c1cad245
commit efef746299
12 changed files with 320 additions and 16 deletions

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,