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

@@ -0,0 +1,257 @@
#include "gpu_shader_preset.hpp"
#include "gpu_texture.hpp"
#include <SDL3/SDL_log.h>
#include <fstream>
#include <sstream>
#include <algorithm>
#include <memory>
// ============================================================================
// Helpers
// ============================================================================
static std::vector<uint8_t> readFile(const std::string& path) {
std::ifstream f(path, std::ios::binary | std::ios::ate);
if (!f) return {};
std::streamsize sz = f.tellg();
f.seekg(0, std::ios::beg);
std::vector<uint8_t> buf(static_cast<size_t>(sz));
if (!f.read(reinterpret_cast<char*>(buf.data()), sz)) return {};
return buf;
}
static std::string trim(const std::string& s) {
size_t a = s.find_first_not_of(" \t\r\n");
if (a == std::string::npos) return {};
size_t b = s.find_last_not_of(" \t\r\n");
return s.substr(a, b - a + 1);
}
// ============================================================================
// GpuShaderPreset
// ============================================================================
bool GpuShaderPreset::parseIni(const std::string& ini_path) {
std::ifstream f(ini_path);
if (!f) {
SDL_Log("GpuShaderPreset: cannot open %s", ini_path.c_str());
return false;
}
int num_passes = 0;
std::string line;
while (std::getline(f, line)) {
// Strip comments
auto comment = line.find(';');
if (comment != std::string::npos) line = line.substr(0, comment);
line = trim(line);
if (line.empty()) continue;
auto eq = line.find('=');
if (eq == std::string::npos) continue;
std::string key = trim(line.substr(0, eq));
std::string value = trim(line.substr(eq + 1));
if (key.empty() || value.empty()) continue;
if (key == "name") {
name_ = value;
} else if (key == "passes") {
num_passes = std::stoi(value);
} else {
// Try to parse as float parameter
try {
params_[key] = std::stof(value);
} catch (...) {
// Non-float values stored separately (pass0_vert etc.)
}
}
}
if (num_passes <= 0) {
SDL_Log("GpuShaderPreset: no passes defined in %s", ini_path.c_str());
return false;
}
// Second pass: read per-pass file names
f.clear();
f.seekg(0, std::ios::beg);
descs_.resize(num_passes);
while (std::getline(f, line)) {
auto comment = line.find(';');
if (comment != std::string::npos) line = line.substr(0, comment);
line = trim(line);
if (line.empty()) continue;
auto eq = line.find('=');
if (eq == std::string::npos) continue;
std::string key = trim(line.substr(0, eq));
std::string value = trim(line.substr(eq + 1));
for (int i = 0; i < num_passes; ++i) {
std::string vi = "pass" + std::to_string(i) + "_vert";
std::string fi = "pass" + std::to_string(i) + "_frag";
if (key == vi) descs_[i].vert_name = value;
if (key == fi) descs_[i].frag_name = value;
}
}
// Validate
for (int i = 0; i < num_passes; ++i) {
if (descs_[i].vert_name.empty() || descs_[i].frag_name.empty()) {
SDL_Log("GpuShaderPreset: pass %d missing vert or frag in %s", i, ini_path.c_str());
return false;
}
}
return true;
}
SDL_GPUGraphicsPipeline* GpuShaderPreset::buildPassPipeline(SDL_GPUDevice* device,
const std::string& vert_spv_path,
const std::string& frag_spv_path,
SDL_GPUTextureFormat target_fmt) {
auto vert_spv = readFile(vert_spv_path);
auto frag_spv = readFile(frag_spv_path);
if (vert_spv.empty()) {
SDL_Log("GpuShaderPreset: cannot read %s", vert_spv_path.c_str());
return nullptr;
}
if (frag_spv.empty()) {
SDL_Log("GpuShaderPreset: cannot read %s", frag_spv_path.c_str());
return nullptr;
}
SDL_GPUShaderCreateInfo vert_info = {};
vert_info.code = vert_spv.data();
vert_info.code_size = vert_spv.size();
vert_info.entrypoint = "main";
vert_info.format = SDL_GPU_SHADERFORMAT_SPIRV;
vert_info.stage = SDL_GPU_SHADERSTAGE_VERTEX;
vert_info.num_samplers = 0;
vert_info.num_uniform_buffers = 0;
SDL_GPUShaderCreateInfo frag_info = {};
frag_info.code = frag_spv.data();
frag_info.code_size = frag_spv.size();
frag_info.entrypoint = "main";
frag_info.format = SDL_GPU_SHADERFORMAT_SPIRV;
frag_info.stage = SDL_GPU_SHADERSTAGE_FRAGMENT;
frag_info.num_samplers = 1;
frag_info.num_uniform_buffers = 1;
SDL_GPUShader* vert_shader = SDL_CreateGPUShader(device, &vert_info);
SDL_GPUShader* frag_shader = SDL_CreateGPUShader(device, &frag_info);
if (!vert_shader || !frag_shader) {
SDL_Log("GpuShaderPreset: shader creation failed for %s / %s: %s",
vert_spv_path.c_str(), frag_spv_path.c_str(), SDL_GetError());
if (vert_shader) SDL_ReleaseGPUShader(device, vert_shader);
if (frag_shader) SDL_ReleaseGPUShader(device, frag_shader);
return nullptr;
}
// Full-screen triangle: no vertex input, no blend
SDL_GPUColorTargetBlendState no_blend = {};
no_blend.enable_blend = false;
no_blend.enable_color_write_mask = false;
SDL_GPUColorTargetDescription ct_desc = {};
ct_desc.format = target_fmt;
ct_desc.blend_state = no_blend;
SDL_GPUVertexInputState no_input = {};
SDL_GPUGraphicsPipelineCreateInfo pipe_info = {};
pipe_info.vertex_shader = vert_shader;
pipe_info.fragment_shader = frag_shader;
pipe_info.vertex_input_state = no_input;
pipe_info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
pipe_info.target_info.num_color_targets = 1;
pipe_info.target_info.color_target_descriptions = &ct_desc;
SDL_GPUGraphicsPipeline* pipeline = SDL_CreateGPUGraphicsPipeline(device, &pipe_info);
SDL_ReleaseGPUShader(device, vert_shader);
SDL_ReleaseGPUShader(device, frag_shader);
if (!pipeline)
SDL_Log("GpuShaderPreset: pipeline creation failed: %s", SDL_GetError());
return pipeline;
}
bool GpuShaderPreset::load(SDL_GPUDevice* device,
const std::string& dir,
SDL_GPUTextureFormat swapchain_fmt,
int w, int h) {
dir_ = dir;
swapchain_fmt_ = swapchain_fmt;
// Parse ini
if (!parseIni(dir + "/preset.ini"))
return false;
int n = static_cast<int>(descs_.size());
passes_.resize(n);
// Intermediate render target format (signed float to handle NTSC signal range)
SDL_GPUTextureFormat inter_fmt = SDL_GPU_TEXTUREFORMAT_R16G16B16A16_FLOAT;
for (int i = 0; i < n; ++i) {
bool is_last = (i == n - 1);
SDL_GPUTextureFormat target_fmt = is_last ? swapchain_fmt : inter_fmt;
std::string vert_spv = dir + "/" + descs_[i].vert_name + ".spv";
std::string frag_spv = dir + "/" + descs_[i].frag_name + ".spv";
passes_[i].pipeline = buildPassPipeline(device, vert_spv, frag_spv, target_fmt);
if (!passes_[i].pipeline) {
SDL_Log("GpuShaderPreset: failed to build pipeline for pass %d", i);
return false;
}
if (!is_last) {
// Create intermediate render target
auto tex = std::make_unique<GpuTexture>();
if (!tex->createRenderTarget(device, w, h, inter_fmt)) {
SDL_Log("GpuShaderPreset: failed to create intermediate target for pass %d", i);
return false;
}
passes_[i].target = tex.get();
targets_.push_back(std::move(tex));
}
// Last pass: target = null (caller binds swapchain)
}
SDL_Log("GpuShaderPreset: loaded '%s' (%d passes)", name_.c_str(), n);
return true;
}
void GpuShaderPreset::recreateTargets(SDL_GPUDevice* device, int w, int h) {
SDL_GPUTextureFormat inter_fmt = SDL_GPU_TEXTUREFORMAT_R16G16B16A16_FLOAT;
for (auto& tex : targets_) {
tex->destroy(device);
tex->createRenderTarget(device, w, h, inter_fmt);
}
}
void GpuShaderPreset::destroy(SDL_GPUDevice* device) {
for (auto& pass : passes_) {
if (pass.pipeline) {
SDL_ReleaseGPUGraphicsPipeline(device, pass.pipeline);
pass.pipeline = nullptr;
}
}
for (auto& tex : targets_) {
if (tex) tex->destroy(device);
}
targets_.clear();
passes_.clear();
descs_.clear();
params_.clear();
}
float GpuShaderPreset::param(const std::string& key, float default_val) const {
auto it = params_.find(key);
return (it != params_.end()) ? it->second : default_val;
}