diff --git a/CMakeLists.txt b/CMakeLists.txt index f0558ac..b2c7e72 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -64,6 +64,34 @@ if(NOT APPLE) endforeach() add_custom_target(shaders ALL DEPENDS ${SPIRV_HEADERS}) + + # External runtime shaders: auto-discover and compile data/shaders/**/*.vert/*.frag + # Output: .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() # Archivos fuente (excluir main_old.cpp) @@ -105,6 +133,9 @@ target_link_libraries(${PROJECT_NAME} ${LINK_LIBS}) if(NOT APPLE) add_dependencies(${PROJECT_NAME} shaders) target_include_directories(${PROJECT_NAME} PRIVATE "${SHADER_GEN_DIR}") + if(TARGET data_shaders) + add_dependencies(${PROJECT_NAME} data_shaders) + endif() endif() # Tool: pack_resources diff --git a/data/shaders/ntsc-md-rainbows.slangp b/data/shaders/ntsc-md-rainbows.slangp new file mode 100644 index 0000000..d644023 --- /dev/null +++ b/data/shaders/ntsc-md-rainbows.slangp @@ -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" diff --git a/data/shaders/ntsc-md-rainbows/pass0_encode.frag b/data/shaders/ntsc-md-rainbows/pass0_encode.frag new file mode 100644 index 0000000..1374e86 --- /dev/null +++ b/data/shaders/ntsc-md-rainbows/pass0_encode.frag @@ -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); +} diff --git a/data/shaders/ntsc-md-rainbows/pass0_encode.frag.spv b/data/shaders/ntsc-md-rainbows/pass0_encode.frag.spv new file mode 100644 index 0000000..7cfa552 Binary files /dev/null and b/data/shaders/ntsc-md-rainbows/pass0_encode.frag.spv differ diff --git a/data/shaders/ntsc-md-rainbows/pass0_encode.vert b/data/shaders/ntsc-md-rainbows/pass0_encode.vert new file mode 100644 index 0000000..006d695 --- /dev/null +++ b/data/shaders/ntsc-md-rainbows/pass0_encode.vert @@ -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]; +} diff --git a/data/shaders/ntsc-md-rainbows/pass0_encode.vert.spv b/data/shaders/ntsc-md-rainbows/pass0_encode.vert.spv new file mode 100644 index 0000000..4cd25bd Binary files /dev/null and b/data/shaders/ntsc-md-rainbows/pass0_encode.vert.spv differ diff --git a/data/shaders/ntsc-md-rainbows/pass1_decode.frag b/data/shaders/ntsc-md-rainbows/pass1_decode.frag new file mode 100644 index 0000000..444237a --- /dev/null +++ b/data/shaders/ntsc-md-rainbows/pass1_decode.frag @@ -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); +} diff --git a/data/shaders/ntsc-md-rainbows/pass1_decode.frag.spv b/data/shaders/ntsc-md-rainbows/pass1_decode.frag.spv new file mode 100644 index 0000000..5fc1317 Binary files /dev/null and b/data/shaders/ntsc-md-rainbows/pass1_decode.frag.spv differ diff --git a/data/shaders/ntsc-md-rainbows/pass1_decode.vert b/data/shaders/ntsc-md-rainbows/pass1_decode.vert new file mode 100644 index 0000000..006d695 --- /dev/null +++ b/data/shaders/ntsc-md-rainbows/pass1_decode.vert @@ -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]; +} diff --git a/data/shaders/ntsc-md-rainbows/pass1_decode.vert.spv b/data/shaders/ntsc-md-rainbows/pass1_decode.vert.spv new file mode 100644 index 0000000..4cd25bd Binary files /dev/null and b/data/shaders/ntsc-md-rainbows/pass1_decode.vert.spv differ diff --git a/data/shaders/ntsc-md-rainbows/preset.ini b/data/shaders/ntsc-md-rainbows/preset.ini new file mode 100644 index 0000000..6a9ac83 --- /dev/null +++ b/data/shaders/ntsc-md-rainbows/preset.ini @@ -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 diff --git a/source/engine.cpp b/source/engine.cpp index ccd31ec..e4da1c4 100644 --- a/source/engine.cpp +++ b/source/engine.cpp @@ -353,6 +353,27 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMod ResourceManager::loadResource("shapes/jailgames.png", tmp, tmp_size); delete[] tmp; } + + // Inicializar ShaderManager (shaders externos desde data/shaders/) + if (gpu_ctx_ && success) { + shader_manager_ = std::make_unique(); + 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; @@ -387,6 +408,7 @@ void Engine::shutdown() { 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_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 @@ -845,20 +867,14 @@ void Engine::render() { 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; SDL_GPUTexture* swapchain = gpu_ctx_->acquireSwapchainTexture(cmd, &sw_w, &sw_h); 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); - - // Viewport/scissor per integer scaling (only F3 fullscreen) - if (fullscreen_enabled_) { + // Helper lambda for viewport/scissor (used in the final pass) + auto applyViewport = [&](SDL_GPURenderPass* rp) { + if (!fullscreen_enabled_) return; float vp_x, vp_y, vp_w, vp_h; if (current_scaling_mode_ == ScalingMode::STRETCH) { vp_x = 0.0f; vp_y = 0.0f; @@ -881,11 +897,90 @@ void Engine::render() { vp_y = (static_cast(sw_h) - vp_h) * 0.5f; } 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(vp_x), static_cast(vp_y), static_cast(vp_w), static_cast(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(current_screen_width_); + ntsc.source_height = static_cast(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) SDL_BindGPUGraphicsPipeline(pass2, gpu_pipeline_->postfxPipeline()); @@ -908,7 +1003,8 @@ void Engine::render() { } SDL_EndGPURenderPass(pass2); - } + } // end else (native PostFX) + } // end if (swapchain && ...) gpu_ctx_->submit(cmd); } @@ -1059,14 +1155,8 @@ void Engine::applyPostFXPreset(int mode) { } void Engine::handlePostFXCycle() { - static constexpr const char* names[4] = { - "PostFX: Vinyeta", "PostFX: Scanlines", - "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_]); + // Delegate to cycleShader() which handles both native PostFX and external shaders + cycleShader(); } void Engine::handlePostFXToggle() { @@ -1074,6 +1164,16 @@ void Engine::handlePostFXToggle() { "PostFX: Vinyeta", "PostFX: Scanlines", "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_; if (postfx_enabled_) { applyPostFXPreset(postfx_effect_mode_); @@ -1101,6 +1201,83 @@ void Engine::setPostFXParamOverrides(float vignette, float 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(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() { // Ciclar entre los 3 modos: INTEGER → LETTERBOX → STRETCH → INTEGER switch (current_scaling_mode_) { @@ -1443,6 +1620,12 @@ void Engine::recreateOffscreenTexture() { current_screen_width_, current_screen_height_, // physical 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_) { app_logo_->initialize(ui_renderer_, current_screen_width_, current_screen_height_); } diff --git a/source/engine.hpp b/source/engine.hpp index 2242a5d..035ec9e 100644 --- a/source/engine.hpp +++ b/source/engine.hpp @@ -19,8 +19,10 @@ #include "gpu/gpu_ball_buffer.hpp" // for GpuBallBuffer, BallGPUData #include "gpu/gpu_context.hpp" // for GpuContext #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_texture.hpp" // for GpuTexture +#include "gpu/shader_manager.hpp" // for ShaderManager #include "input/input_handler.hpp" // for InputHandler #include "scene/scene_manager.hpp" // for SceneManager #include "shapes_mgr/shape_manager.hpp" // for ShapeManager @@ -82,6 +84,11 @@ class Engine { void setInitialPostFX(int mode); 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 void setKioskMode(bool enabled) { kiosk_mode_ = enabled; } bool isKioskMode() const { return kiosk_mode_; } @@ -130,6 +137,7 @@ class Engine { float getPostFXVignette() const { return postfx_uniforms_.vignette_strength; } float getPostFXChroma() const { return postfx_uniforms_.chroma_strength; } float getPostFXScanline() const { return postfx_uniforms_.scanline_strength; } + bool isExternalShaderActive() const { return active_shader_ != nullptr; } private: // === Componentes del sistema (Composición) === @@ -184,6 +192,12 @@ class Engine { float postfx_override_vignette_ = -1.f; // -1 = sin override float postfx_override_chroma_ = -1.f; + // External shader system + std::unique_ptr 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 int current_window_zoom_ = DEFAULT_WINDOW_ZOOM; diff --git a/source/gpu/gpu_shader_preset.cpp b/source/gpu/gpu_shader_preset.cpp new file mode 100644 index 0000000..fe3c0e3 --- /dev/null +++ b/source/gpu/gpu_shader_preset.cpp @@ -0,0 +1,257 @@ +#include "gpu_shader_preset.hpp" +#include "gpu_texture.hpp" + +#include +#include +#include +#include +#include + +// ============================================================================ +// Helpers +// ============================================================================ + +static std::vector 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 buf(static_cast(sz)); + if (!f.read(reinterpret_cast(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(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(); + 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; +} diff --git a/source/gpu/gpu_shader_preset.hpp b/source/gpu/gpu_shader_preset.hpp new file mode 100644 index 0000000..a41595b --- /dev/null +++ b/source/gpu/gpu_shader_preset.hpp @@ -0,0 +1,94 @@ +#pragma once + +#include +#include +#include +#include +#include + +#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: +// /preset.ini — descriptor +// /pass0_xxx.vert — GLSL 4.50 vertex shader source +// /pass0_xxx.frag — GLSL 4.50 fragment shader source +// /pass0_xxx.vert.spv — compiled SPIRV (by CMake/glslc at build time) +// /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(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 passes_; + std::vector> targets_; // intermediate render targets + std::string name_; + std::string dir_; + std::unordered_map 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 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); +}; diff --git a/source/gpu/shader_manager.cpp b/source/gpu/shader_manager.cpp new file mode 100644 index 0000000..dc4a5ff --- /dev/null +++ b/source/gpu/shader_manager.cpp @@ -0,0 +1,68 @@ +#include "shader_manager.hpp" + +#include +#include +#include + +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(); + 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(); +} diff --git a/source/gpu/shader_manager.hpp b/source/gpu/shader_manager.hpp new file mode 100644 index 0000000..b0ae631 --- /dev/null +++ b/source/gpu/shader_manager.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include +#include +#include + +#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& 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 names_; + std::map dirs_; + std::map> loaded_; +}; diff --git a/source/main.cpp b/source/main.cpp index af4c125..07b0804 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -24,6 +24,7 @@ void printHelp() { std::cout << " --postfx [efecto] Arrancar con PostFX activo (default: complet): vinyeta, scanlines, cromatica, complet\n"; std::cout << " --vignette Sobreescribir vignette_strength (activa PostFX si no hay --postfx)\n"; std::cout << " --chroma Sobreescribir chroma_strength (activa PostFX si no hay --postfx)\n"; + std::cout << " --shader Arrancar con shader externo (ej: ntsc-md-rainbows)\n"; std::cout << " --help Mostrar esta ayuda\n\n"; std::cout << "Ejemplos:\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; float override_vignette = -1.f; float override_chroma = -1.f; + std::string initial_shader; AppMode initial_mode = AppMode::SANDBOX; // Modo inicial (default: SANDBOX) // Parsear argumentos @@ -175,6 +177,13 @@ int main(int argc, char* argv[]) { std::cerr << "Error: --max-balls requiere un valor\n"; 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 { std::cerr << "Error: Opción desconocida '" << argv[i] << "'\n"; printHelp(); @@ -206,6 +215,9 @@ int main(int argc, char* argv[]) { engine.setPostFXParamOverrides(override_vignette, override_chroma); } + if (!initial_shader.empty()) + engine.setInitialShader(initial_shader); + if (!engine.initialize(width, height, zoom, fullscreen, initial_mode)) { std::cout << "¡Error al inicializar el engine!" << std::endl; return -1; diff --git a/source/ui/ui_manager.cpp b/source/ui/ui_manager.cpp index de5ba13..f4873a3 100644 --- a/source/ui/ui_manager.cpp +++ b/source/ui/ui_manager.cpp @@ -336,7 +336,9 @@ void UIManager::renderDebugHUD(const Engine* engine, lines.push_back(refresh_text); lines.push_back(theme_text); std::string postfx_text; - if (!engine->isPostFXEnabled()) { + if (engine->isExternalShaderActive()) { + postfx_text = "Shader: " + engine->getActiveShaderName(); + } else if (!engine->isPostFXEnabled()) { postfx_text = "PostFX: OFF"; } else { static constexpr const char* preset_names[4] = {