feat(shaders): sistema de shaders runtime amb presets externs

- Afegir GpuShaderPreset i ShaderManager per carregar shaders des de data/shaders/
- Implementar preset ntsc-md-rainbows (2 passos: encode + decode MAME NTSC)
- Render loop multi-pass per shaders externs (targets intermedis R16G16B16A16_FLOAT)
- cycleShader(): cicla OFF→PostFX natius→shaders externs amb tecla X
- --shader <nom> per arrancar directament amb un preset extern
- CMake auto-descubreix i compila data/shaders/**/*.vert/.frag → .spv
- HUD F1 mostra 'Shader: <nom>' quan hi ha shader extern actiu

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-20 13:37:22 +01:00
parent e3f29c864b
commit f272bab296
19 changed files with 1004 additions and 23 deletions

View File

@@ -0,0 +1,28 @@
# Based on dannyld's rainbow settings
shaders = 2
shader0 = "../crt/shaders/mame_hlsl/shaders/mame_ntsc_encode.slang"
filter_linear0 = "true"
scale_type0 = "source"
scale0 = "1.000000"
shader1 = "../crt/shaders/mame_hlsl/shaders/mame_ntsc_decode.slang"
filter_linear1 = "true"
scale_type1 = "source"
scale_1 = "1.000000"
# ntsc parameters
ntscsignal = "1.000000"
avalue = "0.000000"
bvalue = "0.000000"
scantime = "47.900070"
# optional blur
shadowalpha = "0.100000"
notch_width = "3.450001"
ifreqresponse = "1.750000"
qfreqresponse = "1.450000"
# uncomment for jailbars in blue
#pvalue = "1.100000"

View File

@@ -0,0 +1,69 @@
#version 450
// license:BSD-3-Clause
// copyright-holders:Ryan Holtz,ImJezze
// Adapted from mame_ntsc_encode.slang for SDL3 GPU / Vulkan SPIRV
layout(location=0) in vec2 v_uv;
layout(location=0) out vec4 FragColor;
layout(set=2, binding=0) uniform sampler2D Source;
layout(set=3, binding=0) uniform NTSCParams {
float source_width;
float source_height;
float a_value;
float b_value;
float cc_value;
float scan_time;
float notch_width;
float y_freq;
float i_freq;
float q_freq;
float _pad0;
float _pad1;
} u;
const float PI = 3.1415927;
const float PI2 = PI * 2.0;
void main() {
vec2 source_dims = vec2(u.source_width, u.source_height);
// p_value=1: one texel step per sub-sample (no horizontal stretch)
vec2 PValueSourceTexel = vec2(1.0, 0.0) / source_dims;
vec2 C0 = v_uv + PValueSourceTexel * vec2(0.00, 0.0);
vec2 C1 = v_uv + PValueSourceTexel * vec2(0.25, 0.0);
vec2 C2 = v_uv + PValueSourceTexel * vec2(0.50, 0.0);
vec2 C3 = v_uv + PValueSourceTexel * vec2(0.75, 0.0);
vec4 Cx = vec4(C0.x, C1.x, C2.x, C3.x);
vec4 Cy = vec4(C0.y, C1.y, C2.y, C3.y);
vec4 Texel0 = texture(Source, C0);
vec4 Texel1 = texture(Source, C1);
vec4 Texel2 = texture(Source, C2);
vec4 Texel3 = texture(Source, C3);
vec4 HPosition = Cx;
vec4 VPosition = Cy;
const vec4 YDot = vec4(0.299, 0.587, 0.114, 0.0);
const vec4 IDot = vec4(0.595716, -0.274453, -0.321263, 0.0);
const vec4 QDot = vec4(0.211456, -0.522591, 0.311135, 0.0);
vec4 Y = vec4(dot(Texel0, YDot), dot(Texel1, YDot), dot(Texel2, YDot), dot(Texel3, YDot));
vec4 I = vec4(dot(Texel0, IDot), dot(Texel1, IDot), dot(Texel2, IDot), dot(Texel3, IDot));
vec4 Q = vec4(dot(Texel0, QDot), dot(Texel1, QDot), dot(Texel2, QDot), dot(Texel3, QDot));
float W = PI2 * u.cc_value * u.scan_time;
float WoPI = W / PI;
float HOffset = u.a_value / WoPI;
float VScale = u.b_value * source_dims.y / WoPI;
vec4 T = HPosition + vec4(HOffset) + VPosition * vec4(VScale);
vec4 TW = T * W;
FragColor = Y + I * cos(TW) + Q * sin(TW);
}

Binary file not shown.

View File

