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

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

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

View File

@@ -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<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;
@@ -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<float>(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<int>(vp_x), static_cast<int>(vp_y),
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)
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<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() {
// 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_);
}