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:
@@ -64,6 +64,34 @@ if(NOT APPLE)
|
|||||||
endforeach()
|
endforeach()
|
||||||
|
|
||||||
add_custom_target(shaders ALL DEPENDS ${SPIRV_HEADERS})
|
add_custom_target(shaders ALL DEPENDS ${SPIRV_HEADERS})
|
||||||
|
|
||||||
|
# External runtime shaders: auto-discover and compile data/shaders/**/*.vert/*.frag
|
||||||
|
# Output: <source>.spv alongside each source file (loaded at runtime by GpuShaderPreset)
|
||||||
|
file(GLOB_RECURSE DATA_SHADERS
|
||||||
|
"${CMAKE_SOURCE_DIR}/data/shaders/**/*.vert"
|
||||||
|
"${CMAKE_SOURCE_DIR}/data/shaders/**/*.frag")
|
||||||
|
|
||||||
|
set(DATA_SHADER_SPVS)
|
||||||
|
foreach(SHADER_FILE ${DATA_SHADERS})
|
||||||
|
get_filename_component(SHADER_EXT "${SHADER_FILE}" EXT)
|
||||||
|
if(SHADER_EXT STREQUAL ".vert")
|
||||||
|
set(STAGE_FLAG "-fshader-stage=vertex")
|
||||||
|
else()
|
||||||
|
set(STAGE_FLAG "-fshader-stage=fragment")
|
||||||
|
endif()
|
||||||
|
set(SPV_FILE "${SHADER_FILE}.spv")
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT "${SPV_FILE}"
|
||||||
|
COMMAND "${GLSLC}" ${STAGE_FLAG} -o "${SPV_FILE}" "${SHADER_FILE}"
|
||||||
|
DEPENDS "${SHADER_FILE}"
|
||||||
|
COMMENT "Compiling ${SHADER_FILE}"
|
||||||
|
)
|
||||||
|
list(APPEND DATA_SHADER_SPVS "${SPV_FILE}")
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
if(DATA_SHADER_SPVS)
|
||||||
|
add_custom_target(data_shaders ALL DEPENDS ${DATA_SHADER_SPVS})
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Archivos fuente (excluir main_old.cpp)
|
# Archivos fuente (excluir main_old.cpp)
|
||||||
@@ -105,6 +133,9 @@ target_link_libraries(${PROJECT_NAME} ${LINK_LIBS})
|
|||||||
if(NOT APPLE)
|
if(NOT APPLE)
|
||||||
add_dependencies(${PROJECT_NAME} shaders)
|
add_dependencies(${PROJECT_NAME} shaders)
|
||||||
target_include_directories(${PROJECT_NAME} PRIVATE "${SHADER_GEN_DIR}")
|
target_include_directories(${PROJECT_NAME} PRIVATE "${SHADER_GEN_DIR}")
|
||||||
|
if(TARGET data_shaders)
|
||||||
|
add_dependencies(${PROJECT_NAME} data_shaders)
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Tool: pack_resources
|
# Tool: pack_resources
|
||||||
|
|||||||
28
data/shaders/ntsc-md-rainbows.slangp
Normal file
28
data/shaders/ntsc-md-rainbows.slangp
Normal 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"
|
||||||
69
data/shaders/ntsc-md-rainbows/pass0_encode.frag
Normal file
69
data/shaders/ntsc-md-rainbows/pass0_encode.frag
Normal 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);
|
||||||
|
}
|
||||||
BIN
data/shaders/ntsc-md-rainbows/pass0_encode.frag.spv
Normal file
BIN
data/shaders/ntsc-md-rainbows/pass0_encode.frag.spv
Normal file
Binary file not shown.
8
data/shaders/ntsc-md-rainbows/pass0_encode.vert
Normal file
8
data/shaders/ntsc-md-rainbows/pass0_encode.vert
Normal 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];
|
||||||
|
}
|
||||||
BIN
data/shaders/ntsc-md-rainbows/pass0_encode.vert.spv
Normal file
BIN
data/shaders/ntsc-md-rainbows/pass0_encode.vert.spv
Normal file
Binary file not shown.
148
data/shaders/ntsc-md-rainbows/pass1_decode.frag
Normal file
148
data/shaders/ntsc-md-rainbows/pass1_decode.frag
Normal 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);
|
||||||
|
}
|
||||||
BIN
data/shaders/ntsc-md-rainbows/pass1_decode.frag.spv
Normal file
BIN
data/shaders/ntsc-md-rainbows/pass1_decode.frag.spv
Normal file
Binary file not shown.
8
data/shaders/ntsc-md-rainbows/pass1_decode.vert
Normal file
8
data/shaders/ntsc-md-rainbows/pass1_decode.vert
Normal 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];
|
||||||
|
}
|
||||||
BIN
data/shaders/ntsc-md-rainbows/pass1_decode.vert.spv
Normal file
BIN
data/shaders/ntsc-md-rainbows/pass1_decode.vert.spv
Normal file
Binary file not shown.
18
data/shaders/ntsc-md-rainbows/preset.ini
Normal file
18
data/shaders/ntsc-md-rainbows/preset.ini
Normal 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
|
||||||
@@ -353,6 +353,27 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMod
|
|||||||
ResourceManager::loadResource("shapes/jailgames.png", tmp, tmp_size);
|
ResourceManager::loadResource("shapes/jailgames.png", tmp, tmp_size);
|
||||||
delete[] tmp;
|
delete[] tmp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inicializar ShaderManager (shaders externos desde data/shaders/)
|
||||||
|
if (gpu_ctx_ && success) {
|
||||||
|
shader_manager_ = std::make_unique<ShaderManager>();
|
||||||
|
std::string shaders_dir = getResourcesDirectory() + "/data/shaders";
|
||||||
|
shader_manager_->scan(shaders_dir);
|
||||||
|
|
||||||
|
// Si se especificó --shader, activar el preset inicial
|
||||||
|
if (!initial_shader_name_.empty()) {
|
||||||
|
active_shader_ = shader_manager_->load(
|
||||||
|
gpu_ctx_->device(),
|
||||||
|
initial_shader_name_,
|
||||||
|
gpu_ctx_->swapchainFormat(),
|
||||||
|
current_screen_width_, current_screen_height_);
|
||||||
|
if (active_shader_) {
|
||||||
|
const auto& names = shader_manager_->names();
|
||||||
|
auto it = std::find(names.begin(), names.end(), initial_shader_name_);
|
||||||
|
active_shader_idx_ = (it != names.end()) ? (int)(it - names.begin()) : -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
@@ -387,6 +408,7 @@ void Engine::shutdown() {
|
|||||||
if (sprite_batch_) { sprite_batch_->destroy(gpu_ctx_->device()); sprite_batch_.reset(); }
|
if (sprite_batch_) { sprite_batch_->destroy(gpu_ctx_->device()); sprite_batch_.reset(); }
|
||||||
if (gpu_ball_buffer_) { gpu_ball_buffer_->destroy(gpu_ctx_->device()); gpu_ball_buffer_.reset(); }
|
if (gpu_ball_buffer_) { gpu_ball_buffer_->destroy(gpu_ctx_->device()); gpu_ball_buffer_.reset(); }
|
||||||
if (gpu_pipeline_) { gpu_pipeline_->destroy(gpu_ctx_->device()); gpu_pipeline_.reset(); }
|
if (gpu_pipeline_) { gpu_pipeline_->destroy(gpu_ctx_->device()); gpu_pipeline_.reset(); }
|
||||||
|
if (shader_manager_) { shader_manager_->destroyAll(gpu_ctx_->device()); shader_manager_.reset(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Destroy software UI renderer and surface
|
// Destroy software UI renderer and surface
|
||||||
@@ -845,20 +867,14 @@ void Engine::render() {
|
|||||||
SDL_EndGPURenderPass(pass1);
|
SDL_EndGPURenderPass(pass1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Pass 2: PostFX (vignette) + UI overlay to swapchain ===
|
// === Pass 2+: External multi-pass shader OR native PostFX → swapchain ===
|
||||||
Uint32 sw_w = 0, sw_h = 0;
|
Uint32 sw_w = 0, sw_h = 0;
|
||||||
SDL_GPUTexture* swapchain = gpu_ctx_->acquireSwapchainTexture(cmd, &sw_w, &sw_h);
|
SDL_GPUTexture* swapchain = gpu_ctx_->acquireSwapchainTexture(cmd, &sw_w, &sw_h);
|
||||||
if (swapchain && offscreen_tex_ && offscreen_tex_->isValid()) {
|
if (swapchain && offscreen_tex_ && offscreen_tex_->isValid()) {
|
||||||
SDL_GPUColorTargetInfo ct = {};
|
|
||||||
ct.texture = swapchain;
|
|
||||||
ct.load_op = SDL_GPU_LOADOP_CLEAR;
|
|
||||||
ct.clear_color = {0.0f, 0.0f, 0.0f, 1.0f};
|
|
||||||
ct.store_op = SDL_GPU_STOREOP_STORE;
|
|
||||||
|
|
||||||
SDL_GPURenderPass* pass2 = SDL_BeginGPURenderPass(cmd, &ct, 1, nullptr);
|
// Helper lambda for viewport/scissor (used in the final pass)
|
||||||
|
auto applyViewport = [&](SDL_GPURenderPass* rp) {
|
||||||
// Viewport/scissor per integer scaling (only F3 fullscreen)
|
if (!fullscreen_enabled_) return;
|
||||||
if (fullscreen_enabled_) {
|
|
||||||
float vp_x, vp_y, vp_w, vp_h;
|
float vp_x, vp_y, vp_w, vp_h;
|
||||||
if (current_scaling_mode_ == ScalingMode::STRETCH) {
|
if (current_scaling_mode_ == ScalingMode::STRETCH) {
|
||||||
vp_x = 0.0f; vp_y = 0.0f;
|
vp_x = 0.0f; vp_y = 0.0f;
|
||||||
@@ -881,11 +897,90 @@ void Engine::render() {
|
|||||||
vp_y = (static_cast<float>(sw_h) - vp_h) * 0.5f;
|
vp_y = (static_cast<float>(sw_h) - vp_h) * 0.5f;
|
||||||
}
|
}
|
||||||
SDL_GPUViewport vp = {vp_x, vp_y, vp_w, vp_h, 0.0f, 1.0f};
|
SDL_GPUViewport vp = {vp_x, vp_y, vp_w, vp_h, 0.0f, 1.0f};
|
||||||
SDL_SetGPUViewport(pass2, &vp);
|
SDL_SetGPUViewport(rp, &vp);
|
||||||
SDL_Rect scissor = {static_cast<int>(vp_x), static_cast<int>(vp_y),
|
SDL_Rect scissor = {static_cast<int>(vp_x), static_cast<int>(vp_y),
|
||||||
static_cast<int>(vp_w), static_cast<int>(vp_h)};
|
static_cast<int>(vp_w), static_cast<int>(vp_h)};
|
||||||
SDL_SetGPUScissor(pass2, &scissor);
|
SDL_SetGPUScissor(rp, &scissor);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (active_shader_ != nullptr) {
|
||||||
|
// --- External multi-pass shader ---
|
||||||
|
NTSCParams ntsc = {};
|
||||||
|
ntsc.source_width = static_cast<float>(current_screen_width_);
|
||||||
|
ntsc.source_height = static_cast<float>(current_screen_height_);
|
||||||
|
ntsc.a_value = active_shader_->param("avalue", 0.0f);
|
||||||
|
ntsc.b_value = active_shader_->param("bvalue", 0.0f);
|
||||||
|
ntsc.cc_value = active_shader_->param("ccvalue", 3.5795455f);
|
||||||
|
ntsc.scan_time = active_shader_->param("scantime", 47.9f);
|
||||||
|
ntsc.notch_width = active_shader_->param("notch_width", 3.45f);
|
||||||
|
ntsc.y_freq = active_shader_->param("yfreqresponse", 1.75f);
|
||||||
|
ntsc.i_freq = active_shader_->param("ifreqresponse", 1.75f);
|
||||||
|
ntsc.q_freq = active_shader_->param("qfreqresponse", 1.45f);
|
||||||
|
|
||||||
|
SDL_GPUTexture* current_input = offscreen_tex_->texture();
|
||||||
|
SDL_GPUSampler* current_samp = offscreen_tex_->sampler();
|
||||||
|
|
||||||
|
for (int pi = 0; pi < active_shader_->passCount(); ++pi) {
|
||||||
|
ShaderPass& sp = active_shader_->pass(pi);
|
||||||
|
bool is_last = (pi == active_shader_->passCount() - 1);
|
||||||
|
|
||||||
|
SDL_GPUTexture* target_tex = is_last ? swapchain : sp.target->texture();
|
||||||
|
SDL_GPULoadOp load_op = SDL_GPU_LOADOP_CLEAR;
|
||||||
|
|
||||||
|
SDL_GPUColorTargetInfo ct = {};
|
||||||
|
ct.texture = target_tex;
|
||||||
|
ct.load_op = load_op;
|
||||||
|
ct.clear_color = {0.0f, 0.0f, 0.0f, 1.0f};
|
||||||
|
ct.store_op = SDL_GPU_STOREOP_STORE;
|
||||||
|
|
||||||
|
SDL_GPURenderPass* ext_pass = SDL_BeginGPURenderPass(cmd, &ct, 1, nullptr);
|
||||||
|
if (is_last) applyViewport(ext_pass);
|
||||||
|
|
||||||
|
SDL_BindGPUGraphicsPipeline(ext_pass, sp.pipeline);
|
||||||
|
SDL_GPUTextureSamplerBinding src_tsb = {current_input, current_samp};
|
||||||
|
SDL_BindGPUFragmentSamplers(ext_pass, 0, &src_tsb, 1);
|
||||||
|
SDL_PushGPUFragmentUniformData(cmd, 0, &ntsc, sizeof(NTSCParams));
|
||||||
|
SDL_DrawGPUPrimitives(ext_pass, 3, 1, 0, 0);
|
||||||
|
|
||||||
|
SDL_EndGPURenderPass(ext_pass);
|
||||||
|
|
||||||
|
if (!is_last) {
|
||||||
|
current_input = sp.target->texture();
|
||||||
|
current_samp = sp.target->sampler();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-open swapchain pass for UI overlay
|
||||||
|
SDL_GPUColorTargetInfo ct_ui = {};
|
||||||
|
ct_ui.texture = swapchain;
|
||||||
|
ct_ui.load_op = SDL_GPU_LOADOP_LOAD; // preserve shader output
|
||||||
|
ct_ui.store_op = SDL_GPU_STOREOP_STORE;
|
||||||
|
SDL_GPURenderPass* pass2 = SDL_BeginGPURenderPass(cmd, &ct_ui, 1, nullptr);
|
||||||
|
applyViewport(pass2);
|
||||||
|
|
||||||
|
if (ui_tex_ && ui_tex_->isValid() && sprite_batch_->overlayIndexCount() > 0) {
|
||||||
|
SDL_BindGPUGraphicsPipeline(pass2, gpu_pipeline_->spritePipeline());
|
||||||
|
SDL_GPUBufferBinding vb = {sprite_batch_->vertexBuffer(), 0};
|
||||||
|
SDL_GPUBufferBinding ib = {sprite_batch_->indexBuffer(), 0};
|
||||||
|
SDL_BindGPUVertexBuffers(pass2, 0, &vb, 1);
|
||||||
|
SDL_BindGPUIndexBuffer(pass2, &ib, SDL_GPU_INDEXELEMENTSIZE_32BIT);
|
||||||
|
SDL_GPUTextureSamplerBinding ui_tsb = {ui_tex_->texture(), ui_tex_->sampler()};
|
||||||
|
SDL_BindGPUFragmentSamplers(pass2, 0, &ui_tsb, 1);
|
||||||
|
SDL_DrawGPUIndexedPrimitives(pass2, sprite_batch_->overlayIndexCount(), 1,
|
||||||
|
sprite_batch_->overlayIndexOffset(), 0, 0);
|
||||||
|
}
|
||||||
|
SDL_EndGPURenderPass(pass2);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// --- Native PostFX path ---
|
||||||
|
SDL_GPUColorTargetInfo ct = {};
|
||||||
|
ct.texture = swapchain;
|
||||||
|
ct.load_op = SDL_GPU_LOADOP_CLEAR;
|
||||||
|
ct.clear_color = {0.0f, 0.0f, 0.0f, 1.0f};
|
||||||
|
ct.store_op = SDL_GPU_STOREOP_STORE;
|
||||||
|
|
||||||
|
SDL_GPURenderPass* pass2 = SDL_BeginGPURenderPass(cmd, &ct, 1, nullptr);
|
||||||
|
applyViewport(pass2);
|
||||||
|
|
||||||
// PostFX: full-screen triangle via vertex_id (no vertex buffer needed)
|
// PostFX: full-screen triangle via vertex_id (no vertex buffer needed)
|
||||||
SDL_BindGPUGraphicsPipeline(pass2, gpu_pipeline_->postfxPipeline());
|
SDL_BindGPUGraphicsPipeline(pass2, gpu_pipeline_->postfxPipeline());
|
||||||
@@ -908,7 +1003,8 @@ void Engine::render() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SDL_EndGPURenderPass(pass2);
|
SDL_EndGPURenderPass(pass2);
|
||||||
}
|
} // end else (native PostFX)
|
||||||
|
} // end if (swapchain && ...)
|
||||||
|
|
||||||
gpu_ctx_->submit(cmd);
|
gpu_ctx_->submit(cmd);
|
||||||
}
|
}
|
||||||
@@ -1059,14 +1155,8 @@ void Engine::applyPostFXPreset(int mode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Engine::handlePostFXCycle() {
|
void Engine::handlePostFXCycle() {
|
||||||
static constexpr const char* names[4] = {
|
// Delegate to cycleShader() which handles both native PostFX and external shaders
|
||||||
"PostFX: Vinyeta", "PostFX: Scanlines",
|
cycleShader();
|
||||||
"PostFX: Cromàtica", "PostFX: Complet"
|
|
||||||
};
|
|
||||||
postfx_effect_mode_ = (postfx_effect_mode_ + 1) % 4;
|
|
||||||
postfx_enabled_ = true;
|
|
||||||
applyPostFXPreset(postfx_effect_mode_);
|
|
||||||
showNotificationForAction(names[postfx_effect_mode_]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Engine::handlePostFXToggle() {
|
void Engine::handlePostFXToggle() {
|
||||||
@@ -1074,6 +1164,16 @@ void Engine::handlePostFXToggle() {
|
|||||||
"PostFX: Vinyeta", "PostFX: Scanlines",
|
"PostFX: Vinyeta", "PostFX: Scanlines",
|
||||||
"PostFX: Cromàtica", "PostFX: Complet"
|
"PostFX: Cromàtica", "PostFX: Complet"
|
||||||
};
|
};
|
||||||
|
// If external shader is active, toggle it off
|
||||||
|
if (active_shader_) {
|
||||||
|
active_shader_ = nullptr;
|
||||||
|
active_shader_idx_ = 0; // reset to OFF
|
||||||
|
postfx_uniforms_.vignette_strength = 0.0f;
|
||||||
|
postfx_uniforms_.chroma_strength = 0.0f;
|
||||||
|
postfx_uniforms_.scanline_strength = 0.0f;
|
||||||
|
showNotificationForAction("PostFX: Desactivat");
|
||||||
|
return;
|
||||||
|
}
|
||||||
postfx_enabled_ = !postfx_enabled_;
|
postfx_enabled_ = !postfx_enabled_;
|
||||||
if (postfx_enabled_) {
|
if (postfx_enabled_) {
|
||||||
applyPostFXPreset(postfx_effect_mode_);
|
applyPostFXPreset(postfx_effect_mode_);
|
||||||
@@ -1101,6 +1201,83 @@ void Engine::setPostFXParamOverrides(float vignette, float chroma) {
|
|||||||
if (chroma >= 0.f) postfx_uniforms_.chroma_strength = chroma;
|
if (chroma >= 0.f) postfx_uniforms_.chroma_strength = chroma;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Engine::setInitialShader(const std::string& name) {
|
||||||
|
initial_shader_name_ = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::cycleShader() {
|
||||||
|
// Cycle order:
|
||||||
|
// native OFF → native Vinyeta → Scanlines → Cromàtica → Complet →
|
||||||
|
// ext shader 0 → ext shader 1 → ... → native OFF → ...
|
||||||
|
if (!shader_manager_) {
|
||||||
|
// No shader manager: fall back to native PostFX cycle
|
||||||
|
handlePostFXCycle();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// active_shader_idx_ is a 0-based cycle counter:
|
||||||
|
// -1 = uninitialized (first press → index 0 = OFF)
|
||||||
|
// 0 = OFF
|
||||||
|
// 1 = PostFX Vinyeta, 2 = Scanlines, 3 = Cromàtica, 4 = Complet
|
||||||
|
// 5..4+num_ext = external shaders (0-based into names())
|
||||||
|
const int num_native = 5; // 0=OFF, 1..4=PostFX modes
|
||||||
|
const int num_ext = static_cast<int>(shader_manager_->names().size());
|
||||||
|
const int total = num_native + num_ext;
|
||||||
|
|
||||||
|
static const char* native_names[5] = {
|
||||||
|
"PostFX: Desactivat", "PostFX: Vinyeta", "PostFX: Scanlines",
|
||||||
|
"PostFX: Cromàtica", "PostFX: Complet"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Advance and wrap
|
||||||
|
int cycle = active_shader_idx_ + 1;
|
||||||
|
if (cycle < 0 || cycle >= total) cycle = 0;
|
||||||
|
active_shader_idx_ = cycle;
|
||||||
|
|
||||||
|
if (cycle < num_native) {
|
||||||
|
// Native PostFX
|
||||||
|
active_shader_ = nullptr;
|
||||||
|
if (cycle == 0) {
|
||||||
|
postfx_enabled_ = false;
|
||||||
|
postfx_uniforms_.vignette_strength = 0.0f;
|
||||||
|
postfx_uniforms_.chroma_strength = 0.0f;
|
||||||
|
postfx_uniforms_.scanline_strength = 0.0f;
|
||||||
|
} else {
|
||||||
|
postfx_enabled_ = true;
|
||||||
|
postfx_effect_mode_ = cycle - 1; // 0..3
|
||||||
|
applyPostFXPreset(postfx_effect_mode_);
|
||||||
|
}
|
||||||
|
showNotificationForAction(native_names[cycle]);
|
||||||
|
} else {
|
||||||
|
// External shader
|
||||||
|
int ext_idx = cycle - num_native;
|
||||||
|
const std::string& shader_name = shader_manager_->names()[ext_idx];
|
||||||
|
GpuShaderPreset* preset = shader_manager_->load(
|
||||||
|
gpu_ctx_->device(),
|
||||||
|
shader_name,
|
||||||
|
gpu_ctx_->swapchainFormat(),
|
||||||
|
current_screen_width_, current_screen_height_);
|
||||||
|
if (preset) {
|
||||||
|
active_shader_ = preset;
|
||||||
|
postfx_enabled_ = false;
|
||||||
|
postfx_uniforms_.vignette_strength = 0.0f;
|
||||||
|
postfx_uniforms_.chroma_strength = 0.0f;
|
||||||
|
postfx_uniforms_.scanline_strength = 0.0f;
|
||||||
|
showNotificationForAction("Shader: " + shader_name);
|
||||||
|
} else {
|
||||||
|
// Failed to load: skip to next
|
||||||
|
SDL_Log("Engine::cycleShader: failed to load '%s', skipping", shader_name.c_str());
|
||||||
|
active_shader_ = nullptr;
|
||||||
|
showNotificationForAction("Shader: ERROR " + shader_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Engine::getActiveShaderName() const {
|
||||||
|
if (active_shader_) return active_shader_->name();
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
void Engine::toggleIntegerScaling() {
|
void Engine::toggleIntegerScaling() {
|
||||||
// Ciclar entre los 3 modos: INTEGER → LETTERBOX → STRETCH → INTEGER
|
// Ciclar entre los 3 modos: INTEGER → LETTERBOX → STRETCH → INTEGER
|
||||||
switch (current_scaling_mode_) {
|
switch (current_scaling_mode_) {
|
||||||
@@ -1443,6 +1620,12 @@ void Engine::recreateOffscreenTexture() {
|
|||||||
current_screen_width_, current_screen_height_, // physical
|
current_screen_width_, current_screen_height_, // physical
|
||||||
base_screen_width_, base_screen_height_); // logical (font size based on base)
|
base_screen_width_, base_screen_height_); // logical (font size based on base)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recreate external shader intermediate targets
|
||||||
|
if (shader_manager_) {
|
||||||
|
shader_manager_->onResize(gpu_ctx_->device(), gpu_ctx_->swapchainFormat(),
|
||||||
|
current_screen_width_, current_screen_height_);
|
||||||
|
}
|
||||||
if (ui_renderer_ && app_logo_) {
|
if (ui_renderer_ && app_logo_) {
|
||||||
app_logo_->initialize(ui_renderer_, current_screen_width_, current_screen_height_);
|
app_logo_->initialize(ui_renderer_, current_screen_width_, current_screen_height_);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,10 @@
|
|||||||
#include "gpu/gpu_ball_buffer.hpp" // for GpuBallBuffer, BallGPUData
|
#include "gpu/gpu_ball_buffer.hpp" // for GpuBallBuffer, BallGPUData
|
||||||
#include "gpu/gpu_context.hpp" // for GpuContext
|
#include "gpu/gpu_context.hpp" // for GpuContext
|
||||||
#include "gpu/gpu_pipeline.hpp" // for GpuPipeline
|
#include "gpu/gpu_pipeline.hpp" // for GpuPipeline
|
||||||
|
#include "gpu/gpu_shader_preset.hpp" // for NTSCParams, GpuShaderPreset
|
||||||
#include "gpu/gpu_sprite_batch.hpp" // for GpuSpriteBatch
|
#include "gpu/gpu_sprite_batch.hpp" // for GpuSpriteBatch
|
||||||
#include "gpu/gpu_texture.hpp" // for GpuTexture
|
#include "gpu/gpu_texture.hpp" // for GpuTexture
|
||||||
|
#include "gpu/shader_manager.hpp" // for ShaderManager
|
||||||
#include "input/input_handler.hpp" // for InputHandler
|
#include "input/input_handler.hpp" // for InputHandler
|
||||||
#include "scene/scene_manager.hpp" // for SceneManager
|
#include "scene/scene_manager.hpp" // for SceneManager
|
||||||
#include "shapes_mgr/shape_manager.hpp" // for ShapeManager
|
#include "shapes_mgr/shape_manager.hpp" // for ShapeManager
|
||||||
@@ -82,6 +84,11 @@ class Engine {
|
|||||||
void setInitialPostFX(int mode);
|
void setInitialPostFX(int mode);
|
||||||
void setPostFXParamOverrides(float vignette, float chroma);
|
void setPostFXParamOverrides(float vignette, float chroma);
|
||||||
|
|
||||||
|
// External shader presets (loaded from data/shaders/)
|
||||||
|
void cycleShader();
|
||||||
|
void setInitialShader(const std::string& name);
|
||||||
|
std::string getActiveShaderName() const;
|
||||||
|
|
||||||
// Modo kiosko
|
// Modo kiosko
|
||||||
void setKioskMode(bool enabled) { kiosk_mode_ = enabled; }
|
void setKioskMode(bool enabled) { kiosk_mode_ = enabled; }
|
||||||
bool isKioskMode() const { return kiosk_mode_; }
|
bool isKioskMode() const { return kiosk_mode_; }
|
||||||
@@ -130,6 +137,7 @@ class Engine {
|
|||||||
float getPostFXVignette() const { return postfx_uniforms_.vignette_strength; }
|
float getPostFXVignette() const { return postfx_uniforms_.vignette_strength; }
|
||||||
float getPostFXChroma() const { return postfx_uniforms_.chroma_strength; }
|
float getPostFXChroma() const { return postfx_uniforms_.chroma_strength; }
|
||||||
float getPostFXScanline() const { return postfx_uniforms_.scanline_strength; }
|
float getPostFXScanline() const { return postfx_uniforms_.scanline_strength; }
|
||||||
|
bool isExternalShaderActive() const { return active_shader_ != nullptr; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// === Componentes del sistema (Composición) ===
|
// === Componentes del sistema (Composición) ===
|
||||||
@@ -184,6 +192,12 @@ class Engine {
|
|||||||
float postfx_override_vignette_ = -1.f; // -1 = sin override
|
float postfx_override_vignette_ = -1.f; // -1 = sin override
|
||||||
float postfx_override_chroma_ = -1.f;
|
float postfx_override_chroma_ = -1.f;
|
||||||
|
|
||||||
|
// External shader system
|
||||||
|
std::unique_ptr<ShaderManager> shader_manager_;
|
||||||
|
GpuShaderPreset* active_shader_ = nullptr; // null = native PostFX
|
||||||
|
int active_shader_idx_ = -1; // index into shader_manager_->names()
|
||||||
|
std::string initial_shader_name_; // set before initialize()
|
||||||
|
|
||||||
// Sistema de zoom dinámico
|
// Sistema de zoom dinámico
|
||||||
int current_window_zoom_ = DEFAULT_WINDOW_ZOOM;
|
int current_window_zoom_ = DEFAULT_WINDOW_ZOOM;
|
||||||
|
|
||||||
|
|||||||
257
source/gpu/gpu_shader_preset.cpp
Normal file
257
source/gpu/gpu_shader_preset.cpp
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
#include "gpu_shader_preset.hpp"
|
||||||
|
#include "gpu_texture.hpp"
|
||||||
|
|
||||||
|
#include <SDL3/SDL_log.h>
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Helpers
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
static std::vector<uint8_t> readFile(const std::string& path) {
|
||||||
|
std::ifstream f(path, std::ios::binary | std::ios::ate);
|
||||||
|
if (!f) return {};
|
||||||
|
std::streamsize sz = f.tellg();
|
||||||
|
f.seekg(0, std::ios::beg);
|
||||||
|
std::vector<uint8_t> buf(static_cast<size_t>(sz));
|
||||||
|
if (!f.read(reinterpret_cast<char*>(buf.data()), sz)) return {};
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string trim(const std::string& s) {
|
||||||
|
size_t a = s.find_first_not_of(" \t\r\n");
|
||||||
|
if (a == std::string::npos) return {};
|
||||||
|
size_t b = s.find_last_not_of(" \t\r\n");
|
||||||
|
return s.substr(a, b - a + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// GpuShaderPreset
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
bool GpuShaderPreset::parseIni(const std::string& ini_path) {
|
||||||
|
std::ifstream f(ini_path);
|
||||||
|
if (!f) {
|
||||||
|
SDL_Log("GpuShaderPreset: cannot open %s", ini_path.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int num_passes = 0;
|
||||||
|
std::string line;
|
||||||
|
while (std::getline(f, line)) {
|
||||||
|
// Strip comments
|
||||||
|
auto comment = line.find(';');
|
||||||
|
if (comment != std::string::npos) line = line.substr(0, comment);
|
||||||
|
line = trim(line);
|
||||||
|
if (line.empty()) continue;
|
||||||
|
|
||||||
|
auto eq = line.find('=');
|
||||||
|
if (eq == std::string::npos) continue;
|
||||||
|
|
||||||
|
std::string key = trim(line.substr(0, eq));
|
||||||
|
std::string value = trim(line.substr(eq + 1));
|
||||||
|
if (key.empty() || value.empty()) continue;
|
||||||
|
|
||||||
|
if (key == "name") {
|
||||||
|
name_ = value;
|
||||||
|
} else if (key == "passes") {
|
||||||
|
num_passes = std::stoi(value);
|
||||||
|
} else {
|
||||||
|
// Try to parse as float parameter
|
||||||
|
try {
|
||||||
|
params_[key] = std::stof(value);
|
||||||
|
} catch (...) {
|
||||||
|
// Non-float values stored separately (pass0_vert etc.)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (num_passes <= 0) {
|
||||||
|
SDL_Log("GpuShaderPreset: no passes defined in %s", ini_path.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second pass: read per-pass file names
|
||||||
|
f.clear();
|
||||||
|
f.seekg(0, std::ios::beg);
|
||||||
|
descs_.resize(num_passes);
|
||||||
|
while (std::getline(f, line)) {
|
||||||
|
auto comment = line.find(';');
|
||||||
|
if (comment != std::string::npos) line = line.substr(0, comment);
|
||||||
|
line = trim(line);
|
||||||
|
if (line.empty()) continue;
|
||||||
|
auto eq = line.find('=');
|
||||||
|
if (eq == std::string::npos) continue;
|
||||||
|
std::string key = trim(line.substr(0, eq));
|
||||||
|
std::string value = trim(line.substr(eq + 1));
|
||||||
|
|
||||||
|
for (int i = 0; i < num_passes; ++i) {
|
||||||
|
std::string vi = "pass" + std::to_string(i) + "_vert";
|
||||||
|
std::string fi = "pass" + std::to_string(i) + "_frag";
|
||||||
|
if (key == vi) descs_[i].vert_name = value;
|
||||||
|
if (key == fi) descs_[i].frag_name = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate
|
||||||
|
for (int i = 0; i < num_passes; ++i) {
|
||||||
|
if (descs_[i].vert_name.empty() || descs_[i].frag_name.empty()) {
|
||||||
|
SDL_Log("GpuShaderPreset: pass %d missing vert or frag in %s", i, ini_path.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_GPUGraphicsPipeline* GpuShaderPreset::buildPassPipeline(SDL_GPUDevice* device,
|
||||||
|
const std::string& vert_spv_path,
|
||||||
|
const std::string& frag_spv_path,
|
||||||
|
SDL_GPUTextureFormat target_fmt) {
|
||||||
|
auto vert_spv = readFile(vert_spv_path);
|
||||||
|
auto frag_spv = readFile(frag_spv_path);
|
||||||
|
if (vert_spv.empty()) {
|
||||||
|
SDL_Log("GpuShaderPreset: cannot read %s", vert_spv_path.c_str());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (frag_spv.empty()) {
|
||||||
|
SDL_Log("GpuShaderPreset: cannot read %s", frag_spv_path.c_str());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_GPUShaderCreateInfo vert_info = {};
|
||||||
|
vert_info.code = vert_spv.data();
|
||||||
|
vert_info.code_size = vert_spv.size();
|
||||||
|
vert_info.entrypoint = "main";
|
||||||
|
vert_info.format = SDL_GPU_SHADERFORMAT_SPIRV;
|
||||||
|
vert_info.stage = SDL_GPU_SHADERSTAGE_VERTEX;
|
||||||
|
vert_info.num_samplers = 0;
|
||||||
|
vert_info.num_uniform_buffers = 0;
|
||||||
|
|
||||||
|
SDL_GPUShaderCreateInfo frag_info = {};
|
||||||
|
frag_info.code = frag_spv.data();
|
||||||
|
frag_info.code_size = frag_spv.size();
|
||||||
|
frag_info.entrypoint = "main";
|
||||||
|
frag_info.format = SDL_GPU_SHADERFORMAT_SPIRV;
|
||||||
|
frag_info.stage = SDL_GPU_SHADERSTAGE_FRAGMENT;
|
||||||
|
frag_info.num_samplers = 1;
|
||||||
|
frag_info.num_uniform_buffers = 1;
|
||||||
|
|
||||||
|
SDL_GPUShader* vert_shader = SDL_CreateGPUShader(device, &vert_info);
|
||||||
|
SDL_GPUShader* frag_shader = SDL_CreateGPUShader(device, &frag_info);
|
||||||
|
|
||||||
|
if (!vert_shader || !frag_shader) {
|
||||||
|
SDL_Log("GpuShaderPreset: shader creation failed for %s / %s: %s",
|
||||||
|
vert_spv_path.c_str(), frag_spv_path.c_str(), SDL_GetError());
|
||||||
|
if (vert_shader) SDL_ReleaseGPUShader(device, vert_shader);
|
||||||
|
if (frag_shader) SDL_ReleaseGPUShader(device, frag_shader);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full-screen triangle: no vertex input, no blend
|
||||||
|
SDL_GPUColorTargetBlendState no_blend = {};
|
||||||
|
no_blend.enable_blend = false;
|
||||||
|
no_blend.enable_color_write_mask = false;
|
||||||
|
|
||||||
|
SDL_GPUColorTargetDescription ct_desc = {};
|
||||||
|
ct_desc.format = target_fmt;
|
||||||
|
ct_desc.blend_state = no_blend;
|
||||||
|
|
||||||
|
SDL_GPUVertexInputState no_input = {};
|
||||||
|
|
||||||
|
SDL_GPUGraphicsPipelineCreateInfo pipe_info = {};
|
||||||
|
pipe_info.vertex_shader = vert_shader;
|
||||||
|
pipe_info.fragment_shader = frag_shader;
|
||||||
|
pipe_info.vertex_input_state = no_input;
|
||||||
|
pipe_info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
|
||||||
|
pipe_info.target_info.num_color_targets = 1;
|
||||||
|
pipe_info.target_info.color_target_descriptions = &ct_desc;
|
||||||
|
|
||||||
|
SDL_GPUGraphicsPipeline* pipeline = SDL_CreateGPUGraphicsPipeline(device, &pipe_info);
|
||||||
|
|
||||||
|
SDL_ReleaseGPUShader(device, vert_shader);
|
||||||
|
SDL_ReleaseGPUShader(device, frag_shader);
|
||||||
|
|
||||||
|
if (!pipeline)
|
||||||
|
SDL_Log("GpuShaderPreset: pipeline creation failed: %s", SDL_GetError());
|
||||||
|
|
||||||
|
return pipeline;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GpuShaderPreset::load(SDL_GPUDevice* device,
|
||||||
|
const std::string& dir,
|
||||||
|
SDL_GPUTextureFormat swapchain_fmt,
|
||||||
|
int w, int h) {
|
||||||
|
dir_ = dir;
|
||||||
|
swapchain_fmt_ = swapchain_fmt;
|
||||||
|
|
||||||
|
// Parse ini
|
||||||
|
if (!parseIni(dir + "/preset.ini"))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
int n = static_cast<int>(descs_.size());
|
||||||
|
passes_.resize(n);
|
||||||
|
|
||||||
|
// Intermediate render target format (signed float to handle NTSC signal range)
|
||||||
|
SDL_GPUTextureFormat inter_fmt = SDL_GPU_TEXTUREFORMAT_R16G16B16A16_FLOAT;
|
||||||
|
|
||||||
|
for (int i = 0; i < n; ++i) {
|
||||||
|
bool is_last = (i == n - 1);
|
||||||
|
SDL_GPUTextureFormat target_fmt = is_last ? swapchain_fmt : inter_fmt;
|
||||||
|
|
||||||
|
std::string vert_spv = dir + "/" + descs_[i].vert_name + ".spv";
|
||||||
|
std::string frag_spv = dir + "/" + descs_[i].frag_name + ".spv";
|
||||||
|
|
||||||
|
passes_[i].pipeline = buildPassPipeline(device, vert_spv, frag_spv, target_fmt);
|
||||||
|
if (!passes_[i].pipeline) {
|
||||||
|
SDL_Log("GpuShaderPreset: failed to build pipeline for pass %d", i);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_last) {
|
||||||
|
// Create intermediate render target
|
||||||
|
auto tex = std::make_unique<GpuTexture>();
|
||||||
|
if (!tex->createRenderTarget(device, w, h, inter_fmt)) {
|
||||||
|
SDL_Log("GpuShaderPreset: failed to create intermediate target for pass %d", i);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
passes_[i].target = tex.get();
|
||||||
|
targets_.push_back(std::move(tex));
|
||||||
|
}
|
||||||
|
// Last pass: target = null (caller binds swapchain)
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Log("GpuShaderPreset: loaded '%s' (%d passes)", name_.c_str(), n);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GpuShaderPreset::recreateTargets(SDL_GPUDevice* device, int w, int h) {
|
||||||
|
SDL_GPUTextureFormat inter_fmt = SDL_GPU_TEXTUREFORMAT_R16G16B16A16_FLOAT;
|
||||||
|
for (auto& tex : targets_) {
|
||||||
|
tex->destroy(device);
|
||||||
|
tex->createRenderTarget(device, w, h, inter_fmt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GpuShaderPreset::destroy(SDL_GPUDevice* device) {
|
||||||
|
for (auto& pass : passes_) {
|
||||||
|
if (pass.pipeline) {
|
||||||
|
SDL_ReleaseGPUGraphicsPipeline(device, pass.pipeline);
|
||||||
|
pass.pipeline = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (auto& tex : targets_) {
|
||||||
|
if (tex) tex->destroy(device);
|
||||||
|
}
|
||||||
|
targets_.clear();
|
||||||
|
passes_.clear();
|
||||||
|
descs_.clear();
|
||||||
|
params_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
float GpuShaderPreset::param(const std::string& key, float default_val) const {
|
||||||
|
auto it = params_.find(key);
|
||||||
|
return (it != params_.end()) ? it->second : default_val;
|
||||||
|
}
|
||||||
94
source/gpu/gpu_shader_preset.hpp
Normal file
94
source/gpu/gpu_shader_preset.hpp
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL_gpu.h>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "gpu_texture.hpp"
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// NTSCParams — uniform buffer for NTSC shader passes (set=3, binding=0)
|
||||||
|
// Matches the layout in pass0_encode.frag and pass1_decode.frag.
|
||||||
|
// Pushed via SDL_PushGPUFragmentUniformData(cmd, 0, &ntsc, sizeof(NTSCParams)).
|
||||||
|
// ============================================================================
|
||||||
|
struct 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 _pad[2];
|
||||||
|
};
|
||||||
|
static_assert(sizeof(NTSCParams) == 48, "NTSCParams must be 48 bytes");
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// ShaderPass — one render pass in a multi-pass shader preset
|
||||||
|
// ============================================================================
|
||||||
|
struct ShaderPass {
|
||||||
|
SDL_GPUGraphicsPipeline* pipeline = nullptr;
|
||||||
|
GpuTexture* target = nullptr; // null = swapchain (last pass)
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// GpuShaderPreset — loads and owns a multi-pass shader preset from disk.
|
||||||
|
//
|
||||||
|
// Directory layout:
|
||||||
|
// <dir>/preset.ini — descriptor
|
||||||
|
// <dir>/pass0_xxx.vert — GLSL 4.50 vertex shader source
|
||||||
|
// <dir>/pass0_xxx.frag — GLSL 4.50 fragment shader source
|
||||||
|
// <dir>/pass0_xxx.vert.spv — compiled SPIRV (by CMake/glslc at build time)
|
||||||
|
// <dir>/pass0_xxx.frag.spv — compiled SPIRV
|
||||||
|
// ...
|
||||||
|
// ============================================================================
|
||||||
|
class GpuShaderPreset {
|
||||||
|
public:
|
||||||
|
// Load preset from directory. swapchain_fmt is the target format for the
|
||||||
|
// last pass; intermediate passes use R16G16B16A16_FLOAT.
|
||||||
|
bool load(SDL_GPUDevice* device,
|
||||||
|
const std::string& dir,
|
||||||
|
SDL_GPUTextureFormat swapchain_fmt,
|
||||||
|
int w, int h);
|
||||||
|
|
||||||
|
void destroy(SDL_GPUDevice* device);
|
||||||
|
|
||||||
|
// Recreate intermediate render targets on resolution change.
|
||||||
|
void recreateTargets(SDL_GPUDevice* device, int w, int h);
|
||||||
|
|
||||||
|
int passCount() const { return static_cast<int>(passes_.size()); }
|
||||||
|
ShaderPass& pass(int i) { return passes_[i]; }
|
||||||
|
|
||||||
|
const std::string& name() const { return name_; }
|
||||||
|
|
||||||
|
// Read a float parameter parsed from preset.ini (returns default_val if absent).
|
||||||
|
float param(const std::string& key, float default_val) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<ShaderPass> passes_;
|
||||||
|
std::vector<std::unique_ptr<GpuTexture>> targets_; // intermediate render targets
|
||||||
|
std::string name_;
|
||||||
|
std::string dir_;
|
||||||
|
std::unordered_map<std::string, float> params_;
|
||||||
|
SDL_GPUTextureFormat swapchain_fmt_ = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;
|
||||||
|
|
||||||
|
// Entries read from preset.ini for each pass
|
||||||
|
struct PassDesc {
|
||||||
|
std::string vert_name; // e.g. "pass0_encode.vert"
|
||||||
|
std::string frag_name; // e.g. "pass0_encode.frag"
|
||||||
|
};
|
||||||
|
std::vector<PassDesc> descs_;
|
||||||
|
|
||||||
|
bool parseIni(const std::string& ini_path);
|
||||||
|
|
||||||
|
// Build a full-screen-triangle pipeline from two on-disk SPV files.
|
||||||
|
SDL_GPUGraphicsPipeline* buildPassPipeline(SDL_GPUDevice* device,
|
||||||
|
const std::string& vert_spv_path,
|
||||||
|
const std::string& frag_spv_path,
|
||||||
|
SDL_GPUTextureFormat target_fmt);
|
||||||
|
};
|
||||||
68
source/gpu/shader_manager.cpp
Normal file
68
source/gpu/shader_manager.cpp
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
#include "shader_manager.hpp"
|
||||||
|
|
||||||
|
#include <SDL3/SDL_log.h>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
void ShaderManager::scan(const std::string& root_dir) {
|
||||||
|
root_dir_ = root_dir;
|
||||||
|
names_.clear();
|
||||||
|
dirs_.clear();
|
||||||
|
|
||||||
|
std::error_code ec;
|
||||||
|
for (const auto& entry : fs::directory_iterator(root_dir, ec)) {
|
||||||
|
if (!entry.is_directory()) continue;
|
||||||
|
fs::path ini = entry.path() / "preset.ini";
|
||||||
|
if (!fs::exists(ini)) continue;
|
||||||
|
|
||||||
|
std::string preset_name = entry.path().filename().string();
|
||||||
|
names_.push_back(preset_name);
|
||||||
|
dirs_[preset_name] = entry.path().string();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ec) {
|
||||||
|
SDL_Log("ShaderManager: scan error on %s: %s", root_dir.c_str(), ec.message().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::sort(names_.begin(), names_.end());
|
||||||
|
SDL_Log("ShaderManager: found %d preset(s) in %s", (int)names_.size(), root_dir.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
GpuShaderPreset* ShaderManager::load(SDL_GPUDevice* device,
|
||||||
|
const std::string& name,
|
||||||
|
SDL_GPUTextureFormat swapchain_fmt,
|
||||||
|
int w, int h) {
|
||||||
|
auto it = loaded_.find(name);
|
||||||
|
if (it != loaded_.end()) return it->second.get();
|
||||||
|
|
||||||
|
auto dir_it = dirs_.find(name);
|
||||||
|
if (dir_it == dirs_.end()) {
|
||||||
|
SDL_Log("ShaderManager: preset '%s' not found", name.c_str());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto preset = std::make_unique<GpuShaderPreset>();
|
||||||
|
if (!preset->load(device, dir_it->second, swapchain_fmt, w, h)) {
|
||||||
|
SDL_Log("ShaderManager: failed to load preset '%s'", name.c_str());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
GpuShaderPreset* raw = preset.get();
|
||||||
|
loaded_[name] = std::move(preset);
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShaderManager::onResize(SDL_GPUDevice* device, SDL_GPUTextureFormat /*swapchain_fmt*/, int w, int h) {
|
||||||
|
for (auto& [name, preset] : loaded_) {
|
||||||
|
preset->recreateTargets(device, w, h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShaderManager::destroyAll(SDL_GPUDevice* device) {
|
||||||
|
for (auto& [name, preset] : loaded_) {
|
||||||
|
preset->destroy(device);
|
||||||
|
}
|
||||||
|
loaded_.clear();
|
||||||
|
}
|
||||||
41
source/gpu/shader_manager.hpp
Normal file
41
source/gpu/shader_manager.hpp
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL_gpu.h>
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "gpu_shader_preset.hpp"
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// ShaderManager — discovers and manages runtime shader presets under
|
||||||
|
// a root directory (e.g., data/shaders/).
|
||||||
|
//
|
||||||
|
// Each subdirectory with a preset.ini is treated as a shader preset.
|
||||||
|
// ============================================================================
|
||||||
|
class ShaderManager {
|
||||||
|
public:
|
||||||
|
// Scan root_dir for preset subdirectories (each must contain preset.ini).
|
||||||
|
void scan(const std::string& root_dir);
|
||||||
|
|
||||||
|
// Available preset names (e.g. {"ntsc-md-rainbows"}).
|
||||||
|
const std::vector<std::string>& names() const { return names_; }
|
||||||
|
|
||||||
|
// Load and return a preset (cached). Returns null on failure.
|
||||||
|
GpuShaderPreset* load(SDL_GPUDevice* device,
|
||||||
|
const std::string& name,
|
||||||
|
SDL_GPUTextureFormat swapchain_fmt,
|
||||||
|
int w, int h);
|
||||||
|
|
||||||
|
// Recreate intermediate render targets on resolution change.
|
||||||
|
void onResize(SDL_GPUDevice* device, SDL_GPUTextureFormat swapchain_fmt, int w, int h);
|
||||||
|
|
||||||
|
void destroyAll(SDL_GPUDevice* device);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string root_dir_;
|
||||||
|
std::vector<std::string> names_;
|
||||||
|
std::map<std::string, std::string> dirs_;
|
||||||
|
std::map<std::string, std::unique_ptr<GpuShaderPreset>> loaded_;
|
||||||
|
};
|
||||||
@@ -24,6 +24,7 @@ void printHelp() {
|
|||||||
std::cout << " --postfx [efecto] Arrancar con PostFX activo (default: complet): vinyeta, scanlines, cromatica, complet\n";
|
std::cout << " --postfx [efecto] Arrancar con PostFX activo (default: complet): vinyeta, scanlines, cromatica, complet\n";
|
||||||
std::cout << " --vignette <float> Sobreescribir vignette_strength (activa PostFX si no hay --postfx)\n";
|
std::cout << " --vignette <float> Sobreescribir vignette_strength (activa PostFX si no hay --postfx)\n";
|
||||||
std::cout << " --chroma <float> Sobreescribir chroma_strength (activa PostFX si no hay --postfx)\n";
|
std::cout << " --chroma <float> Sobreescribir chroma_strength (activa PostFX si no hay --postfx)\n";
|
||||||
|
std::cout << " --shader <nombre> Arrancar con shader externo (ej: ntsc-md-rainbows)\n";
|
||||||
std::cout << " --help Mostrar esta ayuda\n\n";
|
std::cout << " --help Mostrar esta ayuda\n\n";
|
||||||
std::cout << "Ejemplos:\n";
|
std::cout << "Ejemplos:\n";
|
||||||
std::cout << " vibe3_physics # 320x240 zoom 3 (ventana 960x720)\n";
|
std::cout << " vibe3_physics # 320x240 zoom 3 (ventana 960x720)\n";
|
||||||
@@ -51,6 +52,7 @@ int main(int argc, char* argv[]) {
|
|||||||
int initial_postfx = -1;
|
int initial_postfx = -1;
|
||||||
float override_vignette = -1.f;
|
float override_vignette = -1.f;
|
||||||
float override_chroma = -1.f;
|
float override_chroma = -1.f;
|
||||||
|
std::string initial_shader;
|
||||||
AppMode initial_mode = AppMode::SANDBOX; // Modo inicial (default: SANDBOX)
|
AppMode initial_mode = AppMode::SANDBOX; // Modo inicial (default: SANDBOX)
|
||||||
|
|
||||||
// Parsear argumentos
|
// Parsear argumentos
|
||||||
@@ -175,6 +177,13 @@ int main(int argc, char* argv[]) {
|
|||||||
std::cerr << "Error: --max-balls requiere un valor\n";
|
std::cerr << "Error: --max-balls requiere un valor\n";
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
} else if (strcmp(argv[i], "--shader") == 0) {
|
||||||
|
if (i + 1 < argc) {
|
||||||
|
initial_shader = argv[++i];
|
||||||
|
} else {
|
||||||
|
std::cerr << "Error: --shader requiere un nombre de preset\n";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
std::cerr << "Error: Opción desconocida '" << argv[i] << "'\n";
|
std::cerr << "Error: Opción desconocida '" << argv[i] << "'\n";
|
||||||
printHelp();
|
printHelp();
|
||||||
@@ -206,6 +215,9 @@ int main(int argc, char* argv[]) {
|
|||||||
engine.setPostFXParamOverrides(override_vignette, override_chroma);
|
engine.setPostFXParamOverrides(override_vignette, override_chroma);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!initial_shader.empty())
|
||||||
|
engine.setInitialShader(initial_shader);
|
||||||
|
|
||||||
if (!engine.initialize(width, height, zoom, fullscreen, initial_mode)) {
|
if (!engine.initialize(width, height, zoom, fullscreen, initial_mode)) {
|
||||||
std::cout << "¡Error al inicializar el engine!" << std::endl;
|
std::cout << "¡Error al inicializar el engine!" << std::endl;
|
||||||
return -1;
|
return -1;
|
||||||
|
|||||||
@@ -336,7 +336,9 @@ void UIManager::renderDebugHUD(const Engine* engine,
|
|||||||
lines.push_back(refresh_text);
|
lines.push_back(refresh_text);
|
||||||
lines.push_back(theme_text);
|
lines.push_back(theme_text);
|
||||||
std::string postfx_text;
|
std::string postfx_text;
|
||||||
if (!engine->isPostFXEnabled()) {
|
if (engine->isExternalShaderActive()) {
|
||||||
|
postfx_text = "Shader: " + engine->getActiveShaderName();
|
||||||
|
} else if (!engine->isPostFXEnabled()) {
|
||||||
postfx_text = "PostFX: OFF";
|
postfx_text = "PostFX: OFF";
|
||||||
} else {
|
} else {
|
||||||
static constexpr const char* preset_names[4] = {
|
static constexpr const char* preset_names[4] = {
|
||||||
|
|||||||
Reference in New Issue
Block a user