From f272bab296c154fa9265571df6a9bd5b047341a1 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Fri, 20 Mar 2026 13:37:22 +0100 Subject: [PATCH] feat(shaders): sistema de shaders runtime amb presets externs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 per arrancar directament amb un preset extern - CMake auto-descubreix i compila data/shaders/**/*.vert/.frag → .spv - HUD F1 mostra 'Shader: ' quan hi ha shader extern actiu Co-Authored-By: Claude Sonnet 4.6 --- CMakeLists.txt | 31 +++ data/shaders/ntsc-md-rainbows.slangp | 28 ++ .../ntsc-md-rainbows/pass0_encode.frag | 69 +++++ .../ntsc-md-rainbows/pass0_encode.frag.spv | Bin 0 -> 5204 bytes .../ntsc-md-rainbows/pass0_encode.vert | 8 + .../ntsc-md-rainbows/pass0_encode.vert.spv | Bin 0 -> 1436 bytes .../ntsc-md-rainbows/pass1_decode.frag | 148 ++++++++++ .../ntsc-md-rainbows/pass1_decode.frag.spv | Bin 0 -> 16192 bytes .../ntsc-md-rainbows/pass1_decode.vert | 8 + .../ntsc-md-rainbows/pass1_decode.vert.spv | Bin 0 -> 1436 bytes data/shaders/ntsc-md-rainbows/preset.ini | 18 ++ source/engine.cpp | 227 ++++++++++++++-- source/engine.hpp | 14 + source/gpu/gpu_shader_preset.cpp | 257 ++++++++++++++++++ source/gpu/gpu_shader_preset.hpp | 94 +++++++ source/gpu/shader_manager.cpp | 68 +++++ source/gpu/shader_manager.hpp | 41 +++ source/main.cpp | 12 + source/ui/ui_manager.cpp | 4 +- 19 files changed, 1004 insertions(+), 23 deletions(-) create mode 100644 data/shaders/ntsc-md-rainbows.slangp create mode 100644 data/shaders/ntsc-md-rainbows/pass0_encode.frag create mode 100644 data/shaders/ntsc-md-rainbows/pass0_encode.frag.spv create mode 100644 data/shaders/ntsc-md-rainbows/pass0_encode.vert create mode 100644 data/shaders/ntsc-md-rainbows/pass0_encode.vert.spv create mode 100644 data/shaders/ntsc-md-rainbows/pass1_decode.frag create mode 100644 data/shaders/ntsc-md-rainbows/pass1_decode.frag.spv create mode 100644 data/shaders/ntsc-md-rainbows/pass1_decode.vert create mode 100644 data/shaders/ntsc-md-rainbows/pass1_decode.vert.spv create mode 100644 data/shaders/ntsc-md-rainbows/preset.ini create mode 100644 source/gpu/gpu_shader_preset.cpp create mode 100644 source/gpu/gpu_shader_preset.hpp create mode 100644 source/gpu/shader_manager.cpp create mode 100644 source/gpu/shader_manager.hpp 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 0000000000000000000000000000000000000000..7cfa55216f50137e2a83931c9abd30d1919cfc60 GIT binary patch literal 5204 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 XJrkyPc;DA^#qiW~R^Wf?;En7*` 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4cd25bd1445ecb964d38a3c1bb5887ab73695bc2 GIT binary patch literal 1436 zcmYk6>rWF=5XFa<76bti0UwB4@lCC7V`5Z{nAM~S5E8y_NV_pNS(m1}ZPZWvXD0qt zelhX^a;XpAmY4SloL7iEbVi=~@z^a+ReI%?qP#dE}PoL#72dH_GF zFl+lY*0d^fv@`>C#2bp5@_}qq^?Eo3CburkO9*6)-3nh&O#PPdf;=^FfiP=>;n6eq zlFaP%z>3o;>GxL!P~JFU-$5ZppY$&h4f=dAMyw z+>x&#hjE^_49_{fQ(a5`u8clEYBqYj zFGCNHhB+R{;E$x?EFV^!+=SyhKsO^hR2BQ$m7yi)-vH~d_s6n8#u?HV7`^X{f0omI XJrkyPc;DA^#qiW~R^Wf?;En7*`(); + 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] = {