primera implementacio de postfx

This commit is contained in:
2026-03-23 13:18:36 +01:00
parent 99cc803f21
commit 2f3161d701
23 changed files with 13306 additions and 771 deletions

View File

@@ -221,6 +221,8 @@ namespace GameDefaults {
constexpr bool VIDEO_VSYNC = true;
constexpr bool VIDEO_INTEGER_SCALE = true;
constexpr bool VIDEO_SHADERS = false;
constexpr bool VIDEO_POSTFX = false;
constexpr bool VIDEO_SUPERSAMPLING = false;
// Music
constexpr bool MUSIC_ENABLED = true;

View File

@@ -105,7 +105,9 @@ void Director::init() {
Logger::section("INIT CONFIG");
Options::setConfigFile(Asset::get()->getPath("config.yaml")); // Establece el fichero de configuración
Options::setControllersFile(Asset::get()->getPath("controllers.json")); // Establece el fichero de configuración de mandos
Options::loadFromFile(); // Carga el archivo de configuración
Options::setPostFXFile(Asset::get()->getPath("postfx.yaml")); // Establece el fichero de presets PostFX
Options::loadFromFile(); // Carga el archivo de configuración
Options::loadPostFXFromFile(); // Carga los presets PostFX
loadParams(); // Carga los parámetros del programa
loadScoreFile(); // Carga el archivo de puntuaciones

View File

@@ -62,10 +62,23 @@ namespace GlobalInputs {
Notifier::get()->show({Lang::getText("[NOTIFICATIONS] 14") + " " + boolToOnOff(Options::video.vsync)});
}
// Activa o desactiva los shaders
void toggleShaders() {
Screen::toggleShaders();
Notifier::get()->show({Lang::getText("[NOTIFICATIONS] 13") + " " + boolToOnOff(Options::video.shaders)});
// Activa o desactiva los efectos PostFX
void togglePostFX() {
Screen::togglePostFX();
Notifier::get()->show({Lang::getText("[NOTIFICATIONS] 13") + " " + boolToOnOff(Options::video.postfx)});
}
// Avanza al siguiente preset PostFX
void nextPostFXPreset() {
Screen::nextPostFXPreset();
const std::string name = Options::postfx_presets.empty() ? "" : Options::postfx_presets.at(static_cast<size_t>(Options::current_postfx_preset)).name;
Notifier::get()->show({"PostFX: " + name});
}
// Activa o desactiva el supersampling 3x
void toggleSupersampling() {
Screen::toggleSupersampling();
Notifier::get()->show({"3x SS: " + boolToOnOff(Options::video.supersampling)});
}
// Cambia al siguiente idioma
@@ -169,7 +182,6 @@ namespace GlobalInputs {
{Action::TOGGLE_AUDIO, toggleAudio},
{Action::TOGGLE_AUTO_FIRE, toggleFireMode},
{Action::CHANGE_LANG, setNextLang},
{Action::TOGGLE_VIDEO_SHADERS, toggleShaders},
{Action::TOGGLE_VIDEO_INTEGER_SCALE, toggleIntegerScale},
{Action::TOGGLE_VIDEO_VSYNC, toggleVSync},
#ifdef _DEBUG
@@ -177,13 +189,29 @@ namespace GlobalInputs {
#endif
};
return std::ranges::any_of(ACTIONS, [](const auto& pair) -> auto {
if (Input::get()->checkAction(pair.first, Input::DO_NOT_ALLOW_REPEAT, Input::CHECK_KEYBOARD)) {
pair.second();
return true;
if (std::ranges::any_of(ACTIONS, [](const auto& pair) -> auto {
if (Input::get()->checkAction(pair.first, Input::DO_NOT_ALLOW_REPEAT, Input::CHECK_KEYBOARD)) {
pair.second();
return true;
}
return false;
})) {
return true;
}
// F4 con modificadores: Ctrl+F4 = supersampling, Shift+F4 = siguiente preset, F4 = toggle PostFX
if (Input::get()->checkAction(Input::Action::TOGGLE_VIDEO_POSTFX, Input::DO_NOT_ALLOW_REPEAT, Input::CHECK_KEYBOARD)) {
if ((SDL_GetModState() & SDL_KMOD_CTRL) != 0U) {
toggleSupersampling();
} else if (Options::video.postfx && ((SDL_GetModState() & SDL_KMOD_SHIFT) != 0U)) {
nextPostFXPreset();
} else {
togglePostFX();
}
return false;
});
return true;
}
return false;
}
// Comprueba el resto de entradas

View File

@@ -82,6 +82,7 @@ class Input {
{Action::WINDOW_INC_SIZE, KeyState(SDL_SCANCODE_F2)},
{Action::WINDOW_FULLSCREEN, KeyState(SDL_SCANCODE_F3)},
{Action::TOGGLE_VIDEO_SHADERS, KeyState(SDL_SCANCODE_F4)},
{Action::TOGGLE_VIDEO_POSTFX, KeyState(SDL_SCANCODE_F4)},
{Action::TOGGLE_VIDEO_INTEGER_SCALE, KeyState(SDL_SCANCODE_F5)},
{Action::TOGGLE_VIDEO_VSYNC, KeyState(SDL_SCANCODE_F6)},

View File

@@ -22,6 +22,7 @@ const std::unordered_map<InputAction, std::string> ACTION_TO_STRING = {
{InputAction::WINDOW_INC_SIZE, "WINDOW_INC_SIZE"},
{InputAction::WINDOW_DEC_SIZE, "WINDOW_DEC_SIZE"},
{InputAction::TOGGLE_VIDEO_SHADERS, "TOGGLE_VIDEO_SHADERS"},
{InputAction::TOGGLE_VIDEO_POSTFX, "TOGGLE_VIDEO_POSTFX"},
{InputAction::TOGGLE_VIDEO_INTEGER_SCALE, "TOGGLE_VIDEO_INTEGER_SCALE"},
{InputAction::TOGGLE_VIDEO_VSYNC, "TOGGLE_VIDEO_VSYNC"},
{InputAction::RESET, "RESET"},
@@ -52,6 +53,7 @@ const std::unordered_map<std::string, InputAction> STRING_TO_ACTION = {
{"WINDOW_INC_SIZE", InputAction::WINDOW_INC_SIZE},
{"WINDOW_DEC_SIZE", InputAction::WINDOW_DEC_SIZE},
{"TOGGLE_VIDEO_SHADERS", InputAction::TOGGLE_VIDEO_SHADERS},
{"TOGGLE_VIDEO_POSTFX", InputAction::TOGGLE_VIDEO_POSTFX},
{"TOGGLE_VIDEO_INTEGER_SCALE", InputAction::TOGGLE_VIDEO_INTEGER_SCALE},
{"TOGGLE_VIDEO_VSYNC", InputAction::TOGGLE_VIDEO_VSYNC},
{"RESET", InputAction::RESET},

View File

@@ -31,7 +31,8 @@ enum class InputAction : int { // Acciones de entrada posibles en el juego
WINDOW_FULLSCREEN,
WINDOW_INC_SIZE,
WINDOW_DEC_SIZE,
TOGGLE_VIDEO_SHADERS,
TOGGLE_VIDEO_SHADERS, // backward compat alias
TOGGLE_VIDEO_POSTFX,
TOGGLE_VIDEO_INTEGER_SCALE,
TOGGLE_VIDEO_VSYNC,
RESET,

View File

@@ -24,6 +24,16 @@ namespace Options {
GamepadManager gamepad_manager; // Opciones de mando para cada jugador
Keyboard keyboard; // Opciones para el teclado
PendingChanges pending_changes; // Opciones que se aplican al cerrar
std::vector<PostFXPreset> postfx_presets = { // Lista de presets de PostFX
{"CRT", 0.6F, 0.7F, 0.15F, 0.5F, 0.5F, 0.0F, 0.0F, 0.0F},
{"NTSC", 0.4F, 0.5F, 0.2F, 0.3F, 0.3F, 0.0F, 0.6F, 0.0F},
{"CURVED", 0.5F, 0.6F, 0.1F, 0.4F, 0.4F, 0.8F, 0.0F, 0.0F},
{"SCANLINES", 0.0F, 0.8F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F},
{"SUBTLE", 0.3F, 0.4F, 0.05F, 0.0F, 0.2F, 0.0F, 0.0F, 0.0F},
{"CRT LIVE", 0.5F, 0.6F, 0.3F, 0.3F, 0.4F, 0.3F, 0.4F, 0.8F},
};
int current_postfx_preset = 0; // Índice del preset PostFX activo
std::string postfx_file_path; // Ruta al fichero de presets PostFX
// Establece el fichero de configuración
void setConfigFile(const std::string& file_path) { settings.config_file = file_path; }
@@ -31,6 +41,166 @@ namespace Options {
// Establece el fichero de configuración de mandos
void setControllersFile(const std::string& file_path) { settings.controllers_file = file_path; }
// Establece la ruta del fichero de PostFX
void setPostFXFile(const std::string& path) { postfx_file_path = path; }
// Helper: extrae un campo float de un nodo YAML si existe, ignorando errores de conversión
static void parseFloatField(const fkyaml::node& node, const std::string& key, float& target) {
if (node.contains(key)) {
try {
target = node[key].get_value<float>();
} catch (...) {}
}
}
// Carga los presets de PostFX desde el fichero
auto loadPostFXFromFile() -> bool {
postfx_presets.clear();
std::ifstream file(postfx_file_path);
if (!file.good()) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "PostFX file not found, creating default: %s", postfx_file_path.c_str());
return savePostFXToFile();
}
std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
file.close();
try {
auto yaml = fkyaml::node::deserialize(content);
if (yaml.contains("presets")) {
const auto& presets = yaml["presets"];
for (const auto& p : presets) {
PostFXPreset preset;
if (p.contains("name")) {
preset.name = p["name"].get_value<std::string>();
}
parseFloatField(p, "vignette", preset.vignette);
parseFloatField(p, "scanlines", preset.scanlines);
parseFloatField(p, "chroma", preset.chroma);
parseFloatField(p, "mask", preset.mask);
parseFloatField(p, "gamma", preset.gamma);
parseFloatField(p, "curvature", preset.curvature);
parseFloatField(p, "bleeding", preset.bleeding);
parseFloatField(p, "flicker", preset.flicker);
postfx_presets.push_back(preset);
}
}
if (!postfx_presets.empty()) {
current_postfx_preset = std::clamp(
current_postfx_preset, 0,
static_cast<int>(postfx_presets.size()) - 1);
} else {
current_postfx_preset = 0;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "PostFX file loaded: %zu preset(s)", postfx_presets.size());
return true;
} catch (const fkyaml::exception& e) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Error parsing PostFX YAML: %s. Recreating defaults.", e.what());
return savePostFXToFile();
}
}
// Guarda los presets de PostFX por defecto al fichero
auto savePostFXToFile() -> bool {
if (postfx_file_path.empty()) {
return false;
}
std::ofstream file(postfx_file_path);
if (!file.is_open()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error: %s can't be opened for writing", postfx_file_path.c_str());
return false;
}
file << "# Coffee Crisis Arcade Edition - PostFX Presets\n";
file << "# Each preset defines the intensity of post-processing effects (0.0 to 1.0).\n";
file << "# vignette: screen darkening at the edges\n";
file << "# scanlines: horizontal scanline effect\n";
file << "# chroma: chromatic aberration (RGB color fringing)\n";
file << "# mask: phosphor dot mask (RGB subpixel pattern)\n";
file << "# gamma: gamma correction input 2.4 / output 2.2\n";
file << "# curvature: CRT barrel distortion\n";
file << "# bleeding: NTSC horizontal colour bleeding\n";
file << "# flicker: phosphor CRT flicker ~50 Hz (0.0 = off, 1.0 = max)\n";
file << "\n";
file << "presets:\n";
file << " - name: \"CRT\"\n";
file << " vignette: 0.6\n";
file << " scanlines: 0.7\n";
file << " chroma: 0.15\n";
file << " mask: 0.5\n";
file << " gamma: 0.5\n";
file << " curvature: 0.0\n";
file << " bleeding: 0.0\n";
file << " flicker: 0.0\n";
file << " - name: \"NTSC\"\n";
file << " vignette: 0.4\n";
file << " scanlines: 0.5\n";
file << " chroma: 0.2\n";
file << " mask: 0.3\n";
file << " gamma: 0.3\n";
file << " curvature: 0.0\n";
file << " bleeding: 0.6\n";
file << " flicker: 0.0\n";
file << " - name: \"CURVED\"\n";
file << " vignette: 0.5\n";
file << " scanlines: 0.6\n";
file << " chroma: 0.1\n";
file << " mask: 0.4\n";
file << " gamma: 0.4\n";
file << " curvature: 0.8\n";
file << " bleeding: 0.0\n";
file << " flicker: 0.0\n";
file << " - name: \"SCANLINES\"\n";
file << " vignette: 0.0\n";
file << " scanlines: 0.8\n";
file << " chroma: 0.0\n";
file << " mask: 0.0\n";
file << " gamma: 0.0\n";
file << " curvature: 0.0\n";
file << " bleeding: 0.0\n";
file << " flicker: 0.0\n";
file << " - name: \"SUBTLE\"\n";
file << " vignette: 0.3\n";
file << " scanlines: 0.4\n";
file << " chroma: 0.05\n";
file << " mask: 0.0\n";
file << " gamma: 0.2\n";
file << " curvature: 0.0\n";
file << " bleeding: 0.0\n";
file << " flicker: 0.0\n";
file << " - name: \"CRT LIVE\"\n";
file << " vignette: 0.5\n";
file << " scanlines: 0.6\n";
file << " chroma: 0.3\n";
file << " mask: 0.3\n";
file << " gamma: 0.4\n";
file << " curvature: 0.3\n";
file << " bleeding: 0.4\n";
file << " flicker: 0.8\n";
file.close();
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "PostFX file created with defaults: %s", postfx_file_path.c_str());
// Cargar los presets recién escritos
postfx_presets.clear();
postfx_presets.push_back({"CRT", 0.6F, 0.7F, 0.15F, 0.5F, 0.5F, 0.0F, 0.0F, 0.0F});
postfx_presets.push_back({"NTSC", 0.4F, 0.5F, 0.2F, 0.3F, 0.3F, 0.0F, 0.6F, 0.0F});
postfx_presets.push_back({"CURVED", 0.5F, 0.6F, 0.1F, 0.4F, 0.4F, 0.8F, 0.0F, 0.0F});
postfx_presets.push_back({"SCANLINES", 0.0F, 0.8F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F});
postfx_presets.push_back({"SUBTLE", 0.3F, 0.4F, 0.05F, 0.0F, 0.2F, 0.0F, 0.0F, 0.0F});
postfx_presets.push_back({"CRT LIVE", 0.5F, 0.6F, 0.3F, 0.3F, 0.4F, 0.3F, 0.4F, 0.8F});
current_postfx_preset = 0;
return true;
}
// Inicializa las opciones del programa
void init() {
// Dificultades
@@ -78,6 +248,20 @@ namespace Options {
if (vid.contains("shaders")) {
try { video.shaders = vid["shaders"].get_value<bool>(); } catch (...) {}
}
if (vid.contains("postfx")) {
try { video.postfx = vid["postfx"].get_value<bool>(); } catch (...) {}
}
if (vid.contains("supersampling")) {
try { video.supersampling = vid["supersampling"].get_value<bool>(); } catch (...) {}
}
if (vid.contains("postfx_preset")) {
try {
int preset = vid["postfx_preset"].get_value<int>();
if (preset >= 0 && preset < static_cast<int>(postfx_presets.size())) {
current_postfx_preset = preset;
}
} catch (...) {}
}
}
void loadAudioFromYaml(const fkyaml::node& yaml) {
@@ -247,6 +431,9 @@ namespace Options {
file << " vsync: " << boolToString(video.vsync) << "\n";
file << " integer_scale: " << boolToString(video.integer_scale) << "\n";
file << " shaders: " << boolToString(video.shaders) << "\n";
file << " postfx: " << boolToString(video.postfx) << "\n";
file << " supersampling: " << boolToString(video.supersampling) << "\n";
file << " postfx_preset: " << current_postfx_preset << "\n";
file << "\n";
// AUDIO

View File

@@ -24,6 +24,18 @@
namespace Options {
// --- Estructuras ---
struct PostFXPreset {
std::string name;
float vignette{0.6F};
float scanlines{0.7F};
float chroma{0.15F};
float mask{0.0F};
float gamma{0.0F};
float curvature{0.0F};
float bleeding{0.0F};
float flicker{0.0F};
};
struct Window {
std::string caption = GameDefaults::Options::WINDOW_CAPTION; // Texto que aparece en la barra de título de la ventana
int zoom = GameDefaults::Options::WINDOW_ZOOM; // Valor por el que se multiplica el tamaño de la ventana
@@ -35,7 +47,9 @@ namespace Options {
bool fullscreen = GameDefaults::Options::VIDEO_FULLSCREEN; // Indica si se usa pantalla completa
bool vsync = GameDefaults::Options::VIDEO_VSYNC; // Indica si se usa vsync
bool integer_scale = GameDefaults::Options::VIDEO_INTEGER_SCALE; // Indica si se usa escalado entero
bool shaders = GameDefaults::Options::VIDEO_SHADERS; // Indica si se usan shaders para los filtros de vídeo
bool shaders = GameDefaults::Options::VIDEO_SHADERS; // Indica si se usan shaders para los filtros de vídeo (backward compat)
bool postfx = GameDefaults::Options::VIDEO_POSTFX; // Indica si se usan efectos PostFX
bool supersampling = GameDefaults::Options::VIDEO_SUPERSAMPLING; // Indica si se usa supersampling 3x
std::string info; // Información sobre el modo de vídeo
};
@@ -278,11 +292,17 @@ namespace Options {
extern GamepadManager gamepad_manager; // Manager de mandos para cada jugador
extern Keyboard keyboard; // Opciones para el teclado
extern PendingChanges pending_changes; // Opciones que se aplican al cerrar
extern std::vector<PostFXPreset> postfx_presets; // Lista de presets de PostFX
extern int current_postfx_preset; // Índice del preset PostFX activo
extern std::string postfx_file_path; // Ruta al fichero de presets PostFX
// --- Funciones ---
void init(); // Inicializa las opciones del programa
void setConfigFile(const std::string& file_path); // Establece el fichero de configuración
void setControllersFile(const std::string& file_path); // Establece el fichero de configuración de mandos
void setPostFXFile(const std::string& path); // Establece el fichero de presets PostFX
auto loadPostFXFromFile() -> bool; // Carga los presets PostFX desde fichero
auto savePostFXToFile() -> bool; // Guarda los presets PostFX por defecto al fichero
auto loadFromFile() -> bool; // Carga el fichero de configuración
auto saveToFile() -> bool; // Guarda el fichero de configuración
void setKeyboardToPlayer(Player::Id player_id); // Asigna el teclado al jugador

View File

@@ -1,522 +0,0 @@
#include "opengl_shader.hpp"
#include <SDL3/SDL.h>
#include <array>
#include <cstring>
#include <stdexcept>
#include <vector>
#include "ui/logger.hpp" // Para Loger
namespace Rendering {
OpenGLShader::~OpenGLShader() {
cleanup();
}
#ifndef __APPLE__
auto OpenGLShader::initGLExtensions() -> bool {
glCreateShader = (PFNGLCREATESHADERPROC)SDL_GL_GetProcAddress("glCreateShader");
glShaderSource = (PFNGLSHADERSOURCEPROC)SDL_GL_GetProcAddress("glShaderSource");
glCompileShader = (PFNGLCOMPILESHADERPROC)SDL_GL_GetProcAddress("glCompileShader");
glGetShaderiv = (PFNGLGETSHADERIVPROC)SDL_GL_GetProcAddress("glGetShaderiv");
glGetShaderInfoLog = (PFNGLGETSHADERINFOLOGPROC)SDL_GL_GetProcAddress("glGetShaderInfoLog");
glDeleteShader = (PFNGLDELETESHADERPROC)SDL_GL_GetProcAddress("glDeleteShader");
glAttachShader = (PFNGLATTACHSHADERPROC)SDL_GL_GetProcAddress("glAttachShader");
glCreateProgram = (PFNGLCREATEPROGRAMPROC)SDL_GL_GetProcAddress("glCreateProgram");
glLinkProgram = (PFNGLLINKPROGRAMPROC)SDL_GL_GetProcAddress("glLinkProgram");
glValidateProgram = (PFNGLVALIDATEPROGRAMPROC)SDL_GL_GetProcAddress("glValidateProgram");
glGetProgramiv = (PFNGLGETPROGRAMIVPROC)SDL_GL_GetProcAddress("glGetProgramiv");
glGetProgramInfoLog = (PFNGLGETPROGRAMINFOLOGPROC)SDL_GL_GetProcAddress("glGetProgramInfoLog");
glUseProgram = (PFNGLUSEPROGRAMPROC)SDL_GL_GetProcAddress("glUseProgram");
glDeleteProgram = (PFNGLDELETEPROGRAMPROC)SDL_GL_GetProcAddress("glDeleteProgram");
glGetUniformLocation = (PFNGLGETUNIFORMLOCATIONPROC)SDL_GL_GetProcAddress("glGetUniformLocation");
glUniform2f = (PFNGLUNIFORM2FPROC)SDL_GL_GetProcAddress("glUniform2f");
glGenVertexArrays = (PFNGLGENVERTEXARRAYSPROC)SDL_GL_GetProcAddress("glGenVertexArrays");
glBindVertexArray = (PFNGLBINDVERTEXARRAYPROC)SDL_GL_GetProcAddress("glBindVertexArray");
glDeleteVertexArrays = (PFNGLDELETEVERTEXARRAYSPROC)SDL_GL_GetProcAddress("glDeleteVertexArrays");
glGenBuffers = (PFNGLGENBUFFERSPROC)SDL_GL_GetProcAddress("glGenBuffers");
glBindBuffer = (PFNGLBINDBUFFERPROC)SDL_GL_GetProcAddress("glBindBuffer");
glBufferData = (PFNGLBUFFERDATAPROC)SDL_GL_GetProcAddress("glBufferData");
glDeleteBuffers = (PFNGLDELETEBUFFERSPROC)SDL_GL_GetProcAddress("glDeleteBuffers");
glVertexAttribPointer = (PFNGLVERTEXATTRIBPOINTERPROC)SDL_GL_GetProcAddress("glVertexAttribPointer");
glEnableVertexAttribArray = (PFNGLENABLEVERTEXATTRIBARRAYPROC)SDL_GL_GetProcAddress("glEnableVertexAttribArray");
return (glCreateShader != nullptr) && (glShaderSource != nullptr) && (glCompileShader != nullptr) && (glGetShaderiv != nullptr) &&
(glGetShaderInfoLog != nullptr) && (glDeleteShader != nullptr) && (glAttachShader != nullptr) && (glCreateProgram != nullptr) &&
(glLinkProgram != nullptr) && (glValidateProgram != nullptr) && (glGetProgramiv != nullptr) && (glGetProgramInfoLog != nullptr) &&
(glUseProgram != nullptr) && (glDeleteProgram != nullptr) && (glGetUniformLocation != nullptr) && (glUniform2f != nullptr) &&
(glGenVertexArrays != nullptr) && (glBindVertexArray != nullptr) && (glDeleteVertexArrays != nullptr) &&
(glGenBuffers != nullptr) && (glBindBuffer != nullptr) && (glBufferData != nullptr) && (glDeleteBuffers != nullptr) &&
(glVertexAttribPointer != nullptr) && (glEnableVertexAttribArray != nullptr);
}
#endif
void OpenGLShader::checkGLError(const char* operation) {
GLenum error = glGetError();
if (error != GL_NO_ERROR) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Error OpenGL en %s: 0x%x",
operation,
error);
}
}
auto OpenGLShader::compileShader(const std::string& source, GLenum shader_type) -> GLuint {
if (source.empty()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"ERROR: El código fuente del shader está vacío");
return 0;
}
GLuint shader_id = glCreateShader(shader_type);
if (shader_id == 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error al crear shader");
checkGLError("glCreateShader");
return 0;
}
std::array<const char*, 1> sources = {source.c_str()};
glShaderSource(shader_id, 1, sources.data(), nullptr);
checkGLError("glShaderSource");
glCompileShader(shader_id);
checkGLError("glCompileShader");
// Verificar compilación
GLint compiled = GL_FALSE;
glGetShaderiv(shader_id, GL_COMPILE_STATUS, &compiled);
if (compiled != GL_TRUE) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Error en compilación del shader");
GLint log_length;
glGetShaderiv(shader_id, GL_INFO_LOG_LENGTH, &log_length);
if (log_length > 0) {
std::vector<char> log(log_length);
glGetShaderInfoLog(shader_id, log_length, &log_length, log.data());
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Log de compilación: %s",
log.data());
}
glDeleteShader(shader_id);
return 0;
}
return shader_id;
}
auto OpenGLShader::linkProgram(GLuint vertex_shader, GLuint fragment_shader) -> GLuint {
GLuint program = glCreateProgram();
if (program == 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Error al crear programa de shaders");
return 0;
}
glAttachShader(program, vertex_shader);
checkGLError("glAttachShader(vertex)");
glAttachShader(program, fragment_shader);
checkGLError("glAttachShader(fragment)");
glLinkProgram(program);
checkGLError("glLinkProgram");
// Verificar enlace
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked != GL_TRUE) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Error al enlazar programa");
GLint log_length;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &log_length);
if (log_length > 0) {
std::vector<char> log(log_length);
glGetProgramInfoLog(program, log_length, &log_length, log.data());
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Log de enlace: %s",
log.data());
}
glDeleteProgram(program);
return 0;
}
glValidateProgram(program);
checkGLError("glValidateProgram");
return program;
}
void OpenGLShader::createQuadGeometry() {
// Datos del quad: posición (x, y) + coordenadas de textura (u, v)
// Formato: x, y, u, v
std::array<float, 16> vertices = {
// Posición // TexCoords
-1.0F,
-1.0F,
0.0F,
0.0F, // Inferior izquierda
1.0F,
-1.0F,
1.0F,
0.0F, // Inferior derecha
1.0F,
1.0F,
1.0F,
1.0F, // Superior derecha
-1.0F,
1.0F,
0.0F,
1.0F // Superior izquierda
};
// Índices para dibujar el quad con dos triángulos
std::array<unsigned int, 6> indices = {
0,
1,
2, // Primer triángulo
2,
3,
0 // Segundo triángulo
};
// Generar y configurar VAO
glGenVertexArrays(1, &vao_);
glBindVertexArray(vao_);
checkGLError("glBindVertexArray");
// Generar y configurar VBO
glGenBuffers(1, &vbo_);
glBindBuffer(GL_ARRAY_BUFFER, vbo_);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices.data(), GL_STATIC_DRAW);
checkGLError("glBufferData(VBO)");
// Generar y configurar EBO
glGenBuffers(1, &ebo_);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo_);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices.data(), GL_STATIC_DRAW);
checkGLError("glBufferData(EBO)");
// Atributo 0: Posición (2 floats)
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), nullptr);
glEnableVertexAttribArray(0);
checkGLError("glVertexAttribPointer(position)");
// Atributo 1: Coordenadas de textura (2 floats)
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), static_cast<const void*>(static_cast<const char*>(nullptr) + (2 * sizeof(float))));
glEnableVertexAttribArray(1);
checkGLError("glVertexAttribPointer(texcoord)");
// Desvincular
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}
auto OpenGLShader::getTextureID(SDL_Texture* texture) -> GLuint {
if (texture == nullptr) {
return 1;
}
SDL_PropertiesID props = SDL_GetTextureProperties(texture);
GLuint texture_id = 0;
// Intentar obtener ID de textura OpenGL
texture_id = static_cast<GLuint>(reinterpret_cast<uintptr_t>(SDL_GetPointerProperty(props, "SDL.texture.opengl.texture", nullptr)));
if (texture_id == 0) {
texture_id = static_cast<GLuint>(reinterpret_cast<uintptr_t>(SDL_GetPointerProperty(props, "texture.opengl.texture", nullptr)));
}
if (texture_id == 0) {
texture_id = static_cast<GLuint>(SDL_GetNumberProperty(props, "SDL.texture.opengl.texture", 1));
}
if (texture_id == 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"No se pudo obtener ID de textura OpenGL, usando 1 por defecto");
texture_id = 1;
}
return texture_id;
}
auto OpenGLShader::init(SDL_Window* window,
SDL_Texture* texture,
const std::string& vertex_source,
const std::string& fragment_source) -> bool {
window_ = window;
back_buffer_ = texture;
renderer_ = SDL_GetRenderer(window);
if (renderer_ == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Error: No se pudo obtener el renderer");
return false;
}
// Obtener tamaños
SDL_GetWindowSize(window_, &window_width_, &window_height_);
SDL_GetTextureSize(back_buffer_, &texture_width_, &texture_height_);
/*
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Inicializando shaders: ventana=%dx%d, textura=%.0fx%.0f",
window_width_,
window_height_,
texture_width_,
texture_height_);
*/
Logger::info(
"Inicializando shaders: ventana=" +
std::to_string(window_width_) +
"x" +
std::to_string(window_height_) +
", textura=" +
std::to_string(static_cast<int>(texture_width_)) +
"x" +
std::to_string(static_cast<int>(texture_height_)));
// Verificar que es OpenGL
const char* renderer_name = SDL_GetRendererName(renderer_);
if ((renderer_name == nullptr) || strncmp(renderer_name, "opengl", 6) != 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Renderer no es OpenGL: %s",
(renderer_name != nullptr) ? renderer_name : "unknown");
return false;
}
#ifndef __APPLE__
// Inicializar extensiones OpenGL en Windows/Linux
if (!initGLExtensions()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Error al inicializar extensiones OpenGL");
return false;
}
#endif
// Limpiar shader anterior si existe
if (program_id_ != 0) {
glDeleteProgram(program_id_);
program_id_ = 0;
}
// Compilar shaders
GLuint vertex_shader = compileShader(vertex_source, GL_VERTEX_SHADER);
GLuint fragment_shader = compileShader(fragment_source, GL_FRAGMENT_SHADER);
if (vertex_shader == 0 || fragment_shader == 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Error al compilar shaders");
if (vertex_shader != 0) {
glDeleteShader(vertex_shader);
}
if (fragment_shader != 0) {
glDeleteShader(fragment_shader);
}
return false;
}
// Enlazar programa
program_id_ = linkProgram(vertex_shader, fragment_shader);
// Limpiar shaders (ya no necesarios tras el enlace)
glDeleteShader(vertex_shader);
glDeleteShader(fragment_shader);
if (program_id_ == 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Error al crear programa de shaders");
return false;
}
// Crear geometría del quad
createQuadGeometry();
// Obtener ubicación del uniform TextureSize
glUseProgram(program_id_);
texture_size_location_ = glGetUniformLocation(program_id_, "TextureSize");
if (texture_size_location_ != -1) {
/*
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Configurando TextureSize uniform: %.0fx%.0f",
texture_width_,
texture_height_);
*/
Logger::info(
"Configurando TextureSize uniform: " +
std::to_string(static_cast<int>(texture_width_)) +
"x" +
std::to_string(static_cast<int>(texture_height_)));
glUniform2f(texture_size_location_, texture_width_, texture_height_);
checkGLError("glUniform2f(TextureSize)");
} else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Uniform 'TextureSize' no encontrado en shader");
}
glUseProgram(0);
is_initialized_ = true;
/*
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"** OpenGL 3.3 Shader Backend inicializado correctamente");
*/
Logger::info("OpenGL 3.3 Shader Backend inicializado correctamente");
return true;
}
void OpenGLShader::render() {
if (!is_initialized_ || program_id_ == 0) {
// Fallback: renderizado SDL normal
SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 255);
SDL_SetRenderTarget(renderer_, nullptr);
SDL_RenderClear(renderer_);
SDL_RenderTexture(renderer_, back_buffer_, nullptr, nullptr);
SDL_RenderPresent(renderer_);
return;
}
// Obtener tamaño actual de ventana (puede haber cambiado)
int current_width;
int current_height;
SDL_GetWindowSize(window_, &current_width, &current_height);
// Guardar estados OpenGL
GLint old_program;
glGetIntegerv(GL_CURRENT_PROGRAM, &old_program);
std::array<GLint, 4> old_viewport{};
glGetIntegerv(GL_VIEWPORT, old_viewport.data());
GLboolean was_texture_enabled = glIsEnabled(GL_TEXTURE_2D);
GLint old_texture;
glGetIntegerv(GL_TEXTURE_BINDING_2D, &old_texture);
GLint old_vao;
glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &old_vao);
// Preparar renderizado
SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 255);
SDL_SetRenderTarget(renderer_, nullptr);
SDL_RenderClear(renderer_);
// Obtener y bindear textura
GLuint texture_id = getTextureID(back_buffer_);
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, texture_id);
checkGLError("glBindTexture");
// Usar nuestro programa
glUseProgram(program_id_);
checkGLError("glUseProgram");
// Configurar viewport (obtener tamaño lógico de SDL)
int logical_w;
int logical_h;
SDL_RendererLogicalPresentation mode;
SDL_GetRenderLogicalPresentation(renderer_, &logical_w, &logical_h, &mode);
if (logical_w == 0 || logical_h == 0) {
logical_w = current_width;
logical_h = current_height;
}
// Calcular viewport considerando aspect ratio
int viewport_x = 0;
int viewport_y = 0;
int viewport_w = current_width;
int viewport_h = current_height;
if (mode == SDL_LOGICAL_PRESENTATION_INTEGER_SCALE) {
int scale_x = current_width / logical_w;
int scale_y = current_height / logical_h;
int scale = (scale_x < scale_y) ? scale_x : scale_y;
scale = std::max(scale, 1);
viewport_w = logical_w * scale;
viewport_h = logical_h * scale;
viewport_x = (current_width - viewport_w) / 2;
viewport_y = (current_height - viewport_h) / 2;
} else {
float window_aspect = static_cast<float>(current_width) / current_height;
float logical_aspect = static_cast<float>(logical_w) / logical_h;
if (window_aspect > logical_aspect) {
viewport_w = static_cast<int>(logical_aspect * current_height);
viewport_x = (current_width - viewport_w) / 2;
} else {
viewport_h = static_cast<int>(current_width / logical_aspect);
viewport_y = (current_height - viewport_h) / 2;
}
}
glViewport(viewport_x, viewport_y, viewport_w, viewport_h);
checkGLError("glViewport");
// Dibujar quad usando VAO
glBindVertexArray(vao_);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr);
checkGLError("glDrawElements");
// Presentar
SDL_GL_SwapWindow(window_);
// Restaurar estados OpenGL
glUseProgram(old_program);
glBindTexture(GL_TEXTURE_2D, old_texture);
if (was_texture_enabled == 0U) {
glDisable(GL_TEXTURE_2D);
}
glBindVertexArray(old_vao);
glViewport(old_viewport[0], old_viewport[1], old_viewport[2], old_viewport[3]);
}
void OpenGLShader::setTextureSize(float width, float height) {
if (!is_initialized_ || program_id_ == 0) {
return;
}
texture_width_ = width;
texture_height_ = height;
GLint old_program;
glGetIntegerv(GL_CURRENT_PROGRAM, &old_program);
glUseProgram(program_id_);
if (texture_size_location_ != -1) {
glUniform2f(texture_size_location_, width, height);
checkGLError("glUniform2f(TextureSize)");
}
glUseProgram(old_program);
}
void OpenGLShader::cleanup() {
if (vao_ != 0) {
glDeleteVertexArrays(1, &vao_);
vao_ = 0;
}
if (vbo_ != 0) {
glDeleteBuffers(1, &vbo_);
vbo_ = 0;
}
if (ebo_ != 0) {
glDeleteBuffers(1, &ebo_);
ebo_ = 0;
}
if (program_id_ != 0) {
glDeleteProgram(program_id_);
program_id_ = 0;
}
is_initialized_ = false;
window_ = nullptr;
renderer_ = nullptr;
back_buffer_ = nullptr;
}
} // namespace Rendering

