mirror of
https://git.eden-emu.dev/archive/citron
synced 2026-03-31 00:18:30 -04:00
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:
@@ -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
|
||||
|
||||
133
src/video_core/host_shaders/vulkan_crt_easymode.frag
Normal file
133
src/video_core/host_shaders/vulkan_crt_easymode.frag
Normal 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);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user