@@ -0,0 +1,8 @@
#version 450
layout(location=0) out vec2 v_uv;
void main() {
vec2 positions[3] = vec2[3](vec2(-1.0,-1.0), vec2(3.0,-1.0), vec2(-1.0,3.0));
vec2 uvs[3] = vec2[3](vec2(0.0, 1.0), vec2(2.0, 1.0), vec2(0.0,-1.0));
gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
v_uv = uvs[gl_VertexIndex];
}

Binary file not shown.

View File

@@ -0,0 +1,148 @@
#version 450
// license:BSD-3-Clause
// copyright-holders:Ryan Holtz,ImJezze
// Adapted from mame_ntsc_decode.slang for SDL3 GPU / Vulkan SPIRV
layout(location=0) in vec2 v_uv;
layout(location=0) out vec4 FragColor;
layout(set=2, binding=0) uniform sampler2D Source;
layout(set=3, binding=0) uniform NTSCParams {
float source_width;
float source_height;
float a_value;
float b_value;
float cc_value;
float scan_time;
float notch_width;
float y_freq;
float i_freq;
float q_freq;
float _pad0;
float _pad1;
} u;
const float PI = 3.1415927;
const float PI2 = PI * 2.0;
const vec3 RDot = vec3(1.0, 0.956, 0.621);
const vec3 GDot = vec3(1.0, -0.272, -0.647);
const vec3 BDot = vec3(1.0, -1.106, 1.703);
const vec4 NotchOffset = vec4(0.0, 1.0, 2.0, 3.0);
const int SampleCount = 64;
const int HalfSampleCount = 32;
void main() {
vec2 source_dims = vec2(u.source_width, u.source_height);
vec4 BaseTexel = texture(Source, v_uv);
float CCValue = u.cc_value;
float ScanTime = u.scan_time;
float NotchHalfWidth = u.notch_width / 2.0;
float YFreqResponse = u.y_freq;
float IFreqResponse = u.i_freq;
float QFreqResponse = u.q_freq;
float AValue = u.a_value;
float BValue = u.b_value;
float TimePerSample = ScanTime / (source_dims.x * 4.0);
float Fc_y1 = (CCValue - NotchHalfWidth) * TimePerSample;
float Fc_y2 = (CCValue + NotchHalfWidth) * TimePerSample;
float Fc_y3 = YFreqResponse * TimePerSample;
float Fc_i = IFreqResponse * TimePerSample;
float Fc_q = QFreqResponse * TimePerSample;
float Fc_i_2 = Fc_i * 2.0;
float Fc_q_2 = Fc_q * 2.0;
float Fc_y1_2 = Fc_y1 * 2.0;
float Fc_y2_2 = Fc_y2 * 2.0;
float Fc_y3_2 = Fc_y3 * 2.0;
float Fc_i_pi2 = Fc_i * PI2;
float Fc_q_pi2 = Fc_q * PI2;
float Fc_y1_pi2 = Fc_y1 * PI2;
float Fc_y2_pi2 = Fc_y2 * PI2;
float Fc_y3_pi2 = Fc_y3 * PI2;
float PI2Length = PI2 / float(SampleCount);
float W = PI2 * CCValue * ScanTime;
float WoPI = W / PI;
float HOffset = BValue / WoPI;
float VScale = AValue * source_dims.y / WoPI;
vec4 YAccum = vec4(0.0);
vec4 IAccum = vec4(0.0);
vec4 QAccum = vec4(0.0);
vec4 Cy = vec4(v_uv.y);
vec4 VPosition = Cy;
for (float i = 0.0; i < float(SampleCount); i += 4.0) {
float n = i - float(HalfSampleCount);
vec4 n4 = n + NotchOffset;
vec4 Cx = vec4(v_uv.x) + (n4 * 0.25) / source_dims.x;
vec4 HPosition = Cx;
vec4 C = texture(Source, vec2(Cx.r, Cy.r));
vec4 T = HPosition + vec4(HOffset) + VPosition * vec4(VScale);
vec4 WT = W * T;
vec4 SincKernel = 0.54 + 0.46 * cos(PI2Length * n4);
vec4 SincYIn1 = Fc_y1_pi2 * n4;
vec4 SincYIn2 = Fc_y2_pi2 * n4;
vec4 SincYIn3 = Fc_y3_pi2 * n4;
vec4 SincIIn = Fc_i_pi2 * n4;
vec4 SincQIn = Fc_q_pi2 * n4;
vec4 SincY1, SincY2, SincY3;
SincY1.x = (SincYIn1.x != 0.0) ? sin(SincYIn1.x) / SincYIn1.x : 1.0;
SincY1.y = (SincYIn1.y != 0.0) ? sin(SincYIn1.y) / SincYIn1.y : 1.0;
SincY1.z = (SincYIn1.z != 0.0) ? sin(SincYIn1.z) / SincYIn1.z : 1.0;
SincY1.w = (SincYIn1.w != 0.0) ? sin(SincYIn1.w) / SincYIn1.w : 1.0;
SincY2.x = (SincYIn2.x != 0.0) ? sin(SincYIn2.x) / SincYIn2.x : 1.0;
SincY2.y = (SincYIn2.y != 0.0) ? sin(SincYIn2.y) / SincYIn2.y : 1.0;
SincY2.z = (SincYIn2.z != 0.0) ? sin(SincYIn2.z) / SincYIn2.z : 1.0;
SincY2.w = (SincYIn2.w != 0.0) ? sin(SincYIn2.w) / SincYIn2.w : 1.0;
SincY3.x = (SincYIn3.x != 0.0) ? sin(SincYIn3.x) / SincYIn3.x : 1.0;
SincY3.y = (SincYIn3.y != 0.0) ? sin(SincYIn3.y) / SincYIn3.y : 1.0;
SincY3.z = (SincYIn3.z != 0.0) ? sin(SincYIn3.z) / SincYIn3.z : 1.0;
SincY3.w = (SincYIn3.w != 0.0) ? sin(SincYIn3.w) / SincYIn3.w : 1.0;
vec4 IdealY = Fc_y1_2 * SincY1 - Fc_y2_2 * SincY2 + Fc_y3_2 * SincY3;
vec4 IdealI, IdealQ;
IdealI.x = Fc_i_2 * ((SincIIn.x != 0.0) ? sin(SincIIn.x) / SincIIn.x : 1.0);
IdealI.y = Fc_i_2 * ((SincIIn.y != 0.0) ? sin(SincIIn.y) / SincIIn.y : 1.0);
IdealI.z = Fc_i_2 * ((SincIIn.z != 0.0) ? sin(SincIIn.z) / SincIIn.z : 1.0);
IdealI.w = Fc_i_2 * ((SincIIn.w != 0.0) ? sin(SincIIn.w) / SincIIn.w : 1.0);
IdealQ.x = Fc_q_2 * ((SincQIn.x != 0.0) ? sin(SincQIn.x) / SincQIn.x : 1.0);
IdealQ.y = Fc_q_2 * ((SincQIn.y != 0.0) ? sin(SincQIn.y) / SincQIn.y : 1.0);
IdealQ.z = Fc_q_2 * ((SincQIn.z != 0.0) ? sin(SincQIn.z) / SincQIn.z : 1.0);
IdealQ.w = Fc_q_2 * ((SincQIn.w != 0.0) ? sin(SincQIn.w) / SincQIn.w : 1.0);
vec4 FilterY = SincKernel * IdealY;
vec4 FilterI = SincKernel * IdealI;
vec4 FilterQ = SincKernel * IdealQ;
YAccum += C * FilterY;
IAccum += C * cos(WT) * FilterI;
QAccum += C * sin(WT) * FilterQ;
}
vec3 YIQ = vec3(
(YAccum.r + YAccum.g + YAccum.b + YAccum.a),
(IAccum.r + IAccum.g + IAccum.b + IAccum.a) * 2.0,
(QAccum.r + QAccum.g + QAccum.b + QAccum.a) * 2.0);
vec3 RGB = vec3(
dot(YIQ, RDot),
dot(YIQ, GDot),
dot(YIQ, BDot));
FragColor = vec4(RGB, BaseTexel.a);
}

Binary file not shown.

View File

@@ -0,0 +1,8 @@
#version 450
layout(location=0) out vec2 v_uv;
void main() {
vec2 positions[3] = vec2[3](vec2(-1.0,-1.0), vec2(3.0,-1.0), vec2(-1.0,3.0));
vec2 uvs[3] = vec2[3](vec2(0.0, 1.0), vec2(2.0, 1.0), vec2(0.0,-1.0));
gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
v_uv = uvs[gl_VertexIndex];
}

Binary file not shown.

View File

@@ -0,0 +1,18 @@
name = ntsc-md-rainbows
passes = 2
pass0_vert = pass0_encode.vert
pass0_frag = pass0_encode.frag
pass1_vert = pass1_decode.vert
pass1_frag = pass1_decode.frag
; NTSC parameters (mapped to NTSCParams uniform buffer)
scantime = 47.9
avalue = 0.0
bvalue = 0.0
ccvalue = 3.5795455
notch_width = 3.45
yfreqresponse = 1.75
ifreqresponse = 1.75
qfreqresponse = 1.45