View File

@@ -1,98 +0,0 @@
#pragma once
#include "../shader_backend.hpp"
#ifdef __APPLE__
#include <OpenGL/gl3.h>
#else
#include <SDL3/SDL_opengl.h>
#endif
namespace Rendering {
/**
* @brief Backend de shaders usando OpenGL 3.3 Core Profile
*
* Implementa el renderizado de shaders usando APIs modernas de OpenGL:
* - VAO (Vertex Array Objects)
* - VBO (Vertex Buffer Objects)
* - Shaders GLSL #version 330 core
*/
class OpenGLShader : public ShaderBackend {
public:
OpenGLShader() = default;
~OpenGLShader() override;
auto init(SDL_Window* window,
SDL_Texture* texture,
const std::string& vertex_source,
const std::string& fragment_source) -> bool override;
void render() override;
void setTextureSize(float width, float height) override;
void cleanup() final;
[[nodiscard]] auto isHardwareAccelerated() const -> bool override { return is_initialized_; }
private:
// Funciones auxiliares
auto initGLExtensions() -> bool;
static auto compileShader(const std::string& source, GLenum shader_type) -> GLuint;
static auto linkProgram(GLuint vertex_shader, GLuint fragment_shader) -> GLuint;
void createQuadGeometry();
static auto getTextureID(SDL_Texture* texture) -> GLuint;
static void checkGLError(const char* operation);
// Estado SDL
SDL_Window* window_ = nullptr;
SDL_Renderer* renderer_ = nullptr;
SDL_Texture* back_buffer_ = nullptr;
// Estado OpenGL
GLuint program_id_ = 0;
GLuint vao_ = 0; // Vertex Array Object
GLuint vbo_ = 0; // Vertex Buffer Object
GLuint ebo_ = 0; // Element Buffer Object
// Ubicaciones de uniforms
GLint texture_size_location_ = -1;
// Tamaños
int window_width_ = 0;
int window_height_ = 0;
float texture_width_ = 0.0F;
float texture_height_ = 0.0F;
// Estado
bool is_initialized_ = false;
#ifndef __APPLE__
// Punteros a funciones OpenGL en Windows/Linux
PFNGLCREATESHADERPROC glCreateShader = nullptr;
PFNGLSHADERSOURCEPROC glShaderSource = nullptr;
PFNGLCOMPILESHADERPROC glCompileShader = nullptr;
PFNGLGETSHADERIVPROC glGetShaderiv = nullptr;
PFNGLGETSHADERINFOLOGPROC glGetShaderInfoLog = nullptr;
PFNGLDELETESHADERPROC glDeleteShader = nullptr;
PFNGLATTACHSHADERPROC glAttachShader = nullptr;
PFNGLCREATEPROGRAMPROC glCreateProgram = nullptr;
PFNGLLINKPROGRAMPROC glLinkProgram = nullptr;
PFNGLVALIDATEPROGRAMPROC glValidateProgram = nullptr;
PFNGLGETPROGRAMIVPROC glGetProgramiv = nullptr;
PFNGLGETPROGRAMINFOLOGPROC glGetProgramInfoLog = nullptr;
PFNGLUSEPROGRAMPROC glUseProgram = nullptr;
PFNGLDELETEPROGRAMPROC glDeleteProgram = nullptr;
PFNGLGETUNIFORMLOCATIONPROC glGetUniformLocation = nullptr;
PFNGLUNIFORM2FPROC glUniform2f = nullptr;
PFNGLGENVERTEXARRAYSPROC glGenVertexArrays = nullptr;
PFNGLBINDVERTEXARRAYPROC glBindVertexArray = nullptr;
PFNGLDELETEVERTEXARRAYSPROC glDeleteVertexArrays = nullptr;
PFNGLGENBUFFERSPROC glGenBuffers = nullptr;
PFNGLBINDBUFFERPROC glBindBuffer = nullptr;
PFNGLBUFFERDATAPROC glBufferData = nullptr;
PFNGLDELETEBUFFERSPROC glDeleteBuffers = nullptr;
PFNGLVERTEXATTRIBPOINTERPROC glVertexAttribPointer = nullptr;
PFNGLENABLEVERTEXATTRIBARRAYPROC glEnableVertexAttribArray = nullptr;
#endif
};
} // namespace Rendering

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,720 @@
#include "rendering/sdl3gpu/sdl3gpu_shader.hpp"
#include <SDL3/SDL_log.h>
#include <algorithm> // std::min, std::max, std::floor
#include <cmath> // std::floor
#include <cstring> // memcpy, strlen
#ifndef __APPLE__
#include "rendering/sdl3gpu/postfx_frag_spv.h"
#include "rendering/sdl3gpu/postfx_vert_spv.h"
#endif
#ifdef __APPLE__
// ============================================================================
// MSL shaders (Metal Shading Language) — macOS
// ============================================================================
// NOLINTBEGIN(readability-identifier-naming)
static const char* POSTFX_VERT_MSL = R"(
#include <metal_stdlib>
using namespace metal;
struct PostVOut {
float4 pos [[position]];
float2 uv;
};
vertex PostVOut postfx_vs(uint vid [[vertex_id]]) {
const float2 positions[3] = { {-1.0, -1.0}, {3.0, -1.0}, {-1.0, 3.0} };
const float2 uvs[3] = { { 0.0, 1.0}, {2.0, 1.0}, { 0.0,-1.0} };
PostVOut out;
out.pos = float4(positions[vid], 0.0, 1.0);
out.uv = uvs[vid];
return out;
}
)";
static const char* POSTFX_FRAG_MSL = R"(
#include <metal_stdlib>
using namespace metal;
struct PostVOut {
float4 pos [[position]];
float2 uv;
};
struct PostFXUniforms {
float vignette_strength;
float chroma_strength;
float scanline_strength;
float screen_height;
float mask_strength;
float gamma_strength;
float curvature;
float bleeding;
float pixel_scale;
float time;
float oversample; // 1.0 = sin SS, 3.0 = 3× supersampling
float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz
};
// YCbCr helpers for NTSC bleeding
static float3 rgb_to_ycc(float3 rgb) {
return float3(
0.299f*rgb.r + 0.587f*rgb.g + 0.114f*rgb.b,
-0.169f*rgb.r - 0.331f*rgb.g + 0.500f*rgb.b + 0.5f,
0.500f*rgb.r - 0.419f*rgb.g - 0.081f*rgb.b + 0.5f
);
}
static float3 ycc_to_rgb(float3 ycc) {
float y = ycc.x;
float cb = ycc.y - 0.5f;
float cr = ycc.z - 0.5f;
return clamp(float3(
y + 1.402f*cr,
y - 0.344f*cb - 0.714f*cr,
y + 1.772f*cb
), 0.0f, 1.0f);
}
fragment float4 postfx_fs(PostVOut in [[stage_in]],
texture2d<float> scene [[texture(0)]],
sampler samp [[sampler(0)]],
constant PostFXUniforms& u [[buffer(0)]]) {
float2 uv = in.uv;
// Curvatura barrel CRT
if (u.curvature > 0.0f) {
float2 c = uv - 0.5f;
float rsq = dot(c, c);
float2 dist = float2(0.05f, 0.1f) * u.curvature;
float2 barrelScale = 1.0f - 0.23f * dist;
c += c * (dist * rsq);
c *= barrelScale;
if (abs(c.x) >= 0.5f || abs(c.y) >= 0.5f) {
return float4(0.0f, 0.0f, 0.0f, 1.0f);
}
uv = c + 0.5f;
}
// Muestra base
float3 base = scene.sample(samp, uv).rgb;
// Sangrado NTSC — difuminado horizontal de crominancia.
// step = 1 pixel de juego en espacio UV (corrige SS: scene.get_width() = game_w * oversample).
float3 colour;
if (u.bleeding > 0.0f) {
float tw = float(scene.get_width());
float step = u.oversample / tw; // 1 pixel lógico en UV
float3 ycc = rgb_to_ycc(base);
float3 ycc_l2 = rgb_to_ycc(scene.sample(samp, uv - float2(2.0f*step, 0.0f)).rgb);
float3 ycc_l1 = rgb_to_ycc(scene.sample(samp, uv - float2(1.0f*step, 0.0f)).rgb);
float3 ycc_r1 = rgb_to_ycc(scene.sample(samp, uv + float2(1.0f*step, 0.0f)).rgb);
float3 ycc_r2 = rgb_to_ycc(scene.sample(samp, uv + float2(2.0f*step, 0.0f)).rgb);
ycc.yz = (ycc_l2.yz + ycc_l1.yz*2.0f + ycc.yz*2.0f + ycc_r1.yz*2.0f + ycc_r2.yz) / 8.0f;
colour = mix(base, ycc_to_rgb(ycc), u.bleeding);
} else {
colour = base;
}
// Aberración cromática (drift animado con time para efecto NTSC real)
float ca = u.chroma_strength * 0.005f * (1.0f + 0.15f * sin(u.time * 7.3f));
colour.r = scene.sample(samp, uv + float2(ca, 0.0f)).r;
colour.b = scene.sample(samp, uv - float2(ca, 0.0f)).b;
// Corrección gamma (linealizar antes de scanlines, codificar después)
if (u.gamma_strength > 0.0f) {
float3 lin = pow(colour, float3(2.4f));
colour = mix(colour, lin, u.gamma_strength);
}
// Scanlines — 1 pixel físico oscuro por fila lógica.
// Usa uv.y (independiente del offset de letterbox) con pixel_scale para
// calcular la posición dentro de la fila en coordenadas físicas.
// 3x: 1 dark + 2 bright. 4x: 1 dark + 3 bright.
// bright=3.5×, dark floor=0.42 (mantiene aspecto CRT original).
if (u.scanline_strength > 0.0f) {
float ps = max(1.0f, round(u.pixel_scale));
float frac_in_row = fract(uv.y * u.screen_height);
float row_pos = floor(frac_in_row * ps);
float is_dark = step(ps - 1.0f, row_pos);
float scan = mix(3.5f, 0.42f, is_dark);
colour *= mix(1.0f, scan, u.scanline_strength);
}
if (u.gamma_strength > 0.0f) {
float3 enc = pow(colour, float3(1.0f/2.2f));
colour = mix(colour, enc, u.gamma_strength);
}
// Viñeta
float2 d = uv - 0.5f;
float vignette = 1.0f - dot(d, d) * u.vignette_strength;
colour *= clamp(vignette, 0.0f, 1.0f);
// Máscara de fósforo RGB — después de scanlines (orden original):
// filas brillantes saturadas → máscara invisible, filas oscuras → RGB visible.
if (u.mask_strength > 0.0f) {
float whichMask = fract(in.pos.x * 0.3333333f);
float3 mask = float3(0.80f);
if (whichMask < 0.3333333f) mask.x = 1.0f;
else if (whichMask < 0.6666667f) mask.y = 1.0f;
else mask.z = 1.0f;
colour = mix(colour, colour * mask, u.mask_strength);
}
// Parpadeo de fósforo CRT (~50 Hz)
if (u.flicker > 0.0f) {
float flicker_wave = sin(u.time * 100.0f) * 0.5f + 0.5f;
colour *= 1.0f - u.flicker * 0.04f * flicker_wave;
}
return float4(colour, 1.0f);
}
)";
// NOLINTEND(readability-identifier-naming)
#endif // __APPLE__
namespace Rendering {
// ---------------------------------------------------------------------------
// Destructor
// ---------------------------------------------------------------------------
SDL3GPUShader::~SDL3GPUShader() {
destroy();
}
// ---------------------------------------------------------------------------
// init
// ---------------------------------------------------------------------------
auto SDL3GPUShader::init(SDL_Window* window,
SDL_Texture* texture,
const std::string& /*vertex_source*/,
const std::string& /*fragment_source*/) -> bool {
// Si ya estaba inicializado (p.ej. al cambiar borde), liberar recursos
// de textura/pipeline pero mantener el device vivo para evitar conflictos
// con SDL_Renderer en Windows/Vulkan.
if (is_initialized_) {
cleanup();
}
window_ = window;
// Dimensions from the SDL_Texture placeholder
float fw = 0.0F;
float fh = 0.0F;
SDL_GetTextureSize(texture, &fw, &fh);
game_width_ = static_cast<int>(fw);
game_height_ = static_cast<int>(fh);
tex_width_ = game_width_ * oversample_;
tex_height_ = game_height_ * oversample_;
uniforms_.screen_height = static_cast<float>(tex_height_); // Altura de la textura GPU
uniforms_.oversample = static_cast<float>(oversample_);
// ----------------------------------------------------------------
// 1. Create GPU device (solo si no existe ya)
// ----------------------------------------------------------------
if (device_ == nullptr) {
#ifdef __APPLE__
const SDL_GPUShaderFormat PREFERRED = SDL_GPU_SHADERFORMAT_MSL | SDL_GPU_SHADERFORMAT_METALLIB;
#else
const SDL_GPUShaderFormat PREFERRED = SDL_GPU_SHADERFORMAT_SPIRV;
#endif
device_ = SDL_CreateGPUDevice(PREFERRED, false, nullptr);
if (device_ == nullptr) {
SDL_Log("SDL3GPUShader: SDL_CreateGPUDevice failed: %s", SDL_GetError());
return false;
}
SDL_Log("SDL3GPUShader: driver = %s", SDL_GetGPUDeviceDriver(device_));
// ----------------------------------------------------------------
// 2. Claim window (una sola vez — no liberar hasta destroy())
// ----------------------------------------------------------------
if (!SDL_ClaimWindowForGPUDevice(device_, window_)) {
SDL_Log("SDL3GPUShader: SDL_ClaimWindowForGPUDevice failed: %s", SDL_GetError());
SDL_DestroyGPUDevice(device_);
device_ = nullptr;
return false;
}
SDL_SetGPUSwapchainParameters(device_, window_, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, vsync_ ? SDL_GPU_PRESENTMODE_VSYNC : SDL_GPU_PRESENTMODE_IMMEDIATE);
}
// ----------------------------------------------------------------
// 3. Create scene texture (upload target + sampler source)
// Format: B8G8R8A8_UNORM matches SDL ARGB8888 byte layout on LE
// ----------------------------------------------------------------
SDL_GPUTextureCreateInfo tex_info = {};
tex_info.type = SDL_GPU_TEXTURETYPE_2D;
tex_info.format = SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM;
tex_info.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER;
tex_info.width = static_cast<Uint32>(tex_width_);
tex_info.height = static_cast<Uint32>(tex_height_);
tex_info.layer_count_or_depth = 1;
tex_info.num_levels = 1;
scene_texture_ = SDL_CreateGPUTexture(device_, &tex_info);
if (scene_texture_ == nullptr) {
SDL_Log("SDL3GPUShader: failed to create scene texture: %s", SDL_GetError());
cleanup();
return false;
}
// ----------------------------------------------------------------
// 4. Create upload transfer buffer (CPU → GPU, size = w*h*4 bytes)
// ----------------------------------------------------------------
SDL_GPUTransferBufferCreateInfo tb_info = {};
tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
tb_info.size = static_cast<Uint32>(tex_width_ * tex_height_ * 4);
upload_buffer_ = SDL_CreateGPUTransferBuffer(device_, &tb_info);
if (upload_buffer_ == nullptr) {
SDL_Log("SDL3GPUShader: failed to create upload buffer: %s", SDL_GetError());
cleanup();
return false;
}
// ----------------------------------------------------------------
// 5. Create samplers: NEAREST (pixel art) + LINEAR (supersampling)
// ----------------------------------------------------------------
SDL_GPUSamplerCreateInfo samp_info = {};
samp_info.min_filter = SDL_GPU_FILTER_NEAREST;
samp_info.mag_filter = SDL_GPU_FILTER_NEAREST;
samp_info.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST;
samp_info.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
samp_info.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
samp_info.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
sampler_ = SDL_CreateGPUSampler(device_, &samp_info);
if (sampler_ == nullptr) {
SDL_Log("SDL3GPUShader: failed to create sampler: %s", SDL_GetError());
cleanup();
return false;
}
SDL_GPUSamplerCreateInfo lsamp_info = {};
lsamp_info.min_filter = SDL_GPU_FILTER_LINEAR;
lsamp_info.mag_filter = SDL_GPU_FILTER_LINEAR;
lsamp_info.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST;
lsamp_info.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
lsamp_info.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
lsamp_info.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
linear_sampler_ = SDL_CreateGPUSampler(device_, &lsamp_info);
if (linear_sampler_ == nullptr) {
SDL_Log("SDL3GPUShader: failed to create linear sampler: %s", SDL_GetError());
cleanup();
return false;
}
// ----------------------------------------------------------------
// 6. Create PostFX graphics pipeline
// ----------------------------------------------------------------
if (!createPipeline()) {
cleanup();
return false;
}
is_initialized_ = true;
SDL_Log("SDL3GPUShader: initialized OK (%dx%d)", tex_width_, tex_height_);
return true;
}
// ---------------------------------------------------------------------------
// createPipeline
// ---------------------------------------------------------------------------
auto SDL3GPUShader::createPipeline() -> bool {
const SDL_GPUTextureFormat SWAPCHAIN_FMT = SDL_GetGPUSwapchainTextureFormat(device_, window_);
#ifdef __APPLE__
SDL_GPUShader* vert = createShaderMSL(device_, POSTFX_VERT_MSL, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
SDL_GPUShader* frag = createShaderMSL(device_, POSTFX_FRAG_MSL, "postfx_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
#else
SDL_GPUShader* vert = createShaderSPIRV(device_, kpostfx_vert_spv, kpostfx_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
SDL_GPUShader* frag = createShaderSPIRV(device_, kpostfx_frag_spv, kpostfx_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
#endif
if ((vert == nullptr) || (frag == nullptr)) {
SDL_Log("SDL3GPUShader: failed to compile PostFX shaders");
if (vert != nullptr) { SDL_ReleaseGPUShader(device_, vert); }
if (frag != nullptr) { SDL_ReleaseGPUShader(device_, frag); }
return false;
}
SDL_GPUColorTargetBlendState no_blend = {};
no_blend.enable_blend = false;
no_blend.enable_color_write_mask = false;
SDL_GPUColorTargetDescription color_target = {};
color_target.format = SWAPCHAIN_FMT;
color_target.blend_state = no_blend;
SDL_GPUVertexInputState no_input = {};
SDL_GPUGraphicsPipelineCreateInfo pipe_info = {};
pipe_info.vertex_shader = vert;
pipe_info.fragment_shader = frag;
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 = &color_target;
pipeline_ = SDL_CreateGPUGraphicsPipeline(device_, &pipe_info);
SDL_ReleaseGPUShader(device_, vert);
SDL_ReleaseGPUShader(device_, frag);
if (pipeline_ == nullptr) {
SDL_Log("SDL3GPUShader: pipeline creation failed: %s", SDL_GetError());
return false;
}
return true;
}
// ---------------------------------------------------------------------------
// uploadPixels — copies ARGB8888 CPU pixels into the GPU transfer buffer.
// Con supersampling (oversample_ > 1) expande cada pixel del juego a un bloque
// oversample × oversample y hornea la scanline oscura en la última fila del bloque.
// ---------------------------------------------------------------------------
void SDL3GPUShader::uploadPixels(const Uint32* pixels, int width, int height) {
if (!is_initialized_ || (upload_buffer_ == nullptr)) { return; }
void* mapped = SDL_MapGPUTransferBuffer(device_, upload_buffer_, false);
if (mapped == nullptr) {
SDL_Log("SDL3GPUShader: SDL_MapGPUTransferBuffer failed: %s", SDL_GetError());
return;
}
if (oversample_ <= 1) {
// Path sin supersampling: copia directa
std::memcpy(mapped, pixels, static_cast<size_t>(width * height * 4));
} else {
// Path con supersampling: expande cada pixel a OS×OS, oscurece última fila.
// Replica la fórmula del shader: mix(3.5, 0.42, scanline_strength).
auto* out = static_cast<Uint32*>(mapped);
const int OS = oversample_;
const float BRIGHT_MUL = 1.0F + (baked_scanline_strength_ * 2.5F); // rows 0..OS-2
const float DARK_MUL = 1.0F - (baked_scanline_strength_ * 0.58F); // row OS-1
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
const Uint32 SRC = pixels[(y * width) + x];
const Uint32 ALPHA = (SRC >> 24) & 0xFFU;
const auto FR = static_cast<float>((SRC >> 16) & 0xFFU);
const auto FG = static_cast<float>((SRC >> 8) & 0xFFU);
const auto FB = static_cast<float>(SRC & 0xFFU);
auto make_px = [ALPHA](float rv, float gv, float bv) -> Uint32 {
auto cl = [](float v) -> Uint32 { return static_cast<Uint32>(std::min(255.0F, v)); };
return (ALPHA << 24) | (cl(rv) << 16) | (cl(gv) << 8) | cl(bv);
};
const Uint32 BRIGHT = make_px(FR * BRIGHT_MUL, FG * BRIGHT_MUL, FB * BRIGHT_MUL);
const Uint32 DARK = make_px(FR * DARK_MUL, FG * DARK_MUL, FB * DARK_MUL);
for (int dy = 0; dy < OS; ++dy) {
const Uint32 OUT_PX = (dy == OS - 1) ? DARK : BRIGHT;
const int DST_Y = (y * OS) + dy;
for (int dx = 0; dx < OS; ++dx) {
out[(DST_Y * (width * OS)) + ((x * OS) + dx)] = OUT_PX;
}
}
}
}
}
SDL_UnmapGPUTransferBuffer(device_, upload_buffer_);
}
// ---------------------------------------------------------------------------
// render — upload scene texture + PostFX pass → swapchain
// ---------------------------------------------------------------------------
void SDL3GPUShader::render() {
if (!is_initialized_) { return; }
SDL_GPUCommandBuffer* cmd = SDL_AcquireGPUCommandBuffer(device_);
if (cmd == nullptr) {
SDL_Log("SDL3GPUShader: SDL_AcquireGPUCommandBuffer failed: %s", SDL_GetError());
return;
}
// ---- Copy pass: transfer buffer → scene texture ----
SDL_GPUCopyPass* copy = SDL_BeginGPUCopyPass(cmd);
if (copy != nullptr) {
SDL_GPUTextureTransferInfo src = {};
src.transfer_buffer = upload_buffer_;
src.offset = 0;
src.pixels_per_row = static_cast<Uint32>(tex_width_);
src.rows_per_layer = static_cast<Uint32>(tex_height_);
SDL_GPUTextureRegion dst = {};
dst.texture = scene_texture_;
dst.w = static_cast<Uint32>(tex_width_);
dst.h = static_cast<Uint32>(tex_height_);
dst.d = 1;
SDL_UploadToGPUTexture(copy, &src, &dst, false);
SDL_EndGPUCopyPass(copy);
}
// ---- Acquire swapchain texture ----
SDL_GPUTexture* swapchain = nullptr;
Uint32 sw = 0;
Uint32 sh = 0;
if (!SDL_AcquireGPUSwapchainTexture(cmd, window_, &swapchain, &sw, &sh)) {
SDL_Log("SDL3GPUShader: SDL_AcquireGPUSwapchainTexture failed: %s", SDL_GetError());
SDL_SubmitGPUCommandBuffer(cmd);
return;
}
if (swapchain == nullptr) {
// Window minimized — skip frame
SDL_SubmitGPUCommandBuffer(cmd);
return;
}
// ---- Render pass: PostFX → swapchain ----
SDL_GPUColorTargetInfo color_target = {};
color_target.texture = swapchain;
color_target.load_op = SDL_GPU_LOADOP_CLEAR;
color_target.store_op = SDL_GPU_STOREOP_STORE;
color_target.clear_color = {.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F};
SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd, &color_target, 1, nullptr);
if (pass != nullptr) {
SDL_BindGPUGraphicsPipeline(pass, pipeline_);
// Calcular viewport usando las dimensiones lógicas del canvas (game_width_/height_),
// no las de la textura GPU (que pueden ser game×3 con supersampling).
// El GPU escala la textura para cubrir el viewport independientemente de su resolución.
float vx = 0.0F;
float vy = 0.0F;
float vw = 0.0F;
float vh = 0.0F;
if (integer_scale_) {
const int SCALE = std::max(1, std::min(static_cast<int>(sw) / game_width_, static_cast<int>(sh) / game_height_));
vw = static_cast<float>(game_width_ * SCALE);
vh = static_cast<float>(game_height_ * SCALE);
} else {
const float SCALE = std::min(
static_cast<float>(sw) / static_cast<float>(game_width_),
static_cast<float>(sh) / static_cast<float>(game_height_));
vw = static_cast<float>(game_width_) * SCALE;
vh = static_cast<float>(game_height_) * SCALE;
}
vx = std::floor((static_cast<float>(sw) - vw) * 0.5F);
vy = std::floor((static_cast<float>(sh) - vh) * 0.5F);
SDL_GPUViewport vp = {.x = vx, .y = vy, .w = vw, .h = vh, .min_depth = 0.0F, .max_depth = 1.0F};
SDL_SetGPUViewport(pass, &vp);
// pixel_scale: pixels físicos por pixel lógico de juego (para scanlines sin SS).
// Con SS las scanlines están horneadas en CPU → scanline_strength=0 → no se usa.
uniforms_.pixel_scale = (game_height_ > 0)
? (vh / static_cast<float>(game_height_))
: 1.0F;
uniforms_.time = static_cast<float>(SDL_GetTicks()) / 1000.0F;
uniforms_.oversample = static_cast<float>(oversample_);
// Con supersampling usamos LINEAR para que el escalado a zooms no-múltiplo-de-3
// promedia correctamente las filas de scanline horneadas en CPU.
SDL_GPUSampler* active_sampler = (oversample_ > 1 && linear_sampler_ != nullptr)
? linear_sampler_
: sampler_;
SDL_GPUTextureSamplerBinding binding = {};
binding.texture = scene_texture_;
binding.sampler = active_sampler;
SDL_BindGPUFragmentSamplers(pass, 0, &binding, 1);
SDL_PushGPUFragmentUniformData(cmd, 0, &uniforms_, sizeof(PostFXUniforms));
SDL_DrawGPUPrimitives(pass, 3, 1, 0, 0);
SDL_EndGPURenderPass(pass);
}
SDL_SubmitGPUCommandBuffer(cmd);
}
// ---------------------------------------------------------------------------
// cleanup — libera pipeline/texturas/buffer pero mantiene device + swapchain
// ---------------------------------------------------------------------------
void SDL3GPUShader::cleanup() {
is_initialized_ = false;
if (device_ != nullptr) {
SDL_WaitForGPUIdle(device_);
if (pipeline_ != nullptr) {
SDL_ReleaseGPUGraphicsPipeline(device_, pipeline_);
pipeline_ = nullptr;
}
if (scene_texture_ != nullptr) {
SDL_ReleaseGPUTexture(device_, scene_texture_);
scene_texture_ = nullptr;
}
if (upload_buffer_ != nullptr) {
SDL_ReleaseGPUTransferBuffer(device_, upload_buffer_);
upload_buffer_ = nullptr;
}
if (sampler_ != nullptr) {
SDL_ReleaseGPUSampler(device_, sampler_);
sampler_ = nullptr;
}
if (linear_sampler_ != nullptr) {
SDL_ReleaseGPUSampler(device_, linear_sampler_);
linear_sampler_ = nullptr;
}
// device_ y el claim de la ventana se mantienen vivos
}
}
// ---------------------------------------------------------------------------
// destroy — limpieza completa incluyendo device y swapchain (solo al cerrar)
// ---------------------------------------------------------------------------
void SDL3GPUShader::destroy() {
cleanup();
if (device_ != nullptr) {
if (window_ != nullptr) {
SDL_ReleaseWindowFromGPUDevice(device_, window_);
}
SDL_DestroyGPUDevice(device_);
device_ = nullptr;
}
window_ = nullptr;
}
// ---------------------------------------------------------------------------
// Shader creation helpers
// ---------------------------------------------------------------------------
auto SDL3GPUShader::createShaderMSL(SDL_GPUDevice* device,
const char* msl_source,
const char* entrypoint,
SDL_GPUShaderStage stage,
Uint32 num_samplers,
Uint32 num_uniform_buffers) -> SDL_GPUShader* {
SDL_GPUShaderCreateInfo info = {};
info.code = reinterpret_cast<const Uint8*>(msl_source);
info.code_size = std::strlen(msl_source) + 1;
info.entrypoint = entrypoint;
info.format = SDL_GPU_SHADERFORMAT_MSL;
info.stage = stage;
info.num_samplers = num_samplers;
info.num_uniform_buffers = num_uniform_buffers;
SDL_GPUShader* shader = SDL_CreateGPUShader(device, &info);
if (shader == nullptr) {
SDL_Log("SDL3GPUShader: MSL shader '%s' failed: %s", entrypoint, SDL_GetError());
}
return shader;
}
auto SDL3GPUShader::createShaderSPIRV(SDL_GPUDevice* device, // NOLINT(readability-convert-member-functions-to-static)
const uint8_t* spv_code,
size_t spv_size,
const char* entrypoint,
SDL_GPUShaderStage stage,
Uint32 num_samplers,
Uint32 num_uniform_buffers) -> SDL_GPUShader* {
SDL_GPUShaderCreateInfo info = {};
info.code = spv_code;
info.code_size = spv_size;
info.entrypoint = entrypoint;
info.format = SDL_GPU_SHADERFORMAT_SPIRV;
info.stage = stage;
info.num_samplers = num_samplers;
info.num_uniform_buffers = num_uniform_buffers;
SDL_GPUShader* shader = SDL_CreateGPUShader(device, &info);
if (shader == nullptr) {
SDL_Log("SDL3GPUShader: SPIRV shader '%s' failed: %s", entrypoint, SDL_GetError());
}
return shader;
}
void SDL3GPUShader::setPostFXParams(const PostFXParams& p) {
uniforms_.vignette_strength = p.vignette;
uniforms_.chroma_strength = p.chroma;
uniforms_.mask_strength = p.mask;
uniforms_.gamma_strength = p.gamma;
uniforms_.curvature = p.curvature;
uniforms_.bleeding = p.bleeding;
uniforms_.flicker = p.flicker;
// Con supersampling las scanlines se hornean en CPU (uploadPixels).
// El shader recibe strength=0 para no aplicarlas de nuevo en GPU.
baked_scanline_strength_ = p.scanlines;
uniforms_.scanline_strength = (oversample_ > 1) ? 0.0F : p.scanlines;
}
void SDL3GPUShader::setVSync(bool vsync) {
vsync_ = vsync;
if (device_ != nullptr && window_ != nullptr) {
SDL_SetGPUSwapchainParameters(device_, window_, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, vsync_ ? SDL_GPU_PRESENTMODE_VSYNC : SDL_GPU_PRESENTMODE_IMMEDIATE);
}
}
void SDL3GPUShader::setScaleMode(bool integer_scale) {
integer_scale_ = integer_scale;
}
// ---------------------------------------------------------------------------
// setOversample — cambia el factor SS; recrea texturas si ya está inicializado
// ---------------------------------------------------------------------------
void SDL3GPUShader::setOversample(int factor) {
const int NEW_FACTOR = std::max(1, factor);
if (NEW_FACTOR == oversample_) { return; }
oversample_ = NEW_FACTOR;
if (is_initialized_) {
reinitTexturesAndBuffer();
// scanline_strength se actualizará en el próximo setPostFXParams
}
}
// ---------------------------------------------------------------------------
// reinitTexturesAndBuffer — recrea scene_texture_ y upload_buffer_ con el
// tamaño actual (game × oversample_). No toca pipeline ni samplers.
// ---------------------------------------------------------------------------
auto SDL3GPUShader::reinitTexturesAndBuffer() -> bool {
if (device_ == nullptr) { return false; }
SDL_WaitForGPUIdle(device_);
if (scene_texture_ != nullptr) {
SDL_ReleaseGPUTexture(device_, scene_texture_);
scene_texture_ = nullptr;
}
if (upload_buffer_ != nullptr) {
SDL_ReleaseGPUTransferBuffer(device_, upload_buffer_);
upload_buffer_ = nullptr;
}
tex_width_ = game_width_ * oversample_;
tex_height_ = game_height_ * oversample_;
uniforms_.screen_height = static_cast<float>(tex_height_);
uniforms_.oversample = static_cast<float>(oversample_);
SDL_GPUTextureCreateInfo tex_info = {};
tex_info.type = SDL_GPU_TEXTURETYPE_2D;
tex_info.format = SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM;
tex_info.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER;
tex_info.width = static_cast<Uint32>(tex_width_);
tex_info.height = static_cast<Uint32>(tex_height_);
tex_info.layer_count_or_depth = 1;
tex_info.num_levels = 1;
scene_texture_ = SDL_CreateGPUTexture(device_, &tex_info);
if (scene_texture_ == nullptr) {
SDL_Log("SDL3GPUShader: reinit — failed to create scene texture: %s", SDL_GetError());
return false;
}
SDL_GPUTransferBufferCreateInfo tb_info = {};
tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
tb_info.size = static_cast<Uint32>(tex_width_ * tex_height_ * 4);
upload_buffer_ = SDL_CreateGPUTransferBuffer(device_, &tb_info);
if (upload_buffer_ == nullptr) {
SDL_Log("SDL3GPUShader: reinit — failed to create upload buffer: %s", SDL_GetError());
SDL_ReleaseGPUTexture(device_, scene_texture_);
scene_texture_ = nullptr;
return false;
}
SDL_Log("SDL3GPUShader: oversample %d → texture %dx%d", oversample_, tex_width_, tex_height_);
return true;
}
} // namespace Rendering

View File

@@ -0,0 +1,106 @@
#pragma once
#include <SDL3/SDL.h>
#include <SDL3/SDL_gpu.h>
#include "rendering/shader_backend.hpp"
// PostFX uniforms pushed to fragment stage each frame.
// Must match the MSL struct and GLSL uniform block layout.
// 12 floats = 48 bytes — meets Metal/Vulkan 16-byte alignment requirement.
struct PostFXUniforms {
float vignette_strength; // 0 = none, ~0.8 = subtle
float chroma_strength; // 0 = off, ~0.2 = subtle chromatic aberration
float scanline_strength; // 0 = off, 1 = full
float screen_height; // logical height in pixels (used by bleeding effect)
float mask_strength; // 0 = off, 1 = full phosphor dot mask
float gamma_strength; // 0 = off, 1 = full gamma 2.4/2.2 correction
float curvature; // 0 = flat, 1 = max barrel distortion
float bleeding; // 0 = off, 1 = max NTSC chrominance bleeding
float pixel_scale; // physical pixels per logical pixel (vh / tex_height_)
float time; // seconds since SDL init (SDL_GetTicks() / 1000.0f)
float oversample; // supersampling factor (1.0 = off, 3.0 = 3×SS)
float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz — keep struct at 48 bytes (3 × 16)
};
namespace Rendering {
/**
* @brief Backend de shaders usando SDL3 GPU API (Metal en macOS, Vulkan/SPIR-V en Win/Linux)
*
* Reemplaza el backend OpenGL para que los shaders PostFX funcionen en macOS.
* Pipeline: Surface pixels (CPU) → SDL_GPUTransferBuffer → SDL_GPUTexture (scene)
* → PostFX render pass → swapchain → present
*/
class SDL3GPUShader : public ShaderBackend {
public:
SDL3GPUShader() = default;
~SDL3GPUShader() override;
auto init(SDL_Window* window,
SDL_Texture* texture,
const std::string& vertex_source,
const std::string& fragment_source) -> bool override;
void render() override;
void setTextureSize(float width, float height) override {}
void cleanup() final; // Libera pipeline/texturas pero mantiene el device vivo
void destroy(); // Limpieza completa (device + swapchain); llamar solo al cerrar
[[nodiscard]] auto isHardwareAccelerated() const -> bool override { return is_initialized_; }
// Sube píxeles ARGB8888 desde CPU; llamado antes de render()
void uploadPixels(const Uint32* pixels, int width, int height) override;
// Actualiza los parámetros de intensidad de los efectos PostFX
void setPostFXParams(const PostFXParams& p) override;
// Activa/desactiva VSync en el swapchain
void setVSync(bool vsync) override;
// Activa/desactiva escalado entero (integer scale)
void setScaleMode(bool integer_scale) override;
// Establece factor de supersampling (1 = off, 3 = 3×SS)
void setOversample(int factor) override;
private:
static auto createShaderMSL(SDL_GPUDevice* device,
const char* msl_source,
const char* entrypoint,
SDL_GPUShaderStage stage,
Uint32 num_samplers,
Uint32 num_uniform_buffers) -> SDL_GPUShader*;
static auto createShaderSPIRV(SDL_GPUDevice* device,
const uint8_t* spv_code,
size_t spv_size,
const char* entrypoint,
SDL_GPUShaderStage stage,
Uint32 num_samplers,
Uint32 num_uniform_buffers) -> SDL_GPUShader*;
auto createPipeline() -> bool;
auto reinitTexturesAndBuffer() -> bool; // Recrea textura y buffer con oversample actual
SDL_Window* window_ = nullptr;
SDL_GPUDevice* device_ = nullptr;
SDL_GPUGraphicsPipeline* pipeline_ = nullptr;
SDL_GPUTexture* scene_texture_ = nullptr;
SDL_GPUTransferBuffer* upload_buffer_ = nullptr;
SDL_GPUSampler* sampler_ = nullptr; // NEAREST — para path sin supersampling
SDL_GPUSampler* linear_sampler_ = nullptr; // LINEAR — para path con supersampling
PostFXUniforms uniforms_{.vignette_strength = 0.6F, .chroma_strength = 0.15F, .scanline_strength = 0.7F, .screen_height = 192.0F, .pixel_scale = 1.0F, .oversample = 1.0F};
int game_width_ = 0; // Dimensiones originales del canvas (sin SS)
int game_height_ = 0;
int tex_width_ = 0; // Dimensiones de la textura GPU (game × oversample_)
int tex_height_ = 0;
int oversample_ = 1; // Factor SS actual (1 o 3)
float baked_scanline_strength_ = 0.0F; // Guardado para hornear en CPU
bool is_initialized_ = false;
bool vsync_ = true;
bool integer_scale_ = false;
};
} // namespace Rendering

View File

@@ -6,6 +6,20 @@
namespace Rendering {
/**
* @brief Parámetros de intensidad de los efectos PostFX
*/
struct PostFXParams {
float vignette = 0.0F;
float scanlines = 0.0F;
float chroma = 0.0F;
float mask = 0.0F;
float gamma = 0.0F;
float curvature = 0.0F;
float bleeding = 0.0F;
float flicker = 0.0F;
};
/**
* @brief Interfaz abstracta para backends de renderizado con shaders
*
@@ -51,6 +65,12 @@ namespace Rendering {
* @return true si usa aceleración (OpenGL/Metal/Vulkan)
*/
[[nodiscard]] virtual auto isHardwareAccelerated() const -> bool = 0;
virtual void uploadPixels(const Uint32* /*pixels*/, int /*width*/, int /*height*/) {}
virtual void setPostFXParams(const PostFXParams& /*p*/) {}
virtual void setVSync(bool /*vsync*/) {}
virtual void setScaleMode(bool /*integer_scale*/) {}
virtual void setOversample(int /*factor*/) {}
};
} // namespace Rendering

