Files
aee-arcade/source/game/options.cpp
T
JailDesigner db2e5d66e0 PATCH-04: backup <path>.bak abans de reescriure config/presets per fallada
Helper `backupFile(path)` que copia el YAML a `<path>.bak` si existeix.
Es crida a `loadFromFile` (versió mismatch i catch) i a
`loadPresetsFromFile<T>` (manca `presets:`, llista buida, catch).
La primera execució no genera .bak. Cada reescriptura forçada registra el
motiu per stderr (INCONS-02 migrarà a std::cout).

Prova manual: config.yaml amb contingut \"===broken===\" genera
config.yaml.bak amb el contingut previ i un nou config.yaml amb defaults.
2026-04-20 17:40:39 +02:00

657 lines
28 KiB
C++

#include "game/options.hpp"
#include <algorithm>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include "external/fkyaml_node.hpp"
#include "utils/defines.hpp"
namespace {
auto scalingModeToString(Defaults::ScalingMode mode) -> const char* {
switch (mode) {
case Defaults::ScalingMode::DISABLED:
return "disabled";
case Defaults::ScalingMode::STRETCH:
return "stretch";
case Defaults::ScalingMode::LETTERBOX:
return "letterbox";
case Defaults::ScalingMode::OVERSCAN:
return "overscan";
case Defaults::ScalingMode::INTEGER:
return "integer";
}
return "letterbox";
}
auto scalingModeFromString(const std::string& text) -> Defaults::ScalingMode {
if (text == "disabled") { return Defaults::ScalingMode::DISABLED; }
if (text == "stretch") { return Defaults::ScalingMode::STRETCH; }
if (text == "overscan") { return Defaults::ScalingMode::OVERSCAN; }
if (text == "integer") { return Defaults::ScalingMode::INTEGER; }
return Defaults::ScalingMode::LETTERBOX;
}
auto filterToString(Defaults::TextureFilter filter) -> const char* {
return filter == Defaults::TextureFilter::LINEAR ? "linear" : "nearest";
}
auto filterFromString(const std::string& text) -> Defaults::TextureFilter {
return text == "linear" ? Defaults::TextureFilter::LINEAR : Defaults::TextureFilter::NEAREST;
}
auto renderInfoPositionToString(Defaults::RenderInfoPosition pos) -> const char* {
switch (pos) {
case Defaults::RenderInfoPosition::TOP:
return "top";
case Defaults::RenderInfoPosition::BOTTOM:
return "bottom";
case Defaults::RenderInfoPosition::OFF:
return "off";
}
return "off";
}
auto renderInfoPositionFromString(const std::string& text) -> Defaults::RenderInfoPosition {
if (text == "top") { return Defaults::RenderInfoPosition::TOP; }
if (text == "bottom") { return Defaults::RenderInfoPosition::BOTTOM; }
return Defaults::RenderInfoPosition::OFF;
}
auto readFileOrEmpty(const std::string& path) -> std::string {
std::ifstream file(path);
if (!file) { return {}; }
std::stringstream buffer;
buffer << file.rdbuf();
return buffer.str();
}
auto writeFile(const std::string& path, const std::string& content) -> bool {
std::ofstream file(path, std::ios::binary);
if (!file) { return false; }
file.write(content.data(), static_cast<std::streamsize>(content.size()));
return file.good();
}
// Còpia `<path>` a `<path>.bak` per protegir ediciones manuals de
// l'usuari quan estem a punt de reescriure el fitxer amb defaults per
// error de parseig o canvi de versió. Si el fitxer no existeix (primera
// execució) o no es pot obrir, no fa res: no hi ha res a salvar.
void backupFile(const std::string& path) {
if (path.empty()) { return; }
std::ifstream src(path, std::ios::binary);
if (!src) { return; }
std::ofstream dst(path + ".bak", std::ios::binary);
if (!dst) { return; }
dst << src.rdbuf();
std::cerr << "Options: còpia de seguretat a " << path << ".bak\n";
}
void parseWindow(const fkyaml::node& yaml) {
if (!yaml.contains("window")) { return; }
const auto& node = yaml["window"];
if (node.contains("zoom")) { Options::window.zoom = node["zoom"].get_value<int>(); }
if (node.contains("fullscreen")) { Options::window.fullscreen = node["fullscreen"].get_value<bool>(); }
}
void parseGpu(const fkyaml::node& video_node) {
if (!video_node.contains("gpu")) { return; }
const auto& node = video_node["gpu"];
if (node.contains("acceleration")) { Options::video.gpu.acceleration = node["acceleration"].get_value<bool>(); }
if (node.contains("preferred_driver")) { Options::video.gpu.preferred_driver = node["preferred_driver"].get_value<std::string>(); }
}
void parseShader(const fkyaml::node& video_node) {
if (!video_node.contains("shader")) { return; }
const auto& node = video_node["shader"];
if (node.contains("enabled")) { Options::video.shader.enabled = node["enabled"].get_value<bool>(); }
if (node.contains("current_shader")) {
Options::video.shader.current_shader = node["current_shader"].get_value<std::string>();
}
if (node.contains("current_postfx_preset")) {
Options::video.shader.current_postfx_preset = node["current_postfx_preset"].get_value<std::string>();
}
if (node.contains("current_crtpi_preset")) {
Options::video.shader.current_crtpi_preset = node["current_crtpi_preset"].get_value<std::string>();
}
}
void parseVideo(const fkyaml::node& yaml) {
if (!yaml.contains("video")) { return; }
const auto& node = yaml["video"];
if (node.contains("vsync")) { Options::video.vsync = node["vsync"].get_value<bool>(); }
if (node.contains("scaling_mode")) {
Options::video.scaling_mode = scalingModeFromString(node["scaling_mode"].get_value<std::string>());
}
if (node.contains("texture_filter")) {
Options::video.texture_filter = filterFromString(node["texture_filter"].get_value<std::string>());
}
if (node.contains("render_info_position")) {
Options::video.render_info_position = renderInfoPositionFromString(node["render_info_position"].get_value<std::string>());
}
if (node.contains("render_info_show_time")) {
Options::video.render_info_show_time = node["render_info_show_time"].get_value<bool>();
}
parseGpu(node);
parseShader(node);
}
void parseMusic(const fkyaml::node& audio_node) {
if (!audio_node.contains("music")) { return; }
const auto& node = audio_node["music"];
if (node.contains("enabled")) { Options::audio.music.enabled = node["enabled"].get_value<bool>(); }
if (node.contains("volume")) { Options::audio.music.volume = node["volume"].get_value<float>(); }
}
void parseSound(const fkyaml::node& audio_node) {
if (!audio_node.contains("sound")) { return; }
const auto& node = audio_node["sound"];
if (node.contains("enabled")) { Options::audio.sound.enabled = node["enabled"].get_value<bool>(); }
if (node.contains("volume")) { Options::audio.sound.volume = node["volume"].get_value<float>(); }
}
void parseAudio(const fkyaml::node& yaml) {
if (!yaml.contains("audio")) { return; }
const auto& node = yaml["audio"];
if (node.contains("enabled")) { Options::audio.enabled = node["enabled"].get_value<bool>(); }
if (node.contains("volume")) { Options::audio.volume = node["volume"].get_value<float>(); }
parseMusic(node);
parseSound(node);
}
// Presets per defecte de PostFX, portats directament de projecte_2026.
auto defaultPostFXPresets() -> std::vector<Options::PostFXPreset> {
return {
{.name = "CRT",
.vignette = 0.6F,
.scanlines = 0.7F,
.chroma = 0.15F,
.mask = 0.6F,
.gamma = 0.8F,
.curvature = 0.0F,
.bleeding = 0.0F,
.flicker = 0.0F},
{.name = "NTSC",
.vignette = 0.4F,
.scanlines = 0.5F,
.chroma = 0.2F,
.mask = 0.4F,
.gamma = 0.5F,
.curvature = 0.0F,
.bleeding = 0.6F,
.flicker = 0.0F},
{.name = "CURVED",
.vignette = 0.5F,
.scanlines = 0.6F,
.chroma = 0.1F,
.mask = 0.5F,
.gamma = 0.7F,
.curvature = 0.8F,
.bleeding = 0.0F,
.flicker = 0.0F},
{.name = "SCANLINES",
.vignette = 0.0F,
.scanlines = 0.8F,
.chroma = 0.0F,
.mask = 0.0F,
.gamma = 0.0F,
.curvature = 0.0F,
.bleeding = 0.0F,
.flicker = 0.0F},
{.name = "SUBTLE",
.vignette = 0.3F,
.scanlines = 0.4F,
.chroma = 0.05F,
.mask = 0.0F,
.gamma = 0.3F,
.curvature = 0.0F,
.bleeding = 0.0F,
.flicker = 0.0F},
{.name = "CRT LIVE",
.vignette = 0.5F,
.scanlines = 0.6F,
.chroma = 0.3F,
.mask = 0.3F,
.gamma = 0.4F,
.curvature = 0.3F,
.bleeding = 0.4F,
.flicker = 0.8F},
};
}
void parsePostFXPreset(const fkyaml::node& node, Options::PostFXPreset& preset) {
if (node.contains("name")) { preset.name = node["name"].get_value<std::string>(); }
if (node.contains("vignette")) { preset.vignette = node["vignette"].get_value<float>(); }
if (node.contains("scanlines")) { preset.scanlines = node["scanlines"].get_value<float>(); }
if (node.contains("chroma")) { preset.chroma = node["chroma"].get_value<float>(); }
if (node.contains("mask")) { preset.mask = node["mask"].get_value<float>(); }
if (node.contains("gamma")) { preset.gamma = node["gamma"].get_value<float>(); }
if (node.contains("curvature")) { preset.curvature = node["curvature"].get_value<float>(); }
if (node.contains("bleeding")) { preset.bleeding = node["bleeding"].get_value<float>(); }
if (node.contains("flicker")) { preset.flicker = node["flicker"].get_value<float>(); }
}
void writePostFXPreset(std::ostringstream& out, const Options::PostFXPreset& preset) {
out << " - name: \"" << preset.name << "\"\n";
out << " vignette: " << preset.vignette << "\n";
out << " scanlines: " << preset.scanlines << "\n";
out << " chroma: " << preset.chroma << "\n";
out << " mask: " << preset.mask << "\n";
out << " gamma: " << preset.gamma << "\n";
out << " curvature: " << preset.curvature << "\n";
out << " bleeding: " << preset.bleeding << "\n";
out << " flicker: " << preset.flicker << "\n";
}
// Presets per defecte del shader CRT-Pi, portats de projecte_2026.
auto defaultCrtPiPresets() -> std::vector<Options::CrtPiPreset> {
return {
{.name = "DEFAULT",
.scanline_weight = 6.0F,
.scanline_gap_brightness = 0.12F,
.bloom_factor = 3.5F,
.input_gamma = 2.4F,
.output_gamma = 2.2F,
.mask_brightness = 0.80F,
.curvature_x = 0.05F,
.curvature_y = 0.10F,
.mask_type = 2,
.enable_scanlines = true,
.enable_multisample = true,
.enable_gamma = true,
.enable_curvature = false,
.enable_sharper = false},
{.name = "CURVED",
.scanline_weight = 6.0F,
.scanline_gap_brightness = 0.12F,
.bloom_factor = 3.5F,
.input_gamma = 2.4F,
.output_gamma = 2.2F,
.mask_brightness = 0.80F,
.curvature_x = 0.05F,
.curvature_y = 0.10F,
.mask_type = 2,
.enable_scanlines = true,
.enable_multisample = true,
.enable_gamma = true,
.enable_curvature = true,
.enable_sharper = false},
{.name = "SHARP",
.scanline_weight = 6.0F,
.scanline_gap_brightness = 0.12F,
.bloom_factor = 3.5F,
.input_gamma = 2.4F,
.output_gamma = 2.2F,
.mask_brightness = 0.80F,
.curvature_x = 0.05F,
.curvature_y = 0.10F,
.mask_type = 2,
.enable_scanlines = true,
.enable_multisample = false,
.enable_gamma = true,
.enable_curvature = false,
.enable_sharper = true},
{.name = "MINIMAL",
.scanline_weight = 8.0F,
.scanline_gap_brightness = 0.05F,
.bloom_factor = 2.0F,
.input_gamma = 2.4F,
.output_gamma = 2.2F,
.mask_brightness = 1.00F,
.curvature_x = 0.0F,
.curvature_y = 0.0F,
.mask_type = 0,
.enable_scanlines = true,
.enable_multisample = false,
.enable_gamma = false,
.enable_curvature = false,
.enable_sharper = false},
};
}
void parseCrtPiPreset(const fkyaml::node& node, Options::CrtPiPreset& preset) {
if (node.contains("name")) { preset.name = node["name"].get_value<std::string>(); }
if (node.contains("scanline_weight")) { preset.scanline_weight = node["scanline_weight"].get_value<float>(); }
if (node.contains("scanline_gap_brightness")) { preset.scanline_gap_brightness = node["scanline_gap_brightness"].get_value<float>(); }
if (node.contains("bloom_factor")) { preset.bloom_factor = node["bloom_factor"].get_value<float>(); }
if (node.contains("input_gamma")) { preset.input_gamma = node["input_gamma"].get_value<float>(); }
if (node.contains("output_gamma")) { preset.output_gamma = node["output_gamma"].get_value<float>(); }
if (node.contains("mask_brightness")) { preset.mask_brightness = node["mask_brightness"].get_value<float>(); }
if (node.contains("curvature_x")) { preset.curvature_x = node["curvature_x"].get_value<float>(); }
if (node.contains("curvature_y")) { preset.curvature_y = node["curvature_y"].get_value<float>(); }
if (node.contains("mask_type")) { preset.mask_type = node["mask_type"].get_value<int>(); }
if (node.contains("enable_scanlines")) { preset.enable_scanlines = node["enable_scanlines"].get_value<bool>(); }
if (node.contains("enable_multisample")) { preset.enable_multisample = node["enable_multisample"].get_value<bool>(); }
if (node.contains("enable_gamma")) { preset.enable_gamma = node["enable_gamma"].get_value<bool>(); }
if (node.contains("enable_curvature")) { preset.enable_curvature = node["enable_curvature"].get_value<bool>(); }
if (node.contains("enable_sharper")) { preset.enable_sharper = node["enable_sharper"].get_value<bool>(); }
}
void writeCrtPiPreset(std::ostringstream& out, const Options::CrtPiPreset& preset) {
out << " - name: \"" << preset.name << "\"\n";
out << " scanline_weight: " << preset.scanline_weight << "\n";
out << " scanline_gap_brightness: " << preset.scanline_gap_brightness << "\n";
out << " bloom_factor: " << preset.bloom_factor << "\n";
out << " input_gamma: " << preset.input_gamma << "\n";
out << " output_gamma: " << preset.output_gamma << "\n";
out << " mask_brightness: " << preset.mask_brightness << "\n";
out << " curvature_x: " << preset.curvature_x << "\n";
out << " curvature_y: " << preset.curvature_y << "\n";
out << " mask_type: " << preset.mask_type << "\n";
out << " enable_scanlines: " << (preset.enable_scanlines ? "true" : "false") << "\n";
out << " enable_multisample: " << (preset.enable_multisample ? "true" : "false") << "\n";
out << " enable_gamma: " << (preset.enable_gamma ? "true" : "false") << "\n";
out << " enable_curvature: " << (preset.enable_curvature ? "true" : "false") << "\n";
out << " enable_sharper: " << (preset.enable_sharper ? "true" : "false") << "\n";
}
// Pipeline genèric de càrrega de presets: si el fitxer no existeix o és
// invàlid (no deserialitza, manca `presets:`, no és seqüència, queda
// buit), es restauren els defaults via `defaults_fn` i es reescriu via
// `save_fn`. Unifica PostFX i CrtPi — abans eren dues còpies del mateix
// codi que només diferien en el tipus concret de preset.
template <typename T>
auto loadPresetsFromFile(
const std::string& path,
std::vector<T>& presets,
std::vector<T> (*defaults_fn)(),
void (*parse_fn)(const fkyaml::node&, T&),
bool (*save_fn)(),
const char* kind_label) -> bool {
if (path.empty()) { return false; }
const std::string CONTENT = readFileOrEmpty(path);
if (CONTENT.empty()) {
// Primera execució (no existia o era buit): no cal backup.
presets = defaults_fn();
return save_fn();
}
try {
auto yaml = fkyaml::node::deserialize(CONTENT);
if (!yaml.contains("presets") || !yaml["presets"].is_sequence()) {
std::cerr << "Options: " << kind_label << " manca `presets:`; reescrivint amb defaults.\n";
backupFile(path);
presets = defaults_fn();
return save_fn();
}
presets.clear();
for (const auto& preset_node : yaml["presets"]) {
T preset;
parse_fn(preset_node, preset);
presets.push_back(preset);
}
if (presets.empty()) {
std::cerr << "Options: " << kind_label << " sense presets; reescrivint amb defaults.\n";
backupFile(path);
presets = defaults_fn();
return save_fn();
}
return true;
} catch (const std::exception& e) {
std::cerr << "Options: error parsejant " << kind_label << ": " << e.what() << '\n';
std::cerr << "Options: reescrivint el fitxer amb presets per defecte.\n";
backupFile(path);
presets = defaults_fn();
return save_fn();
}
}
// Pipeline genèric d'escriptura: capçalera de comentari + `presets:` +
// una entrada per preset via `write_fn`. `config_key_note` és la segona
// línia de comentari que recorda amb quin camp de config.yaml han de
// coincidir els noms.
template <typename T>
auto savePresetsToFile(
const std::string& path,
const std::vector<T>& presets,
void (*write_fn)(std::ostringstream&, const T&),
const char* header_comment,
const char* config_key_note) -> bool {
if (path.empty()) { return false; }
std::ostringstream out;
out << "# " << header_comment << "\n";
out << "# " << config_key_note << "\n";
out << "presets:\n";
for (const auto& preset : presets) {
write_fn(out, preset);
}
return writeFile(path, out.str());
}
} // namespace
namespace Options {
void setConfigFile(const std::string& path) {
config_file_path = path;
}
auto loadFromFile() -> bool {
if (config_file_path.empty()) { return false; }
const std::string CONTENT = readFileOrEmpty(config_file_path);
if (CONTENT.empty()) {
// Primera execució: creem el fitxer amb els valors per defecte.
return saveToFile();
}
try {
auto yaml = fkyaml::node::deserialize(CONTENT);
std::string file_version;
if (yaml.contains("version")) {
file_version = yaml["version"].get_value<std::string>();
}
// Sense migració per ara: si la versió no coincideix, reescrivim
// amb els defaults de la versió nova. El fitxer original queda
// a `<path>.bak` per si l'usuari volia preservar tuning manual.
if (file_version != Texts::VERSION) {
std::cerr << "Options: config.yaml versió \"" << file_version
<< "\" != \"" << Texts::VERSION << "\"; reescrivint amb defaults.\n";
backupFile(config_file_path);
return saveToFile();
}
config_version = file_version;
if (yaml.contains("locale")) { Options::locale = yaml["locale"].get_value<std::string>(); }
parseWindow(yaml);
parseVideo(yaml);
parseAudio(yaml);
return true;
} catch (const std::exception& e) {
std::cerr << "Options: error parsejant config.yaml: " << e.what() << '\n';
std::cerr << "Options: reescrivint el fitxer amb defaults.\n";
backupFile(config_file_path);
return saveToFile();
}
}
auto saveToFile() -> bool {
if (config_file_path.empty()) { return false; }
config_version = Texts::VERSION;
std::ostringstream out;
out << "# Configuració d'AEE 2026. Generat automàticament; editable a mà.\n";
out << "version: \"" << config_version << "\"\n";
out << "locale: \"" << locale << "\"\n\n";
out << "window:\n";
out << " zoom: " << window.zoom << "\n";
out << " fullscreen: " << (window.fullscreen ? "true" : "false") << "\n\n";
out << "video:\n";
out << " vsync: " << (video.vsync ? "true" : "false") << "\n";
out << " scaling_mode: \"" << scalingModeToString(video.scaling_mode) << "\"\n";
out << " texture_filter: \"" << filterToString(video.texture_filter) << "\"\n";
out << " render_info_position: \"" << renderInfoPositionToString(video.render_info_position) << "\"\n";
out << " render_info_show_time: " << (video.render_info_show_time ? "true" : "false") << "\n";
out << " gpu:\n";
out << " acceleration: " << (video.gpu.acceleration ? "true" : "false") << "\n";
out << " preferred_driver: \"" << video.gpu.preferred_driver << "\"\n";
out << " shader:\n";
out << " enabled: " << (video.shader.enabled ? "true" : "false") << "\n";
out << " current_shader: \"" << video.shader.current_shader << "\"\n";
out << " current_postfx_preset: \"" << video.shader.current_postfx_preset << "\"\n";
out << " current_crtpi_preset: \"" << video.shader.current_crtpi_preset << "\"\n\n";
out << "audio:\n";
out << " enabled: " << (audio.enabled ? "true" : "false") << "\n";
out << " volume: " << audio.volume << "\n";
out << " music:\n";
out << " enabled: " << (audio.music.enabled ? "true" : "false") << "\n";
out << " volume: " << audio.music.volume << "\n";
out << " sound:\n";
out << " enabled: " << (audio.sound.enabled ? "true" : "false") << "\n";
out << " volume: " << audio.sound.volume << "\n";
return writeFile(config_file_path, out.str());
}
auto buildScreenConfig() -> Screen::Config {
Screen::Config config;
config.zoom = window.zoom;
config.fullscreen = window.fullscreen;
config.vsync = video.vsync;
config.scaling_mode = video.scaling_mode;
config.texture_filter = video.texture_filter;
config.gpu_acceleration = video.gpu.acceleration;
config.preferred_driver = video.gpu.preferred_driver;
return config;
}
auto buildAudioConfig() -> ::Audio::Config {
return {
.enabled = audio.enabled,
.volume = audio.volume,
.music_enabled = audio.music.enabled,
.music_volume = audio.music.volume,
.sound_enabled = audio.sound.enabled,
.sound_volume = audio.sound.volume,
};
}
void setPostFXFile(const std::string& path) {
postfx_file_path = path;
}
auto loadPostFXFromFile() -> bool {
return loadPresetsFromFile<PostFXPreset>(
postfx_file_path,
postfx_presets,
&defaultPostFXPresets,
&parsePostFXPreset,
&savePostFXToFile,
"postfx.yaml");
}
auto savePostFXToFile() -> bool {
return savePresetsToFile<PostFXPreset>(
postfx_file_path,
postfx_presets,
&writePostFXPreset,
"Presets PostFX per a AEE 2026. Editable a mà.",
"Els noms han de coincidir amb config.yaml > video.shader.current_postfx_preset.");
}
void setCrtPiFile(const std::string& path) {
crtpi_file_path = path;
}
auto loadCrtPiFromFile() -> bool {
return loadPresetsFromFile<CrtPiPreset>(
crtpi_file_path,
crtpi_presets,
&defaultCrtPiPresets,
&parseCrtPiPreset,
&saveCrtPiToFile,
"crtpi.yaml");
}
auto saveCrtPiToFile() -> bool {
return savePresetsToFile<CrtPiPreset>(
crtpi_file_path,
crtpi_presets,
&writeCrtPiPreset,
"Presets CRT-Pi per a AEE 2026. Editable a mà.",
"Els noms han de coincidir amb config.yaml > video.shader.current_crtpi_preset.");
}
auto buildActiveShader() -> Rendering::ShaderType {
if (video.shader.current_shader == "crtpi") {
return Rendering::ShaderType::CRTPI;
}
return Rendering::ShaderType::POSTFX;
}
auto buildRenderInfoFrame(Uint8 text_color, Uint8 shadow_color) -> RenderInfo::Frame {
const std::string& active_preset = (video.shader.current_shader == "crtpi")
? video.shader.current_crtpi_preset
: video.shader.current_postfx_preset;
return {
.target_position = video.render_info_position,
.show_time = video.render_info_show_time,
.shader_enabled = video.shader.enabled,
.shader_name = video.shader.current_shader,
.shader_preset = active_preset,
.text_color = text_color,
.shadow_color = shadow_color,
};
}
auto buildCrtPiParams() -> Rendering::CrtPiParams {
// Si el shader està deshabilitat, retornem defaults (el dispatch anirà per
// PostFX passthrough, però sent nets per si algú consulta aquests params).
if (!video.shader.enabled) { return {}; }
const auto IT = std::ranges::find_if(crtpi_presets,
[](const CrtPiPreset& preset) { return preset.name == video.shader.current_crtpi_preset; });
if (IT == crtpi_presets.end()) {
std::cerr << "Options: preset CrtPi \"" << video.shader.current_crtpi_preset << "\" no trobat; aplicant defaults.\n";
return {};
}
return {
.scanline_weight = IT->scanline_weight,
.scanline_gap_brightness = IT->scanline_gap_brightness,
.bloom_factor = IT->bloom_factor,
.input_gamma = IT->input_gamma,
.output_gamma = IT->output_gamma,
.mask_brightness = IT->mask_brightness,
.curvature_x = IT->curvature_x,
.curvature_y = IT->curvature_y,
.mask_type = IT->mask_type,
.enable_scanlines = IT->enable_scanlines,
.enable_multisample = IT->enable_multisample,
.enable_gamma = IT->enable_gamma,
.enable_curvature = IT->enable_curvature,
.enable_sharper = IT->enable_sharper,
};
}
auto buildPostFXParams() -> Rendering::PostFXParams {
// Si el shader està deshabilitat, retornem params a zero → passthrough.
if (!video.shader.enabled) { return {}; }
const auto IT = std::ranges::find_if(postfx_presets,
[](const PostFXPreset& preset) { return preset.name == video.shader.current_postfx_preset; });
if (IT == postfx_presets.end()) {
std::cerr << "Options: preset PostFX \"" << video.shader.current_postfx_preset << "\" no trobat; aplicant passthrough.\n";
return {};
}
return {
.vignette = IT->vignette,
.scanlines = IT->scanlines,
.chroma = IT->chroma,
.mask = IT->mask,
.gamma = IT->gamma,
.curvature = IT->curvature,
.bleeding = IT->bleeding,
.flicker = IT->flicker,
};
}
} // namespace Options