Shader CRT-Pi alternatiu en pipeline separat; selecció via config + presets (v0.6.0)

This commit is contained in:
JailDesigner committed 2026-04-18 23:16:17 +02:00
1 parent 26b4d9e610
commit ce5bec1b34
16 files changed
+11205 -8

No files matched your search

@@ -0,0 +1,311 @@
#include "core/rendering/sdl3gpu/pipelines/crtpi_pipeline.hpp"
#include <cstring>
#include <iostream>
#ifndef __APPLE__
#include "core/rendering/sdl3gpu/spv/crtpi_frag_spv.h"
#include "core/rendering/sdl3gpu/spv/postfx_vert_spv.h"
#endif
namespace {
#ifdef __APPLE__
// NOLINTBEGIN(readability-identifier-naming)
const char* CRTPI_VERT_MSL_ = R"(
#include <metal_stdlib>
using namespace metal;
struct PostVOut {
float4 pos [[position]];
float2 uv;
};
vertex PostVOut postfx_vs(uint vid [[vertex_id]]) {
const float2 positions[3] = { {-1.0, -1.0}, {3.0, -1.0}, {-1.0, 3.0} };
const float2 uvs[3] = { { 0.0, 1.0}, {2.0, 1.0}, { 0.0,-1.0} };
PostVOut out;
out.pos = float4(positions[vid], 0.0, 1.0);
out.uv = uvs[vid];
return out;
}
)";
const char* CRTPI_FRAG_MSL_ = R"(
#include <metal_stdlib>
using namespace metal;
struct PostVOut {
float4 pos [[position]];
float2 uv;
};
struct CrtPiUniforms {
float scanline_weight;
float scanline_gap_brightness;
float bloom_factor;
float input_gamma;
float output_gamma;
float mask_brightness;
float curvature_x;
float curvature_y;
int mask_type;
int enable_scanlines;
int enable_multisample;
int enable_gamma;
int enable_curvature;
int enable_sharper;
float texture_width;
float texture_height;
};
static float2 crtpi_distort(float2 coord, float2 screen_scale, float cx, float cy) {
float2 curvature = float2(cx, cy);
float2 barrel_scale = 1.0f - (0.23f * curvature);
coord *= screen_scale;
coord -= 0.5f;
float rsq = coord.x * coord.x + coord.y * coord.y;
coord += coord * (curvature * rsq);
coord *= barrel_scale;
if (abs(coord.x) >= 0.5f || abs(coord.y) >= 0.5f) { return float2(-1.0f); }
coord += 0.5f;
coord /= screen_scale;
return coord;
}
static float crtpi_scan_weight(float dist, float sw, float gap) {
return max(1.0f - dist * dist * sw, gap);
}
static float crtpi_scan_line(float dy, float filter_w, float sw, float gap, bool ms) {
float w = crtpi_scan_weight(dy, sw, gap);
if (ms) {
w += crtpi_scan_weight(dy - filter_w, sw, gap);
w += crtpi_scan_weight(dy + filter_w, sw, gap);
w *= 0.3333333f;
}
return w;
}
fragment float4 crtpi_fs(PostVOut in [[stage_in]],
texture2d<float> tex [[texture(0)]],
sampler samp [[sampler(0)]],
constant CrtPiUniforms& u [[buffer(0)]]) {
float2 tex_size = float2(u.texture_width, u.texture_height);
float filter_width = (768.0f / u.texture_height) / 3.0f;
float2 texcoord = in.uv;
if (u.enable_curvature != 0) {
texcoord = crtpi_distort(texcoord, float2(1.0f, 1.0f), u.curvature_x, u.curvature_y);
if (texcoord.x < 0.0f) { return float4(0.0f, 0.0f, 0.0f, 1.0f); }
}
float2 coord_in_pixels = texcoord * tex_size;
float2 tc;
float scan_weight;
if (u.enable_sharper != 0) {
float2 temp = floor(coord_in_pixels) + 0.5f;
tc = temp / tex_size;
float2 deltas = coord_in_pixels - temp;
scan_weight = crtpi_scan_line(deltas.y, filter_width, u.scanline_weight, u.scanline_gap_brightness, u.enable_multisample != 0);
float2 signs = sign(deltas);
deltas.x *= 2.0f;
deltas = deltas * deltas;
deltas.y = deltas.y * deltas.y;
deltas.x *= 0.5f;
deltas.y *= 8.0f;
deltas /= tex_size;
deltas *= signs;
tc = tc + deltas;
} else {
float temp_y = floor(coord_in_pixels.y) + 0.5f;
float y_coord = temp_y / tex_size.y;
float dy = coord_in_pixels.y - temp_y;
scan_weight = crtpi_scan_line(dy, filter_width, u.scanline_weight, u.scanline_gap_brightness, u.enable_multisample != 0);
float sign_y = sign(dy);
dy = dy * dy;
dy = dy * dy;
dy *= 8.0f;
dy /= tex_size.y;
dy *= sign_y;
tc = float2(texcoord.x, y_coord + dy);
}
float3 colour = tex.sample(samp, tc).rgb;
if (u.enable_scanlines != 0) {
if (u.enable_gamma != 0) { colour = pow(colour, float3(u.input_gamma)); }
colour *= scan_weight * u.bloom_factor;
if (u.enable_gamma != 0) { colour = pow(colour, float3(1.0f / u.output_gamma)); }
}
if (u.mask_type == 1) {
float wm = fract(in.pos.x * 0.5f);
float3 mask = (wm < 0.5f) ? float3(u.mask_brightness, 1.0f, u.mask_brightness)
: float3(1.0f, u.mask_brightness, 1.0f);
colour *= mask;
} else if (u.mask_type == 2) {
float wm = fract(in.pos.x * 0.3333333f);
float3 mask = float3(u.mask_brightness);
if (wm < 0.3333333f) mask.x = 1.0f;
else if (wm < 0.6666666f) mask.y = 1.0f;
else mask.z = 1.0f;
colour *= mask;
}
return float4(colour, 1.0f);
}
)";
// NOLINTEND(readability-identifier-naming)
#endif
// Helpers per a crear shaders. Duplicats en cada pipeline per mantindre els
// fitxers autònoms — són funcions trivials i no val la pena un header compartit.
#ifdef __APPLE__
auto createShaderMSL(SDL_GPUDevice* device,
const char* msl_source,
const char* entrypoint,
SDL_GPUShaderStage stage,
Uint32 num_samplers,
Uint32 num_uniform_buffers) -> SDL_GPUShader* {
SDL_GPUShaderCreateInfo info{};
info.code = reinterpret_cast<const Uint8*>(msl_source);
info.code_size = std::strlen(msl_source) + 1;
info.entrypoint = entrypoint;
info.format = SDL_GPU_SHADERFORMAT_MSL;
info.stage = stage;
info.num_samplers = num_samplers;
info.num_uniform_buffers = num_uniform_buffers;
return SDL_CreateGPUShader(device, &info);
}
#else
auto createShaderSPIRV(SDL_GPUDevice* device,
const std::uint8_t* spv_code,
std::size_t spv_size,
const char* entrypoint,
SDL_GPUShaderStage stage,
Uint32 num_samplers,
Uint32 num_uniform_buffers) -> SDL_GPUShader* {
SDL_GPUShaderCreateInfo info{};
info.code = spv_code;
info.code_size = spv_size;
info.entrypoint = entrypoint;
info.format = SDL_GPU_SHADERFORMAT_SPIRV;
info.stage = stage;
info.num_samplers = num_samplers;
info.num_uniform_buffers = num_uniform_buffers;
return SDL_CreateGPUShader(device, &info);
}
#endif
} // namespace
namespace Rendering {
CrtPiPipeline::~CrtPiPipeline() { cleanup(); }
auto CrtPiPipeline::init(SDL_GPUDevice* device, SDL_Window* window) -> bool {
device_ = device;
#ifdef __APPLE__
SDL_GPUShader* vert = createShaderMSL(device_, CRTPI_VERT_MSL_, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
SDL_GPUShader* frag = createShaderMSL(device_, CRTPI_FRAG_MSL_, "crtpi_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
#else
SDL_GPUShader* vert = createShaderSPIRV(device_, kpostfx_vert_spv, kpostfx_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
SDL_GPUShader* frag = createShaderSPIRV(device_, kcrtpi_frag_spv, kcrtpi_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
#endif
if (vert == nullptr || frag == nullptr) {
std::cerr << "CrtPiPipeline: creació de shaders fallida: " << SDL_GetError() << '\n';
if (vert != nullptr) { SDL_ReleaseGPUShader(device_, vert); }
if (frag != nullptr) { SDL_ReleaseGPUShader(device_, frag); }
return false;
}
const SDL_GPUTextureFormat SWAPCHAIN_FORMAT = SDL_GetGPUSwapchainTextureFormat(device_, window);
SDL_GPUColorTargetBlendState no_blend{};
SDL_GPUColorTargetDescription color_target{};
color_target.format = SWAPCHAIN_FORMAT;
color_target.blend_state = no_blend;
SDL_GPUGraphicsPipelineCreateInfo info{};
info.vertex_shader = vert;
info.fragment_shader = frag;
info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
info.target_info.num_color_targets = 1;
info.target_info.color_target_descriptions = &color_target;
pipeline_ = SDL_CreateGPUGraphicsPipeline(device_, &info);
// El pipeline manté la seua propia referència als shaders; podem
// alliberar-los immediatament.
SDL_ReleaseGPUShader(device_, vert);
SDL_ReleaseGPUShader(device_, frag);
if (pipeline_ == nullptr) {
std::cerr << "CrtPiPipeline: creació del pipeline ha fallat: " << SDL_GetError() << '\n';
return false;
}
return true;
}
void CrtPiPipeline::cleanup() {
if (device_ != nullptr && pipeline_ != nullptr) {
SDL_ReleaseGPUGraphicsPipeline(device_, pipeline_);
pipeline_ = nullptr;
}
device_ = nullptr;
}
void CrtPiPipeline::setParams(const CrtPiParams& params) {
uniforms_.scanline_weight = params.scanline_weight;
uniforms_.scanline_gap_brightness = params.scanline_gap_brightness;
uniforms_.bloom_factor = params.bloom_factor;
uniforms_.input_gamma = params.input_gamma;
uniforms_.output_gamma = params.output_gamma;
uniforms_.mask_brightness = params.mask_brightness;
uniforms_.curvature_x = params.curvature_x;
uniforms_.curvature_y = params.curvature_y;
uniforms_.mask_type = params.mask_type;
uniforms_.enable_scanlines = params.enable_scanlines ? 1 : 0;
uniforms_.enable_multisample = params.enable_multisample ? 1 : 0;
uniforms_.enable_gamma = params.enable_gamma ? 1 : 0;
uniforms_.enable_curvature = params.enable_curvature ? 1 : 0;
uniforms_.enable_sharper = params.enable_sharper ? 1 : 0;
}
void CrtPiPipeline::render(SDL_GPUCommandBuffer* cmd,
SDL_GPUTexture* swapchain,
SDL_GPUTexture* scene,
SDL_GPUSampler* sampler,
int game_width,
int game_height) {
if (pipeline_ == nullptr) { return; }
SDL_GPUColorTargetInfo color_target{};
color_target.texture = swapchain;
color_target.load_op = SDL_GPU_LOADOP_CLEAR;
color_target.store_op = SDL_GPU_STOREOP_STORE;
color_target.clear_color = {.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F};
SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd, &color_target, 1, nullptr);
if (pass == nullptr) { return; }
SDL_BindGPUGraphicsPipeline(pass, pipeline_);
SDL_GPUTextureSamplerBinding binding{};
binding.texture = scene;
binding.sampler = sampler;
SDL_BindGPUFragmentSamplers(pass, 0, &binding, 1);
// Injectem la mida del canvas per al shader (usada en el filtrat analític).
uniforms_.texture_width = static_cast<float>(game_width);
uniforms_.texture_height = static_cast<float>(game_height);
SDL_PushGPUFragmentUniformData(cmd, 0, &uniforms_, sizeof(CrtPiUniforms));
SDL_DrawGPUPrimitives(pass, 3, 1, 0, 0);
SDL_EndGPURenderPass(pass);
}
} // namespace Rendering
@@ -0,0 +1,85 @@
#pragma once
#include <SDL3/SDL.h>
#include <SDL3/SDL_gpu.h>
#include "core/rendering/shader_backend.hpp"
// Uniforms del shader CRT-Pi. 4 × vec4 (64 bytes) per a alineament Metal/Vulkan.
struct CrtPiUniforms {
// vec4 #0
float scanline_weight;
float scanline_gap_brightness;
float bloom_factor;
float input_gamma;
// vec4 #1
float output_gamma;
float mask_brightness;
float curvature_x;
float curvature_y;
// vec4 #2
int mask_type;
int enable_scanlines;
int enable_multisample;
int enable_gamma;
// vec4 #3
int enable_curvature;
int enable_sharper;
float texture_width;
float texture_height;
};
namespace Rendering {
// Pipeline dedicat al shader CRT-Pi. Separat del PostFX perquè usa uniforms
// i un fragment shader diferents. Comparteix el mateix vertex shader de
// fullscreen-triangle (es carrega de nou internament; els shaders es
// destrueixen en acabar la creació del pipeline, i la pipeline en manté la
// referència). Renderitza directament al swapchain, sense SS ni Lanczos.
class CrtPiPipeline {
public:
CrtPiPipeline() = default;
~CrtPiPipeline();
CrtPiPipeline(const CrtPiPipeline&) = delete;
CrtPiPipeline(CrtPiPipeline&&) = delete;
auto operator=(const CrtPiPipeline&) -> CrtPiPipeline& = delete;
auto operator=(CrtPiPipeline&&) -> CrtPiPipeline& = delete;
auto init(SDL_GPUDevice* device, SDL_Window* window) -> bool;
void cleanup();
void setParams(const CrtPiParams& params);
void render(SDL_GPUCommandBuffer* cmd,
SDL_GPUTexture* swapchain,
SDL_GPUTexture* scene,
SDL_GPUSampler* sampler,
int game_width,
int game_height);
private:
SDL_GPUDevice* device_{nullptr};
SDL_GPUGraphicsPipeline* pipeline_{nullptr};
CrtPiUniforms uniforms_{
.scanline_weight = 6.0F,
.scanline_gap_brightness = 0.12F,
.bloom_factor = 3.5F,
.input_gamma = 2.4F,
.output_gamma = 2.2F,
.mask_brightness = 0.80F,
.curvature_x = 0.05F,
.curvature_y = 0.10F,
.mask_type = 2,
.enable_scanlines = 1,
.enable_multisample = 1,
.enable_gamma = 1,
.enable_curvature = 0,
.enable_sharper = 0,
.texture_width = 0.0F,
.texture_height = 0.0F,
};
};
} // namespace Rendering