View File

@@ -1,8 +1,9 @@
#include "screen.hpp"
#include <SDL3/SDL.h> // Para SDL_SetRenderTarget, SDL_RenderTexture, SDL_SetRenderDrawColor, SDL_SetRenderVSync, SDL_LogCategory, SDL_GetError, SDL_LogError, SDL_LogInfo, SDL_RendererLogicalPresentation, SDL_SetRenderLogicalPresentation, SDL_CreateTexture, SDL_DestroyTexture, SDL_DestroyWindow, SDL_GetDisplayName, SDL_GetTicks, SDL_Quit, SDL_RENDERER_VSYNC_DISABLED, SDL_RenderClear, SDL_CreateRenderer, SDL_CreateWindow, SDL_DestroyRenderer, SDL_DisplayID, SDL_FRect, SDL_GetCurrentDisplayMode, SDL_GetDisplays, SDL_GetRenderTarget, SDL_GetWindowPosition, SDL_GetWindowSize, SDL_Init, SDL_LogWarn, SDL_PixelFormat, SDL_RenderFillRect, SDL_RenderPresent, SDL_SetHint, SDL_SetRenderDrawBlendMode, SDL_SetTextureScaleMode, SDL_SetWindowFullscreen, SDL_SetWindowPosition, SDL_SetWindowSize, SDL_TextureAccess, SDL_free, SDL_BLENDMODE_BLEND, SDL_HINT_RENDER_DRIVER, SDL_INIT_VIDEO, SDL_ScaleMode, SDL_WINDOW_FULLSCREEN, SDL_WINDOW_OPENGL, SDL_WindowFlags
#include <SDL3/SDL.h> // Para SDL_SetRenderTarget, SDL_RenderTexture, SDL_SetRenderDrawColor, SDL_SetRenderVSync, SDL_LogCategory, SDL_GetError, SDL_LogError, SDL_LogInfo, SDL_RendererLogicalPresentation, SDL_SetRenderLogicalPresentation, SDL_CreateTexture, SDL_DestroyTexture, SDL_DestroyWindow, SDL_GetDisplayName, SDL_GetTicks, SDL_Quit, SDL_RENDERER_VSYNC_DISABLED, SDL_RenderClear, SDL_CreateRenderer, SDL_CreateWindow, SDL_DestroyRenderer, SDL_DisplayID, SDL_FRect, SDL_GetCurrentDisplayMode, SDL_GetDisplays, SDL_GetRenderTarget, SDL_GetWindowPosition, SDL_GetWindowSize, SDL_Init, SDL_LogWarn, SDL_PixelFormat, SDL_RenderFillRect, SDL_RenderPresent, SDL_SetHint, SDL_SetRenderDrawBlendMode, SDL_SetTextureScaleMode, SDL_SetWindowFullscreen, SDL_SetWindowPosition, SDL_SetWindowSize, SDL_TextureAccess, SDL_free, SDL_BLENDMODE_BLEND, SDL_HINT_RENDER_DRIVER, SDL_INIT_VIDEO, SDL_ScaleMode, SDL_WINDOW_FULLSCREEN, SDL_WindowFlags
#include <algorithm> // Para min, max
#include <cstring> // Para memcpy
#include <memory> // Para allocator, shared_ptr, unique_ptr, __shared_ptr_access, make_shared, make_unique
#include <string> // Para basic_string, operator+, char_traits, to_string, string
#include <vector> // Para vector
@@ -11,8 +12,7 @@
#include "mouse.hpp" // Para updateCursorVisibility
#include "options.hpp" // Para Video, video, Window, window
#include "param.hpp" // Para Param, param, ParamGame, ParamDebug
#include "rendering/opengl/opengl_shader.hpp" // Para OpenGLShader
#include "shader_backend.hpp" // Para ShaderBackend
#include "rendering/sdl3gpu/sdl3gpu_shader.hpp" // Para SDL3GPUShader
#include "text.hpp" // Para Text
#include "texture.hpp" // Para Texture
#include "ui/logger.hpp" // Para info
@@ -23,7 +23,10 @@
Screen* Screen::instance = nullptr;
// Inicializa la instancia única del singleton
void Screen::init() { Screen::instance = new Screen(); }
void Screen::init() {
Screen::instance = new Screen();
Screen::initShaders(); // Llamar aquí para que Screen::get() ya devuelva la instancia
}
// Libera la instancia
void Screen::destroy() { delete Screen::instance; }
@@ -44,9 +47,12 @@ Screen::Screen()
initSDLVideo();
// Crea la textura de destino
game_canvas_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, param.game.width, param.game.height);
game_canvas_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, param.game.width, param.game.height);
SDL_SetTextureScaleMode(game_canvas_, SDL_SCALEMODE_NEAREST);
// Inicializar buffer de píxeles para SDL3GPU
pixel_buffer_.resize(static_cast<size_t>(param.game.width) * static_cast<size_t>(param.game.height));
// Crea el objeto de texto
createText();
@@ -63,8 +69,6 @@ Screen::Screen()
SDL_RenderClear(renderer_);
SDL_RenderPresent(renderer_);
// Ahora sí inicializar los shaders
initShaders();
}
// Destructor
@@ -101,15 +105,37 @@ void Screen::coreRender() {
// Renderiza el contenido del game_canvas_
void Screen::renderPresent() {
#ifndef NO_SHADERS
if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
// Leer píxeles de game_canvas_ con la API SDL3 (devuelve SDL_Surface*)
SDL_SetRenderTarget(renderer_, game_canvas_);
SDL_Surface* surface = SDL_RenderReadPixels(renderer_, nullptr);
if (surface != nullptr) {
if (surface->format == SDL_PIXELFORMAT_ARGB8888) {
std::memcpy(pixel_buffer_.data(), surface->pixels,
pixel_buffer_.size() * sizeof(Uint32));
} else {
SDL_Surface* converted = SDL_ConvertSurface(surface, SDL_PIXELFORMAT_ARGB8888);
if (converted != nullptr) {
std::memcpy(pixel_buffer_.data(), converted->pixels,
pixel_buffer_.size() * sizeof(Uint32));
SDL_DestroySurface(converted);
}
}
SDL_DestroySurface(surface);
}
SDL_SetRenderTarget(renderer_, nullptr);
// Subir a GPU y presentar con PostFX
shader_backend_->uploadPixels(pixel_buffer_.data(), param.game.width, param.game.height);
shader_backend_->render();
return;
}
#endif
// Fallback: SDL_Renderer
SDL_SetRenderTarget(renderer_, nullptr);
clean();
if (Options::video.shaders && shader_backend_) {
shader_backend_->render();
} else {
SDL_RenderTexture(renderer_, game_canvas_, nullptr, nullptr);
SDL_RenderPresent(renderer_);
}
SDL_RenderTexture(renderer_, game_canvas_, nullptr, nullptr);
SDL_RenderPresent(renderer_);
}
// Establece el modo de video
@@ -230,61 +256,32 @@ void Screen::renderInfo() const {
}
}
#endif
// Carga el contenido de los archivos GLSL
void Screen::loadShaders() {
if (vertex_shader_source_.empty()) {
// Detectar si necesitamos OpenGL ES (Raspberry Pi)
// Intentar cargar versión ES primero si existe
std::string vertex_file = "crtpi_vertex_es.glsl";
auto data = Asset::get()->loadData(vertex_file);
if (data.empty()) {
// Si no existe versión ES, usar versión Desktop
vertex_file = "crtpi_vertex.glsl";
data = Asset::get()->loadData(vertex_file);
// SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Usando shaders OpenGL Desktop 3.3");
Logger::info("Usando shaders OpenGL Desktop 3.3");
} else {
// SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Usando shaders OpenGL ES 3.0 (Raspberry Pi)");
Logger::info("Usando shaders OpenGL ES 3.0 (Raspberry Pi)");
}
if (!data.empty()) {
vertex_shader_source_ = std::string(data.begin(), data.end());
}
// Inicializa los shaders (SDL3GPU)
void Screen::initShaders() {
#ifndef NO_SHADERS
auto* self = Screen::get();
if (self == nullptr) {
SDL_Log("Screen::initShaders: instance is null, skipping");
return;
}
if (fragment_shader_source_.empty()) {
// Intentar cargar versión ES primero si existe
std::string fragment_file = "crtpi_fragment_es.glsl";
auto data = Asset::get()->loadData(fragment_file);
if (data.empty()) {
// Si no existe versión ES, usar versión Desktop
fragment_file = "crtpi_fragment.glsl";
data = Asset::get()->loadData(fragment_file);
}
if (!data.empty()) {
fragment_shader_source_ = std::string(data.begin(), data.end());
}
if (!self->shader_backend_) {
self->shader_backend_ = std::make_unique<Rendering::SDL3GPUShader>();
}
if (!self->shader_backend_->isHardwareAccelerated()) {
const bool ok = self->shader_backend_->init(self->window_, self->game_canvas_, "", "");
SDL_Log("Screen::initShaders: SDL3GPUShader::init() = %s", ok ? "OK" : "FAILED");
}
SDL_Log("Screen::initShaders: presets=%d current=%d postfx=%s",
static_cast<int>(Options::postfx_presets.size()),
Options::current_postfx_preset,
Options::video.postfx ? "ON" : "OFF");
self->applyCurrentPostFXPreset();
#endif
}
// Inicializa los shaders
void Screen::initShaders() {
#ifndef __APPLE__
if (Options::video.shaders) {
loadShaders();
if (!shader_backend_) {
shader_backend_ = std::make_unique<Rendering::OpenGLShader>();
}
shader_backend_->init(window_, game_canvas_, vertex_shader_source_, fragment_shader_source_);
}
#else
// En macOS, OpenGL está deprecated y rinde mal
// TODO: Implementar backend de Metal para shaders en macOS
Logger::info("WARNING: Shaders no disponibles en macOS (OpenGL deprecated). Usa Metal backend.", Logger::YELLOW);
#endif
// Inicializa PostFX (alias de initShaders)
void Screen::initPostFX() {
initShaders();
}
// Calcula el tamaño de la ventana
@@ -350,32 +347,13 @@ auto Screen::initSDLVideo() -> bool {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Warning: Failed to set Metal hint!");
}
#else
// Configurar hint de render driver
if (!SDL_SetHint(SDL_HINT_RENDER_DRIVER, "opengl")) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Warning: Failed to set OpenGL hint!");
}
#ifdef _WIN32
// Windows: Pedir explícitamente OpenGL 3.3 Core Profile
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
// SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Solicitando OpenGL 3.3 Core Profile");
Logger::info("Solicitando OpenGL 3.3 Core Profile");
#else
// Linux: Dejar que SDL elija (Desktop 3.3 en PC, ES 3.0 en RPi automáticamente)
// SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Usando OpenGL por defecto del sistema");
Logger::info("Usando OpenGL por defecto del sistema");
#endif
#endif
// Crear ventana
#ifdef __APPLE__
SDL_WindowFlags window_flags = SDL_WINDOW_METAL;
#else
SDL_WindowFlags window_flags = SDL_WINDOW_OPENGL;
SDL_WindowFlags window_flags = 0;
#endif
if (Options::video.fullscreen) {
window_flags |= SDL_WINDOW_FULLSCREEN;
@@ -465,22 +443,79 @@ void Screen::getDisplayInfo() {
}
}
// Alterna entre activar y desactivar los shaders
// Alterna entre activar y desactivar los shaders (backward compat)
void Screen::toggleShaders() {
Options::video.shaders = !Options::video.shaders;
initShaders();
Screen::togglePostFX();
}
// Alterna entre activar y desactivar los efectos PostFX
void Screen::togglePostFX() {
Options::video.postfx = !Options::video.postfx;
auto* self = Screen::get();
if (self != nullptr) {
self->applyCurrentPostFXPreset();
}
}
// Avanza al siguiente preset PostFX
void Screen::nextPostFXPreset() {
if (Options::postfx_presets.empty()) { return; }
Options::current_postfx_preset = (Options::current_postfx_preset + 1) % static_cast<int>(Options::postfx_presets.size());
auto* self = Screen::get();
if (self != nullptr) {
self->applyCurrentPostFXPreset();
}
}
// Alterna entre activar y desactivar el supersampling 3x
void Screen::toggleSupersampling() {
Options::video.supersampling = !Options::video.supersampling;
auto* self = Screen::get();
if (self != nullptr && self->shader_backend_) {
self->shader_backend_->setOversample(Options::video.supersampling ? 3 : 1);
}
}
// Aplica el preset PostFX activo al backend
void Screen::applyCurrentPostFXPreset() {
if (!shader_backend_) { return; }
Rendering::PostFXParams p{};
if (Options::video.postfx && !Options::postfx_presets.empty()) {
const auto& preset = Options::postfx_presets.at(static_cast<size_t>(Options::current_postfx_preset));
p.vignette = preset.vignette;
p.scanlines = preset.scanlines;
p.chroma = preset.chroma;
p.mask = preset.mask;
p.gamma = preset.gamma;
p.curvature = preset.curvature;
p.bleeding = preset.bleeding;
p.flicker = preset.flicker;
SDL_Log("Screen::applyCurrentPostFXPreset: preset='%s' scan=%.2f vign=%.2f chroma=%.2f",
preset.name.c_str(), p.scanlines, p.vignette, p.chroma);
} else {
SDL_Log("Screen::applyCurrentPostFXPreset: PostFX=%s presets=%d → passthrough",
Options::video.postfx ? "ON" : "OFF",
static_cast<int>(Options::postfx_presets.size()));
}
shader_backend_->setPostFXParams(p);
}
// Alterna entre activar y desactivar el escalado entero
void Screen::toggleIntegerScale() {
Options::video.integer_scale = !Options::video.integer_scale;
SDL_SetRenderLogicalPresentation(renderer_, param.game.width, param.game.height, Options::video.integer_scale ? SDL_LOGICAL_PRESENTATION_INTEGER_SCALE : SDL_LOGICAL_PRESENTATION_LETTERBOX);
if (shader_backend_) {
shader_backend_->setScaleMode(Options::video.integer_scale);
}
}
// Alterna entre activar y desactivar el V-Sync
void Screen::toggleVSync() {
Options::video.vsync = !Options::video.vsync;
SDL_SetRenderVSync(renderer_, Options::video.vsync ? 1 : SDL_RENDERER_VSYNC_DISABLED);
if (shader_backend_) {
shader_backend_->setVSync(Options::video.vsync);
}
}
// Establece el estado del V-Sync

View File

@@ -4,6 +4,7 @@
#include <memory> // Para shared_ptr
#include <string> // Para string
#include <vector> // Para vector
#include "color.hpp" // Para Color
#include "options.hpp" // Para VideoOptions, video
@@ -40,12 +41,16 @@ class Screen {
auto decWindowSize() -> bool; // Reduce el tamaño de la ventana
auto incWindowSize() -> bool; // Aumenta el tamaño de la ventana
void applySettings(); // Aplica los valores de las opciones
static void initShaders(); // Inicializa los shaders
static void initShaders(); // Inicializa los shaders (SDL3GPU)
static void initPostFX(); // Inicializa PostFX (alias de initShaders)
// --- Efectos visuales ---
void shake(int desp = 2, float delay_s = 0.05F, float duration_s = 0.133F) { shake_effect_.enable(src_rect_, dst_rect_, desp, delay_s, duration_s); } // Agita la pantalla (tiempo en segundos)
void flash(Color color, float duration_s = 0.167F, float delay_s = 0.0F) { flash_effect_ = FlashEffect(true, duration_s, delay_s, color); } // Pone la pantalla de color (tiempo en segundos)
static void toggleShaders(); // Alterna entre activar y desactivar los shaders
static void toggleShaders(); // Alterna entre activar y desactivar los shaders (backward compat)
static void togglePostFX(); // Alterna entre activar y desactivar los efectos PostFX
static void nextPostFXPreset(); // Avanza al siguiente preset PostFX
static void toggleSupersampling(); // Alterna entre activar y desactivar el supersampling 3x
void toggleIntegerScale(); // Alterna entre activar y desactivar el escalado entero
void toggleVSync(); // Alterna entre activar y desactivar el V-Sync
void setVSync(bool enabled); // Establece el estado del V-Sync
@@ -214,14 +219,13 @@ class Screen {
ServiceMenu* service_menu_; // Objeto para mostrar el menú de servicio
Notifier* notifier_; // Objeto para mostrar las notificaciones por pantalla
std::shared_ptr<Text> text_; // Objeto para escribir texto en pantalla
std::unique_ptr<Rendering::ShaderBackend> shader_backend_; // Backend de shaders (OpenGL/Metal/Vulkan)
std::unique_ptr<Rendering::ShaderBackend> shader_backend_; // Backend de shaders (SDL3GPU)
// --- Variables de estado ---
SDL_FRect src_rect_; // Coordenadas de origen para dibujar la textura del juego
SDL_FRect dst_rect_; // Coordenadas destino para dibujar la textura del juego
std::string vertex_shader_source_; // Almacena el vertex shader
std::string fragment_shader_source_; // Almacena el fragment shader
FPS fps_; // Gestión de frames por segundo
SDL_FRect src_rect_; // Coordenadas de origen para dibujar la textura del juego
SDL_FRect dst_rect_; // Coordenadas destino para dibujar la textura del juego
std::vector<Uint32> pixel_buffer_; // Buffer de píxeles para SDL_RenderReadPixels
FPS fps_; // Gestión de frames por segundo
FlashEffect flash_effect_; // Efecto de flash en pantalla
ShakeEffect shake_effect_; // Efecto de agitar la pantalla
bool attenuate_effect_ = false; // Indica si la pantalla ha de estar atenuada
@@ -231,17 +235,17 @@ class Screen {
#endif
// --- Métodos internos ---
auto initSDLVideo() -> bool; // Arranca SDL VIDEO y crea la ventana
void renderFlash(); // Dibuja el efecto de flash en la pantalla
void renderShake(); // Aplica el efecto de agitar la pantalla
void renderInfo() const; // Muestra información por pantalla
void renderPresent(); // Selecciona y ejecuta el método de renderizado adecuado
void loadShaders(); // Carga el contenido del archivo GLSL
void adjustWindowSize(); // Calcula el tamaño de la ventana
void getDisplayInfo(); // Obtiene información sobre la pantalla
void renderOverlays(); // Renderiza todos los overlays y efectos
void renderAttenuate(); // Atenúa la pantalla
void createText(); // Crea el objeto de texto
auto initSDLVideo() -> bool; // Arranca SDL VIDEO y crea la ventana
void renderFlash(); // Dibuja el efecto de flash en la pantalla
void renderShake(); // Aplica el efecto de agitar la pantalla
void renderInfo() const; // Muestra información por pantalla
void renderPresent(); // Selecciona y ejecuta el método de renderizado adecuado
void applyCurrentPostFXPreset(); // Aplica el preset PostFX activo al backend
void adjustWindowSize(); // Calcula el tamaño de la ventana
void getDisplayInfo(); // Obtiene información sobre la pantalla
void renderOverlays(); // Renderiza todos los overlays y efectos
void renderAttenuate(); // Atenúa la pantalla
void createText(); // Crea el objeto de texto
// --- Constructores y destructor privados (singleton) ---
Screen(); // Constructor privado