Shader CRT-Pi alternatiu en pipeline separat; selecció via config + presets (v0.6.0)
This commit is contained in:
1 parent
26b4d9e610
commit
ce5bec1b34
16 files changed
+11200
-3
No files matched your search
@@ -40,6 +40,7 @@ set(APP_SOURCES
|
|||||||
source/core/rendering/sprite/moving_sprite.cpp
|
source/core/rendering/sprite/moving_sprite.cpp
|
||||||
source/core/rendering/sprite/animated_sprite.cpp
|
source/core/rendering/sprite/animated_sprite.cpp
|
||||||
source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp
|
source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp
|
||||||
|
source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.cpp
|
||||||
source/core/resources/resource_helper.cpp
|
source/core/resources/resource_helper.cpp
|
||||||
source/core/resources/resource_cache.cpp
|
source/core/resources/resource_cache.cpp
|
||||||
source/utils/utils.cpp
|
source/utils/utils.cpp
|
||||||
@@ -60,10 +61,12 @@ if(NOT APPLE)
|
|||||||
set(ALL_SHADER_SOURCES
|
set(ALL_SHADER_SOURCES
|
||||||
"${SHADERS_DIR}/postfx.vert"
|
"${SHADERS_DIR}/postfx.vert"
|
||||||
"${SHADERS_DIR}/postfx.frag"
|
"${SHADERS_DIR}/postfx.frag"
|
||||||
|
"${SHADERS_DIR}/crtpi_frag.glsl"
|
||||||
)
|
)
|
||||||
set(ALL_SHADER_HEADERS
|
set(ALL_SHADER_HEADERS
|
||||||
"${HEADERS_DIR}/postfx_vert_spv.h"
|
"${HEADERS_DIR}/postfx_vert_spv.h"
|
||||||
"${HEADERS_DIR}/postfx_frag_spv.h"
|
"${HEADERS_DIR}/postfx_frag_spv.h"
|
||||||
|
"${HEADERS_DIR}/crtpi_frag_spv.h"
|
||||||
)
|
)
|
||||||
|
|
||||||
if(GLSLC_EXE)
|
if(GLSLC_EXE)
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
// Vulkan GLSL fragment shader — CRT-Pi PostFX
|
||||||
|
// Algoritmo de scanlines continuas con pesos gaussianos, bloom y máscara de fósforo.
|
||||||
|
// Basado en el shader CRT-Pi original (GLSL 3.3), portado a GLSL 4.50 con parámetros uniformes.
|
||||||
|
//
|
||||||
|
// Compile: glslc -fshader-stage=frag --target-env=vulkan1.0 crtpi_frag.glsl -o crtpi_frag.spv
|
||||||
|
// xxd -i crtpi_frag.spv > ../../source/core/rendering/sdl3gpu/crtpi_frag_spv.h
|
||||||
|
|
||||||
|
layout(location = 0) in vec2 v_uv;
|
||||||
|
layout(location = 0) out vec4 out_color;
|
||||||
|
|
||||||
|
layout(set = 2, binding = 0) uniform sampler2D Texture;
|
||||||
|
|
||||||
|
layout(set = 3, binding = 0) uniform CrtPiBlock {
|
||||||
|
// vec4 #0
|
||||||
|
float scanline_weight; // Ajuste gaussiano de scanlines (default 6.0)
|
||||||
|
float scanline_gap_brightness; // Brillo mínimo entre scanlines (default 0.12)
|
||||||
|
float bloom_factor; // Factor de brillo en zonas iluminadas (default 3.5)
|
||||||
|
float input_gamma; // Gamma de entrada — linealización (default 2.4)
|
||||||
|
// vec4 #1
|
||||||
|
float output_gamma; // Gamma de salida — codificación (default 2.2)
|
||||||
|
float mask_brightness; // Brillo sub-píxeles de la máscara (default 0.80)
|
||||||
|
float curvature_x; // Distorsión barrel eje X (default 0.05)
|
||||||
|
float curvature_y; // Distorsión barrel eje Y (default 0.10)
|
||||||
|
// vec4 #2
|
||||||
|
int mask_type; // 0=ninguna, 1=verde/magenta, 2=RGB fósforo
|
||||||
|
int enable_scanlines; // 0 = off, 1 = on
|
||||||
|
int enable_multisample; // 0 = off, 1 = on (antialiasing analítico de scanlines)
|
||||||
|
int enable_gamma; // 0 = off, 1 = on
|
||||||
|
// vec4 #3
|
||||||
|
int enable_curvature; // 0 = off, 1 = on
|
||||||
|
int enable_sharper; // 0 = off, 1 = on
|
||||||
|
float texture_width; // Ancho del canvas lógico en píxeles
|
||||||
|
float texture_height; // Alto del canvas lógico en píxeles
|
||||||
|
} u;
|
||||||
|
|
||||||
|
// Distorsión barrel CRT
|
||||||
|
vec2 distort(vec2 coord, vec2 screen_scale) {
|
||||||
|
vec2 curvature = vec2(u.curvature_x, u.curvature_y);
|
||||||
|
vec2 barrel_scale = 1.0 - (0.23 * curvature);
|
||||||
|
coord *= screen_scale;
|
||||||
|
coord -= vec2(0.5);
|
||||||
|
float rsq = coord.x * coord.x + coord.y * coord.y;
|
||||||
|
coord += coord * (curvature * rsq);
|
||||||
|
coord *= barrel_scale;
|
||||||
|
if (abs(coord.x) >= 0.5 || abs(coord.y) >= 0.5) {
|
||||||
|
return vec2(-1.0); // fuera de pantalla
|
||||||
|
}
|
||||||
|
coord += vec2(0.5);
|
||||||
|
coord /= screen_scale;
|
||||||
|
return coord;
|
||||||
|
}
|
||||||
|
|
||||||
|
float calcScanLineWeight(float dist) {
|
||||||
|
return max(1.0 - dist * dist * u.scanline_weight, u.scanline_gap_brightness);
|
||||||
|
}
|
||||||
|
|
||||||
|
float calcScanLine(float dy, float filter_width) {
|
||||||
|
float weight = calcScanLineWeight(dy);
|
||||||
|
if (u.enable_multisample != 0) {
|
||||||
|
weight += calcScanLineWeight(dy - filter_width);
|
||||||
|
weight += calcScanLineWeight(dy + filter_width);
|
||||||
|
weight *= 0.3333333;
|
||||||
|
}
|
||||||
|
return weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 tex_size = vec2(u.texture_width, u.texture_height);
|
||||||
|
|
||||||
|
// filterWidth: equivalente al original (768.0 / TextureSize.y) / 3.0
|
||||||
|
float filter_width = (768.0 / u.texture_height) / 3.0;
|
||||||
|
|
||||||
|
vec2 texcoord = v_uv;
|
||||||
|
|
||||||
|
// Curvatura barrel opcional
|
||||||
|
if (u.enable_curvature != 0) {
|
||||||
|
texcoord = distort(texcoord, vec2(1.0, 1.0));
|
||||||
|
if (texcoord.x < 0.0) {
|
||||||
|
out_color = vec4(0.0, 0.0, 0.0, 1.0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vec2 texcoord_in_pixels = texcoord * tex_size;
|
||||||
|
vec2 tc;
|
||||||
|
float scan_line_weight;
|
||||||
|
|
||||||
|
if (u.enable_sharper != 0) {
|
||||||
|
// Modo SHARPER: filtrado bicúbico-like con subpixel sharpen
|
||||||
|
vec2 temp_coord = floor(texcoord_in_pixels) + 0.5;
|
||||||
|
tc = temp_coord / tex_size;
|
||||||
|
vec2 deltas = texcoord_in_pixels - temp_coord;
|
||||||
|
scan_line_weight = calcScanLine(deltas.y, filter_width);
|
||||||
|
vec2 signs = sign(deltas);
|
||||||
|
deltas.x *= 2.0;
|
||||||
|
deltas = deltas * deltas;
|
||||||
|
deltas.y = deltas.y * deltas.y;
|
||||||
|
deltas.x *= 0.5;
|
||||||
|
deltas.y *= 8.0;
|
||||||
|
deltas /= tex_size;
|
||||||
|
deltas *= signs;
|
||||||
|
tc = tc + deltas;
|
||||||
|
} else {
|
||||||
|
// Modo estándar
|
||||||
|
float temp_y = floor(texcoord_in_pixels.y) + 0.5;
|
||||||
|
float y_coord = temp_y / tex_size.y;
|
||||||
|
float dy = texcoord_in_pixels.y - temp_y;
|
||||||
|
scan_line_weight = calcScanLine(dy, filter_width);
|
||||||
|
float sign_y = sign(dy);
|
||||||
|
dy = dy * dy;
|
||||||
|
dy = dy * dy;
|
||||||
|
dy *= 8.0;
|
||||||
|
dy /= tex_size.y;
|
||||||
|
dy *= sign_y;
|
||||||
|
tc = vec2(texcoord.x, y_coord + dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 colour = texture(Texture, tc).rgb;
|
||||||
|
|
||||||
|
if (u.enable_scanlines != 0) {
|
||||||
|
if (u.enable_gamma != 0) {
|
||||||
|
colour = pow(colour, vec3(u.input_gamma));
|
||||||
|
}
|
||||||
|
colour *= scan_line_weight * u.bloom_factor;
|
||||||
|
if (u.enable_gamma != 0) {
|
||||||
|
colour = pow(colour, vec3(1.0 / u.output_gamma));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Máscara de fósforo
|
||||||
|
if (u.mask_type == 1) {
|
||||||
|
float which_mask = fract(gl_FragCoord.x * 0.5);
|
||||||
|
vec3 mask = (which_mask < 0.5)
|
||||||
|
? vec3(u.mask_brightness, 1.0, u.mask_brightness)
|
||||||
|
: vec3(1.0, u.mask_brightness, 1.0);
|
||||||
|
colour *= mask;
|
||||||
|
} else if (u.mask_type == 2) {
|
||||||
|
float which_mask = fract(gl_FragCoord.x * 0.3333333);
|
||||||
|
vec3 mask = vec3(u.mask_brightness);
|
||||||
|
if (which_mask < 0.3333333)
|
||||||
|
mask.x = 1.0;
|
||||||
|
else if (which_mask < 0.6666666)
|
||||||
|
mask.y = 1.0;
|
||||||
|
else
|
||||||
|
mask.z = 1.0;
|
||||||
|
colour *= mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
out_color = vec4(colour, 1.0);
|
||||||
|
}
|
||||||
@@ -142,6 +142,18 @@ void Screen::applyPostFXParams(const Rendering::PostFXParams& params) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Screen::setActiveShader(Rendering::ShaderType type) {
|
||||||
|
if (shader_backend_ != nullptr) {
|
||||||
|
shader_backend_->setActiveShader(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Screen::applyCrtPiParams(const Rendering::CrtPiParams& params) {
|
||||||
|
if (shader_backend_ != nullptr) {
|
||||||
|
shader_backend_->setCrtPiParams(params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Screen::clear(std::uint8_t r, std::uint8_t g, std::uint8_t b) {
|
void Screen::clear(std::uint8_t r, std::uint8_t g, std::uint8_t b) {
|
||||||
const std::uint32_t COLOR = (0xFFU << 24) | (static_cast<std::uint32_t>(r) << 16) | (static_cast<std::uint32_t>(g) << 8) | static_cast<std::uint32_t>(b);
|
const std::uint32_t COLOR = (0xFFU << 24) | (static_cast<std::uint32_t>(r) << 16) | (static_cast<std::uint32_t>(g) << 8) | static_cast<std::uint32_t>(b);
|
||||||
std::ranges::fill(canvas_, COLOR);
|
std::ranges::fill(canvas_, COLOR);
|
||||||
|
|||||||
@@ -48,6 +48,12 @@ class Screen {
|
|||||||
// SDL_Renderer (fallback) o si encara no hi ha backend.
|
// SDL_Renderer (fallback) o si encara no hi ha backend.
|
||||||
void applyPostFXParams(const Rendering::PostFXParams& params);
|
void applyPostFXParams(const Rendering::PostFXParams& params);
|
||||||
|
|
||||||
|
// Selecciona el shader actiu (PostFX o CrtPi). No-op al path fallback.
|
||||||
|
void setActiveShader(Rendering::ShaderType type);
|
||||||
|
|
||||||
|
// Reenvia paràmetres CrtPi al backend GPU. No-op al path fallback.
|
||||||
|
void applyCrtPiParams(const Rendering::CrtPiParams& params);
|
||||||
|
|
||||||
void clear(std::uint8_t r = 0, std::uint8_t g = 0, std::uint8_t b = 0);
|
void clear(std::uint8_t r = 0, std::uint8_t g = 0, std::uint8_t b = 0);
|
||||||
void present();
|
void present();
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -222,6 +222,13 @@ namespace Rendering {
|
|||||||
cleanup();
|
cleanup();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// El pipeline CrtPi es crea sempre — l'usuari pot alternar en calent
|
||||||
|
// via setActiveShader() sense tornar a init'ar la GPU.
|
||||||
|
crtpi_pipeline_ = std::make_unique<CrtPiPipeline>();
|
||||||
|
if (!crtpi_pipeline_->init(device_, window_)) {
|
||||||
|
cleanup();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
is_ready_ = true;
|
is_ready_ = true;
|
||||||
return true;
|
return true;
|
||||||
@@ -361,6 +368,13 @@ namespace Rendering {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Camí CrtPi: delega tot el render pass al pipeline dedicat.
|
||||||
|
if (active_shader_ == ShaderType::CRTPI && crtpi_pipeline_ != nullptr) {
|
||||||
|
crtpi_pipeline_->render(cmd, swapchain, scene_texture_, sampler_, game_width_, game_height_);
|
||||||
|
SDL_SubmitGPUCommandBuffer(cmd);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
uniforms_.time = static_cast<float>(SDL_GetTicks()) / 1000.0F;
|
uniforms_.time = static_cast<float>(SDL_GetTicks()) / 1000.0F;
|
||||||
|
|
||||||
SDL_GPUColorTargetInfo color_target{};
|
SDL_GPUColorTargetInfo color_target{};
|
||||||
@@ -386,6 +400,12 @@ namespace Rendering {
|
|||||||
SDL_SubmitGPUCommandBuffer(cmd);
|
SDL_SubmitGPUCommandBuffer(cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SDL3GPUShader::setCrtPiParams(const CrtPiParams& params) {
|
||||||
|
if (crtpi_pipeline_ != nullptr) {
|
||||||
|
crtpi_pipeline_->setParams(params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void SDL3GPUShader::setPostFXParams(const PostFXParams& params) {
|
void SDL3GPUShader::setPostFXParams(const PostFXParams& params) {
|
||||||
uniforms_.vignette_strength = params.vignette;
|
uniforms_.vignette_strength = params.vignette;
|
||||||
uniforms_.scanline_strength = params.scanlines;
|
uniforms_.scanline_strength = params.scanlines;
|
||||||
@@ -430,6 +450,9 @@ namespace Rendering {
|
|||||||
if (device_ != nullptr) {
|
if (device_ != nullptr) {
|
||||||
SDL_WaitForGPUIdle(device_);
|
SDL_WaitForGPUIdle(device_);
|
||||||
|
|
||||||
|
// El pipeline CrtPi ha d'alliberar-se mentre el device segueix viu.
|
||||||
|
crtpi_pipeline_.reset();
|
||||||
|
|
||||||
if (pipeline_ != nullptr) {
|
if (pipeline_ != nullptr) {
|
||||||
SDL_ReleaseGPUGraphicsPipeline(device_, pipeline_);
|
SDL_ReleaseGPUGraphicsPipeline(device_, pipeline_);
|
||||||
pipeline_ = nullptr;
|
pipeline_ = nullptr;
|
||||||
|
|||||||
@@ -4,8 +4,10 @@
|
|||||||
#include <SDL3/SDL_gpu.h>
|
#include <SDL3/SDL_gpu.h>
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
#include "core/rendering/sdl3gpu/pipelines/crtpi_pipeline.hpp"
|
||||||
#include "core/rendering/shader_backend.hpp"
|
#include "core/rendering/shader_backend.hpp"
|
||||||
|
|
||||||
// Uniforms enviats al fragment stage cada frame.
|
// Uniforms enviats al fragment stage cada frame.
|
||||||
@@ -48,6 +50,8 @@ namespace Rendering {
|
|||||||
void uploadPixels(const std::uint32_t* pixels, int width, int height) override;
|
void uploadPixels(const std::uint32_t* pixels, int width, int height) override;
|
||||||
void render() override;
|
void render() override;
|
||||||
void setPostFXParams(const PostFXParams& params) override;
|
void setPostFXParams(const PostFXParams& params) override;
|
||||||
|
void setActiveShader(ShaderType type) override { active_shader_ = type; }
|
||||||
|
void setCrtPiParams(const CrtPiParams& params) override;
|
||||||
void setVSync(bool vsync) override;
|
void setVSync(bool vsync) override;
|
||||||
void setPreferredDriver(const std::string& driver) override { preferred_driver_ = driver; }
|
void setPreferredDriver(const std::string& driver) override { preferred_driver_ = driver; }
|
||||||
void cleanup() override;
|
void cleanup() override;
|
||||||
@@ -75,6 +79,8 @@ namespace Rendering {
|
|||||||
SDL_GPUTexture* scene_texture_{nullptr};
|
SDL_GPUTexture* scene_texture_{nullptr};
|
||||||
SDL_GPUTransferBuffer* upload_buffer_{nullptr};
|
SDL_GPUTransferBuffer* upload_buffer_{nullptr};
|
||||||
SDL_GPUSampler* sampler_{nullptr};
|
SDL_GPUSampler* sampler_{nullptr};
|
||||||
|
std::unique_ptr<CrtPiPipeline> crtpi_pipeline_;
|
||||||
|
ShaderType active_shader_{ShaderType::POSTFX};
|
||||||
|
|
||||||
PostFXUniforms uniforms_{
|
PostFXUniforms uniforms_{
|
||||||
.vignette_strength = 0.0F,
|
.vignette_strength = 0.0F,
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -7,6 +7,10 @@
|
|||||||
|
|
||||||
namespace Rendering {
|
namespace Rendering {
|
||||||
|
|
||||||
|
// Shader de post-procés actiu. El backend escull el pipeline corresponent.
|
||||||
|
enum class ShaderType : std::uint8_t { POSTFX,
|
||||||
|
CRTPI };
|
||||||
|
|
||||||
// Paràmetres d'intensitat dels efectes PostFX. Amb tot a 0 el shader
|
// Paràmetres d'intensitat dels efectes PostFX. Amb tot a 0 el shader
|
||||||
// actua com un passthrough. Els presets els omplen en fases futures.
|
// actua com un passthrough. Els presets els omplen en fases futures.
|
||||||
struct PostFXParams {
|
struct PostFXParams {
|
||||||
@@ -20,6 +24,24 @@ namespace Rendering {
|
|||||||
float flicker{0.0F};
|
float flicker{0.0F};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Paràmetres del shader CRT-Pi (algoritme de scanlines amb pesos gaussians).
|
||||||
|
struct CrtPiParams {
|
||||||
|
float scanline_weight{6.0F};
|
||||||
|
float scanline_gap_brightness{0.12F};
|
||||||
|
float bloom_factor{3.5F};
|
||||||
|
float input_gamma{2.4F};
|
||||||
|
float output_gamma{2.2F};
|
||||||
|
float mask_brightness{0.80F};
|
||||||
|
float curvature_x{0.05F};
|
||||||
|
float curvature_y{0.10F};
|
||||||
|
int mask_type{2}; // 0=cap, 1=verd/magenta, 2=RGB fòsfor
|
||||||
|
bool enable_scanlines{true};
|
||||||
|
bool enable_multisample{true};
|
||||||
|
bool enable_gamma{true};
|
||||||
|
bool enable_curvature{false};
|
||||||
|
bool enable_sharper{false};
|
||||||
|
};
|
||||||
|
|
||||||
// Interfície abstracta per a backends de renderitzat per GPU.
|
// Interfície abstracta per a backends de renderitzat per GPU.
|
||||||
// Per ara, l'única implementació és SDL3GPUShader. Un backend
|
// Per ara, l'única implementació és SDL3GPUShader. Un backend
|
||||||
// alternatiu (p.ex. OpenGL o dummy) podria afegir-se més endavant.
|
// alternatiu (p.ex. OpenGL o dummy) podria afegir-se més endavant.
|
||||||
@@ -47,6 +69,12 @@ namespace Rendering {
|
|||||||
// Actualitza els uniforms de PostFX per al pròxim render().
|
// Actualitza els uniforms de PostFX per al pròxim render().
|
||||||
virtual void setPostFXParams(const PostFXParams& params) = 0;
|
virtual void setPostFXParams(const PostFXParams& params) = 0;
|
||||||
|
|
||||||
|
// Selecciona quin shader aplica a la pròxima crida a render().
|
||||||
|
virtual void setActiveShader(ShaderType type) = 0;
|
||||||
|
|
||||||
|
// Actualitza els uniforms del shader CRT-Pi per al pròxim render().
|
||||||
|
virtual void setCrtPiParams(const CrtPiParams& params) = 0;
|
||||||
|
|
||||||
// Canvia el mode de presentació (VSYNC/IMMEDIATE/MAILBOX) en calent.
|
// Canvia el mode de presentació (VSYNC/IMMEDIATE/MAILBOX) en calent.
|
||||||
virtual void setVSync(bool vsync) = 0;
|
virtual void setVSync(bool vsync) = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -37,11 +37,18 @@ Director::Director() {
|
|||||||
if (!Options::loadPostFXFromFile()) {
|
if (!Options::loadPostFXFromFile()) {
|
||||||
std::cerr << "Director: no s'ha pogut carregar ni desar postfx.yaml.\n";
|
std::cerr << "Director: no s'ha pogut carregar ni desar postfx.yaml.\n";
|
||||||
}
|
}
|
||||||
|
// Presets del shader CRT-Pi (shaders/crtpi.yaml).
|
||||||
|
Options::setCrtPiFile(system_folder_ + Defaults::Video::CRTPI_FILE);
|
||||||
|
if (!Options::loadCrtPiFromFile()) {
|
||||||
|
std::cerr << "Director: no s'ha pogut carregar ni desar crtpi.yaml.\n";
|
||||||
|
}
|
||||||
|
|
||||||
// Screen es configura ara (subsystem + finestra + GPU/renderer); la seua
|
// Screen es configura ara (subsystem + finestra + GPU/renderer); la seua
|
||||||
// destrucció la fa aquest Director (veure ~Director). Res de static dtor.
|
// destrucció la fa aquest Director (veure ~Director). Res de static dtor.
|
||||||
Screen::get().configure(Options::buildScreenConfig());
|
Screen::get().configure(Options::buildScreenConfig());
|
||||||
|
Screen::get().setActiveShader(Options::buildActiveShader());
|
||||||
Screen::get().applyPostFXParams(Options::buildPostFXParams());
|
Screen::get().applyPostFXParams(Options::buildPostFXParams());
|
||||||
|
Screen::get().applyCrtPiParams(Options::buildCrtPiParams());
|
||||||
(void)Resource::Cache::get();
|
(void)Resource::Cache::get();
|
||||||
|
|
||||||
current_scene_ = std::make_unique<GameScene>();
|
current_scene_ = std::make_unique<GameScene>();
|
||||||
|
|||||||
+153
-1
@@ -76,9 +76,15 @@ namespace {
|
|||||||
if (!video_node.contains("shader")) { return; }
|
if (!video_node.contains("shader")) { return; }
|
||||||
const auto& node = video_node["shader"];
|
const auto& node = video_node["shader"];
|
||||||
if (node.contains("enabled")) { Options::video.shader.enabled = node["enabled"].get_value<bool>(); }
|
if (node.contains("enabled")) { Options::video.shader.enabled = node["enabled"].get_value<bool>(); }
|
||||||
|
if (node.contains("current_shader")) {
|
||||||
|
Options::video.shader.current_shader = node["current_shader"].get_value<std::string>();
|
||||||
|
}
|
||||||
if (node.contains("current_postfx_preset")) {
|
if (node.contains("current_postfx_preset")) {
|
||||||
Options::video.shader.current_postfx_preset = node["current_postfx_preset"].get_value<std::string>();
|
Options::video.shader.current_postfx_preset = node["current_postfx_preset"].get_value<std::string>();
|
||||||
}
|
}
|
||||||
|
if (node.contains("current_crtpi_preset")) {
|
||||||
|
Options::video.shader.current_crtpi_preset = node["current_crtpi_preset"].get_value<std::string>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void parseVideo(const fkyaml::node& yaml) {
|
void parseVideo(const fkyaml::node& yaml) {
|
||||||
@@ -167,6 +173,61 @@ namespace {
|
|||||||
out << " flicker: " << preset.flicker << "\n";
|
out << " flicker: " << preset.flicker << "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Presets per defecte de CrtPi (tuning bàsic del shader original CRT-Pi).
|
||||||
|
auto defaultCrtPiPresets() -> std::vector<Options::CrtPiPreset> {
|
||||||
|
return {
|
||||||
|
{.name = "default"},
|
||||||
|
{.name = "sharper",
|
||||||
|
.scanline_weight = 8.0F,
|
||||||
|
.scanline_gap_brightness = 0.10F,
|
||||||
|
.bloom_factor = 3.0F,
|
||||||
|
.enable_sharper = true},
|
||||||
|
{.name = "curved",
|
||||||
|
.scanline_weight = 6.0F,
|
||||||
|
.scanline_gap_brightness = 0.12F,
|
||||||
|
.bloom_factor = 3.5F,
|
||||||
|
.curvature_x = 0.05F,
|
||||||
|
.curvature_y = 0.10F,
|
||||||
|
.enable_curvature = true},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void parseCrtPiPreset(const fkyaml::node& node, Options::CrtPiPreset& preset) {
|
||||||
|
if (node.contains("name")) { preset.name = node["name"].get_value<std::string>(); }
|
||||||
|
if (node.contains("scanline_weight")) { preset.scanline_weight = node["scanline_weight"].get_value<float>(); }
|
||||||
|
if (node.contains("scanline_gap_brightness")) { preset.scanline_gap_brightness = node["scanline_gap_brightness"].get_value<float>(); }
|
||||||
|
if (node.contains("bloom_factor")) { preset.bloom_factor = node["bloom_factor"].get_value<float>(); }
|
||||||
|
if (node.contains("input_gamma")) { preset.input_gamma = node["input_gamma"].get_value<float>(); }
|
||||||
|
if (node.contains("output_gamma")) { preset.output_gamma = node["output_gamma"].get_value<float>(); }
|
||||||
|
if (node.contains("mask_brightness")) { preset.mask_brightness = node["mask_brightness"].get_value<float>(); }
|
||||||
|
if (node.contains("curvature_x")) { preset.curvature_x = node["curvature_x"].get_value<float>(); }
|
||||||
|
if (node.contains("curvature_y")) { preset.curvature_y = node["curvature_y"].get_value<float>(); }
|
||||||
|
if (node.contains("mask_type")) { preset.mask_type = node["mask_type"].get_value<int>(); }
|
||||||
|
if (node.contains("enable_scanlines")) { preset.enable_scanlines = node["enable_scanlines"].get_value<bool>(); }
|
||||||
|
if (node.contains("enable_multisample")) { preset.enable_multisample = node["enable_multisample"].get_value<bool>(); }
|
||||||
|
if (node.contains("enable_gamma")) { preset.enable_gamma = node["enable_gamma"].get_value<bool>(); }
|
||||||
|
if (node.contains("enable_curvature")) { preset.enable_curvature = node["enable_curvature"].get_value<bool>(); }
|
||||||
|
if (node.contains("enable_sharper")) { preset.enable_sharper = node["enable_sharper"].get_value<bool>(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
void writeCrtPiPreset(std::ostringstream& out, const Options::CrtPiPreset& preset) {
|
||||||
|
out << " - name: \"" << preset.name << "\"\n";
|
||||||
|
out << " scanline_weight: " << preset.scanline_weight << "\n";
|
||||||
|
out << " scanline_gap_brightness: " << preset.scanline_gap_brightness << "\n";
|
||||||
|
out << " bloom_factor: " << preset.bloom_factor << "\n";
|
||||||
|
out << " input_gamma: " << preset.input_gamma << "\n";
|
||||||
|
out << " output_gamma: " << preset.output_gamma << "\n";
|
||||||
|
out << " mask_brightness: " << preset.mask_brightness << "\n";
|
||||||
|
out << " curvature_x: " << preset.curvature_x << "\n";
|
||||||
|
out << " curvature_y: " << preset.curvature_y << "\n";
|
||||||
|
out << " mask_type: " << preset.mask_type << "\n";
|
||||||
|
out << " enable_scanlines: " << (preset.enable_scanlines ? "true" : "false") << "\n";
|
||||||
|
out << " enable_multisample: " << (preset.enable_multisample ? "true" : "false") << "\n";
|
||||||
|
out << " enable_gamma: " << (preset.enable_gamma ? "true" : "false") << "\n";
|
||||||
|
out << " enable_curvature: " << (preset.enable_curvature ? "true" : "false") << "\n";
|
||||||
|
out << " enable_sharper: " << (preset.enable_sharper ? "true" : "false") << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
namespace Options {
|
namespace Options {
|
||||||
@@ -229,7 +290,9 @@ namespace Options {
|
|||||||
out << " preferred_driver: \"" << video.gpu.preferred_driver << "\"\n";
|
out << " preferred_driver: \"" << video.gpu.preferred_driver << "\"\n";
|
||||||
out << " shader:\n";
|
out << " shader:\n";
|
||||||
out << " enabled: " << (video.shader.enabled ? "true" : "false") << "\n";
|
out << " enabled: " << (video.shader.enabled ? "true" : "false") << "\n";
|
||||||
out << " current_postfx_preset: \"" << video.shader.current_postfx_preset << "\"\n\n";
|
out << " current_shader: \"" << video.shader.current_shader << "\"\n";
|
||||||
|
out << " current_postfx_preset: \"" << video.shader.current_postfx_preset << "\"\n";
|
||||||
|
out << " current_crtpi_preset: \"" << video.shader.current_crtpi_preset << "\"\n\n";
|
||||||
|
|
||||||
out << "audio:\n";
|
out << "audio:\n";
|
||||||
out << " enabled: " << (audio.enabled ? "true" : "false") << "\n";
|
out << " enabled: " << (audio.enabled ? "true" : "false") << "\n";
|
||||||
@@ -309,6 +372,95 @@ namespace Options {
|
|||||||
return writeFile(postfx_file_path, out.str());
|
return writeFile(postfx_file_path, out.str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setCrtPiFile(const std::string& path) {
|
||||||
|
crtpi_file_path = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto loadCrtPiFromFile() -> bool {
|
||||||
|
if (crtpi_file_path.empty()) { return false; }
|
||||||
|
|
||||||
|
const std::string CONTENT = readFileOrEmpty(crtpi_file_path);
|
||||||
|
if (CONTENT.empty()) {
|
||||||
|
crtpi_presets = defaultCrtPiPresets();
|
||||||
|
return saveCrtPiToFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
auto yaml = fkyaml::node::deserialize(CONTENT);
|
||||||
|
if (!yaml.contains("presets") || !yaml["presets"].is_sequence()) {
|
||||||
|
crtpi_presets = defaultCrtPiPresets();
|
||||||
|
return saveCrtPiToFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
crtpi_presets.clear();
|
||||||
|
for (const auto& preset_node : yaml["presets"]) {
|
||||||
|
CrtPiPreset preset;
|
||||||
|
parseCrtPiPreset(preset_node, preset);
|
||||||
|
crtpi_presets.push_back(preset);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (crtpi_presets.empty()) {
|
||||||
|
crtpi_presets = defaultCrtPiPresets();
|
||||||
|
return saveCrtPiToFile();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "Options: error parsejant crtpi.yaml: " << e.what() << '\n';
|
||||||
|
std::cerr << "Options: reescrivint el fitxer amb presets per defecte.\n";
|
||||||
|
crtpi_presets = defaultCrtPiPresets();
|
||||||
|
return saveCrtPiToFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto saveCrtPiToFile() -> bool {
|
||||||
|
if (crtpi_file_path.empty()) { return false; }
|
||||||
|
|
||||||
|
std::ostringstream out;
|
||||||
|
out << "# Presets CRT-Pi per a AEE 2026. Editable a mà.\n";
|
||||||
|
out << "# Els noms han de coincidir amb config.yaml > video.shader.current_crtpi_preset.\n";
|
||||||
|
out << "presets:\n";
|
||||||
|
for (const auto& preset : crtpi_presets) {
|
||||||
|
writeCrtPiPreset(out, preset);
|
||||||
|
}
|
||||||
|
return writeFile(crtpi_file_path, out.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto buildActiveShader() -> Rendering::ShaderType {
|
||||||
|
if (video.shader.current_shader == "crtpi") {
|
||||||
|
return Rendering::ShaderType::CRTPI;
|
||||||
|
}
|
||||||
|
return Rendering::ShaderType::POSTFX;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto buildCrtPiParams() -> Rendering::CrtPiParams {
|
||||||
|
// Si el shader està deshabilitat, retornem defaults (el dispatch anirà per
|
||||||
|
// PostFX passthrough, però sent nets per si algú consulta aquests params).
|
||||||
|
if (!video.shader.enabled) { return {}; }
|
||||||
|
|
||||||
|
const auto IT = std::ranges::find_if(crtpi_presets,
|
||||||
|
[](const CrtPiPreset& preset) { return preset.name == video.shader.current_crtpi_preset; });
|
||||||
|
if (IT == crtpi_presets.end()) {
|
||||||
|
std::cerr << "Options: preset CrtPi \"" << video.shader.current_crtpi_preset << "\" no trobat; aplicant defaults.\n";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
.scanline_weight = IT->scanline_weight,
|
||||||
|
.scanline_gap_brightness = IT->scanline_gap_brightness,
|
||||||
|
.bloom_factor = IT->bloom_factor,
|
||||||
|
.input_gamma = IT->input_gamma,
|
||||||
|
.output_gamma = IT->output_gamma,
|
||||||
|
.mask_brightness = IT->mask_brightness,
|
||||||
|
.curvature_x = IT->curvature_x,
|
||||||
|
.curvature_y = IT->curvature_y,
|
||||||
|
.mask_type = IT->mask_type,
|
||||||
|
.enable_scanlines = IT->enable_scanlines,
|
||||||
|
.enable_multisample = IT->enable_multisample,
|
||||||
|
.enable_gamma = IT->enable_gamma,
|
||||||
|
.enable_curvature = IT->enable_curvature,
|
||||||
|
.enable_sharper = IT->enable_sharper,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
auto buildPostFXParams() -> Rendering::PostFXParams {
|
auto buildPostFXParams() -> Rendering::PostFXParams {
|
||||||
// Si el shader està deshabilitat, retornem params a zero → passthrough.
|
// Si el shader està deshabilitat, retornem params a zero → passthrough.
|
||||||
if (!video.shader.enabled) { return {}; }
|
if (!video.shader.enabled) { return {}; }
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ namespace Options {
|
|||||||
|
|
||||||
struct Shader {
|
struct Shader {
|
||||||
bool enabled{Defaults::Video::SHADER_ENABLED};
|
bool enabled{Defaults::Video::SHADER_ENABLED};
|
||||||
|
std::string current_shader{Defaults::Video::CURRENT_SHADER}; // "postfx" o "crtpi"
|
||||||
std::string current_postfx_preset{Defaults::Video::POSTFX_DEFAULT_PRESET};
|
std::string current_postfx_preset{Defaults::Video::POSTFX_DEFAULT_PRESET};
|
||||||
|
std::string current_crtpi_preset{Defaults::Video::CRTPI_DEFAULT_PRESET};
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Video {
|
struct Video {
|
||||||
@@ -49,6 +51,25 @@ namespace Options {
|
|||||||
float flicker{0.0F};
|
float flicker{0.0F};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Preset del shader CRT-Pi (un per entrada a crtpi.yaml).
|
||||||
|
struct CrtPiPreset {
|
||||||
|
std::string name;
|
||||||
|
float scanline_weight{6.0F};
|
||||||
|
float scanline_gap_brightness{0.12F};
|
||||||
|
float bloom_factor{3.5F};
|
||||||
|
float input_gamma{2.4F};
|
||||||
|
float output_gamma{2.2F};
|
||||||
|
float mask_brightness{0.80F};
|
||||||
|
float curvature_x{0.05F};
|
||||||
|
float curvature_y{0.10F};
|
||||||
|
int mask_type{2};
|
||||||
|
bool enable_scanlines{true};
|
||||||
|
bool enable_multisample{true};
|
||||||
|
bool enable_gamma{true};
|
||||||
|
bool enable_curvature{false};
|
||||||
|
bool enable_sharper{false};
|
||||||
|
};
|
||||||
|
|
||||||
struct Music {
|
struct Music {
|
||||||
bool enabled{Defaults::Audio::MUSIC_ENABLED};
|
bool enabled{Defaults::Audio::MUSIC_ENABLED};
|
||||||
float volume{Defaults::Audio::MUSIC_VOLUME};
|
float volume{Defaults::Audio::MUSIC_VOLUME};
|
||||||
@@ -71,10 +92,12 @@ namespace Options {
|
|||||||
inline std::string config_version{};
|
inline std::string config_version{};
|
||||||
inline std::string config_file_path{};
|
inline std::string config_file_path{};
|
||||||
inline std::string postfx_file_path{};
|
inline std::string postfx_file_path{};
|
||||||
|
inline std::string crtpi_file_path{};
|
||||||
inline Window window{};
|
inline Window window{};
|
||||||
inline Video video{};
|
inline Video video{};
|
||||||
inline Audio audio{};
|
inline Audio audio{};
|
||||||
inline std::vector<PostFXPreset> postfx_presets{};
|
inline std::vector<PostFXPreset> postfx_presets{};
|
||||||
|
inline std::vector<CrtPiPreset> crtpi_presets{};
|
||||||
|
|
||||||
// --- API pública ---
|
// --- API pública ---
|
||||||
void setConfigFile(const std::string& path);
|
void setConfigFile(const std::string& path);
|
||||||
@@ -85,8 +108,14 @@ namespace Options {
|
|||||||
auto loadPostFXFromFile() -> bool;
|
auto loadPostFXFromFile() -> bool;
|
||||||
auto savePostFXToFile() -> bool;
|
auto savePostFXToFile() -> bool;
|
||||||
|
|
||||||
|
void setCrtPiFile(const std::string& path);
|
||||||
|
auto loadCrtPiFromFile() -> bool;
|
||||||
|
auto saveCrtPiToFile() -> bool;
|
||||||
|
|
||||||
// --- Builders per als subsistemes ---
|
// --- Builders per als subsistemes ---
|
||||||
[[nodiscard]] auto buildScreenConfig() -> Screen::Config;
|
[[nodiscard]] auto buildScreenConfig() -> Screen::Config;
|
||||||
[[nodiscard]] auto buildPostFXParams() -> Rendering::PostFXParams;
|
[[nodiscard]] auto buildPostFXParams() -> Rendering::PostFXParams;
|
||||||
|
[[nodiscard]] auto buildCrtPiParams() -> Rendering::CrtPiParams;
|
||||||
|
[[nodiscard]] auto buildActiveShader() -> Rendering::ShaderType;
|
||||||
|
|
||||||
} // namespace Options
|
} // namespace Options
|
||||||
@@ -35,8 +35,11 @@ namespace Defaults {
|
|||||||
constexpr bool GPU_ACCELERATION = true; // Path SDL3 GPU per defecte; fallback a SDL_Renderer si falla.
|
constexpr bool GPU_ACCELERATION = true; // Path SDL3 GPU per defecte; fallback a SDL_Renderer si falla.
|
||||||
constexpr const char* GPU_PREFERRED_DRIVER = ""; // Buit = auto. Aplica a l'arrencada.
|
constexpr const char* GPU_PREFERRED_DRIVER = ""; // Buit = auto. Aplica a l'arrencada.
|
||||||
constexpr bool SHADER_ENABLED = true; // Aplica PostFX; si false, sempre passthrough.
|
constexpr bool SHADER_ENABLED = true; // Aplica PostFX; si false, sempre passthrough.
|
||||||
constexpr const char* POSTFX_DEFAULT_PRESET = "off"; // Preset inicial a config.yaml.
|
constexpr const char* CURRENT_SHADER = "postfx"; // Shader actiu: "postfx" o "crtpi".
|
||||||
|
constexpr const char* POSTFX_DEFAULT_PRESET = "off"; // Preset inicial PostFX.
|
||||||
|
constexpr const char* CRTPI_DEFAULT_PRESET = "default"; // Preset inicial CrtPi.
|
||||||
constexpr const char* POSTFX_FILE = "shaders/postfx.yaml"; // Relatiu a la carpeta de sistema.
|
constexpr const char* POSTFX_FILE = "shaders/postfx.yaml"; // Relatiu a la carpeta de sistema.
|
||||||
|
constexpr const char* CRTPI_FILE = "shaders/crtpi.yaml"; // Relatiu a la carpeta de sistema.
|
||||||
} // namespace Video
|
} // namespace Video
|
||||||
|
|
||||||
namespace Audio {
|
namespace Audio {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
namespace Texts {
|
namespace Texts {
|
||||||
constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte — JailDesigner";
|
constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte — JailDesigner";
|
||||||
constexpr const char* COPYRIGHT = "© 2026 JailDesigner";
|
constexpr const char* COPYRIGHT = "© 2026 JailDesigner";
|
||||||
constexpr const char* VERSION = "0.5.0"; // Single source of truth — Makefile el grep
|
constexpr const char* VERSION = "0.6.0"; // Single source of truth — Makefile el grep
|
||||||
} // namespace Texts
|
} // namespace Texts
|
||||||
|
|
||||||
namespace GameCanvas {
|
namespace GameCanvas {
|
||||||
|
|||||||
@@ -17,12 +17,21 @@ cmake_policy(SET CMP0007 NEW)
|
|||||||
set(SHADER_SOURCES
|
set(SHADER_SOURCES
|
||||||
"postfx.vert"
|
"postfx.vert"
|
||||||
"postfx.frag"
|
"postfx.frag"
|
||||||
|
"crtpi_frag.glsl"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Nom de la variable C++ per a cada shader (mateix ordre)
|
# Nom de la variable C++ per a cada shader (mateix ordre)
|
||||||
set(SHADER_VARS
|
set(SHADER_VARS
|
||||||
"kpostfx_vert_spv"
|
"kpostfx_vert_spv"
|
||||||
"kpostfx_frag_spv"
|
"kpostfx_frag_spv"
|
||||||
|
"kcrtpi_frag_spv"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Flags extra per a cada shader (vacío si cap)
|
||||||
|
set(SHADER_FLAGS
|
||||||
|
""
|
||||||
|
""
|
||||||
|
"-fshader-stage=frag"
|
||||||
)
|
)
|
||||||
|
|
||||||
list(LENGTH SHADER_SOURCES NUM_SHADERS)
|
list(LENGTH SHADER_SOURCES NUM_SHADERS)
|
||||||
@@ -31,6 +40,7 @@ math(EXPR LAST_IDX "${NUM_SHADERS} - 1")
|
|||||||
foreach(IDX RANGE ${LAST_IDX})
|
foreach(IDX RANGE ${LAST_IDX})
|
||||||
list(GET SHADER_SOURCES ${IDX} SRC_NAME)
|
list(GET SHADER_SOURCES ${IDX} SRC_NAME)
|
||||||
list(GET SHADER_VARS ${IDX} VAR)
|
list(GET SHADER_VARS ${IDX} VAR)
|
||||||
|
list(GET SHADER_FLAGS ${IDX} EXTRA_FLAG)
|
||||||
|
|
||||||
# Derivem el nom del header a partir de la variable: kpostfx_vert_spv → postfx_vert_spv.h
|
# Derivem el nom del header a partir de la variable: kpostfx_vert_spv → postfx_vert_spv.h
|
||||||
string(REGEX REPLACE "^k" "" HDR_BASE "${VAR}")
|
string(REGEX REPLACE "^k" "" HDR_BASE "${VAR}")
|
||||||
@@ -40,11 +50,19 @@ foreach(IDX RANGE ${LAST_IDX})
|
|||||||
|
|
||||||
message(STATUS "Compilant ${SRC} ...")
|
message(STATUS "Compilant ${SRC} ...")
|
||||||
|
|
||||||
|
if(EXTRA_FLAG)
|
||||||
|
execute_process(
|
||||||
|
COMMAND "${GLSLC}" "${EXTRA_FLAG}" "${SRC}" -o "${SPV}"
|
||||||
|
RESULT_VARIABLE GLSLC_RESULT
|
||||||
|
ERROR_VARIABLE GLSLC_ERROR
|
||||||
|
)
|
||||||
|
else()
|
||||||
execute_process(
|
execute_process(
|
||||||
COMMAND "${GLSLC}" "${SRC}" -o "${SPV}"
|
COMMAND "${GLSLC}" "${SRC}" -o "${SPV}"
|
||||||
RESULT_VARIABLE GLSLC_RESULT
|
RESULT_VARIABLE GLSLC_RESULT
|
||||||
ERROR_VARIABLE GLSLC_ERROR
|
ERROR_VARIABLE GLSLC_ERROR
|
||||||
)
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
if(NOT GLSLC_RESULT EQUAL 0)
|
if(NOT GLSLC_RESULT EQUAL 0)
|
||||||
message(FATAL_ERROR "glslc ha fallat per a ${SRC}:\n${GLSLC_ERROR}")
|
message(FATAL_ERROR "glslc ha fallat per a ${SRC}:\n${GLSLC_ERROR}")
|
||||||
|
|||||||
Reference in new issue
Block a user