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:
@@ -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_);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user