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:
257
source/gpu/gpu_shader_preset.cpp
Normal file
257
source/gpu/gpu_shader_preset.cpp
Normal 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;
|
||||
}
|
||||
94
source/gpu/gpu_shader_preset.hpp
Normal file
94
source/gpu/gpu_shader_preset.hpp
Normal file
@@ -0,0 +1,94 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#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:
|
||||
// <dir>/preset.ini — descriptor
|
||||
// <dir>/pass0_xxx.vert — GLSL 4.50 vertex shader source
|
||||
// <dir>/pass0_xxx.frag — GLSL 4.50 fragment shader source
|
||||
// <dir>/pass0_xxx.vert.spv — compiled SPIRV (by CMake/glslc at build time)
|
||||
// <dir>/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<int>(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<ShaderPass> passes_;
|
||||
std::vector<std::unique_ptr<GpuTexture>> targets_; // intermediate render targets
|
||||
std::string name_;
|
||||
std::string dir_;
|
||||
std::unordered_map<std::string, float> 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<PassDesc> 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);
|
||||
};
|
||||
68
source/gpu/shader_manager.cpp
Normal file
68
source/gpu/shader_manager.cpp
Normal file
@@ -0,0 +1,68 @@
|
||||
#include "shader_manager.hpp"
|
||||
|
||||
#include <SDL3/SDL_log.h>
|
||||
#include <filesystem>
|
||||
#include <algorithm>
|
||||
|
||||
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<GpuShaderPreset>();
|
||||
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();
|
||||
}
|
||||
41
source/gpu/shader_manager.hpp
Normal file
41
source/gpu/shader_manager.hpp
Normal file
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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<std::string>& 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<std::string> names_;
|
||||
std::map<std::string, std::string> dirs_;
|
||||
std::map<std::string, std::unique_ptr<GpuShaderPreset>> loaded_;
|
||||
};
|
||||
Reference in New Issue
Block a user