6 Commits

Author SHA1 Message Date
06457654f4 postfx subpixel 2026-03-21 14:41:51 +01:00
e9fc2e8fa0 normalitzat el caption de la finestra 2026-03-21 14:15:40 +01:00
23863c02a6 millores en els presets 2026-03-21 14:12:11 +01:00
6996b3a82a presets en postfx 2026-03-21 13:57:18 +01:00
2b2eb31c67 treballant en postfx 2026-03-21 13:31:42 +01:00
8aad52f33f treballant en makefile 2026-03-21 12:32:05 +01:00
26 changed files with 950 additions and 105 deletions

View File

@@ -111,9 +111,17 @@ set(APP_SOURCES
)
# Fuentes del sistema de renderizado
set(RENDERING_SOURCES
source/core/rendering/opengl/opengl_shader.cpp
)
# En macOS usamos SDL3 GPU (Metal), no OpenGL
if(APPLE)
set(RENDERING_SOURCES
source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp
)
else()
set(RENDERING_SOURCES
source/core/rendering/opengl/opengl_shader.cpp
source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp
)
endif()
# Fuentes de debug (solo en modo Debug)
set(DEBUG_SOURCES
@@ -161,8 +169,10 @@ elseif(UNIX AND NOT APPLE)
target_compile_definitions(${PROJECT_NAME} PRIVATE LINUX_BUILD)
endif()
# Configuración común para OpenGL
if(NOT WIN32)
# Configuración común para OpenGL (no requerido en macOS: usamos SDL3 GPU API / Metal)
if(WIN32)
target_link_libraries(${PROJECT_NAME} PRIVATE opengl32)
elseif(NOT APPLE)
find_package(OpenGL REQUIRED)
if(OPENGL_FOUND)
message(STATUS "OpenGL encontrado: ${OPENGL_LIBRARIES}")

View File

@@ -13,7 +13,7 @@ TARGET_NAME := jaildoctors_dilemma
TARGET_FILE := $(DIR_BIN)$(TARGET_NAME)
APP_NAME := JailDoctor's Dilemma
DIST_DIR := dist
RELEASE_FOLDER := jdd_release
RELEASE_FOLDER := dist/_tmp
RELEASE_FILE := $(RELEASE_FOLDER)/$(TARGET_NAME)
RESOURCE_FILE := release/windows/jdd.res
@@ -76,6 +76,8 @@ APP_SOURCES := \
source/core/rendering/text.cpp \
source/core/rendering/texture.cpp \
source/core/rendering/gif.cpp \
source/core/rendering/pixel_reveal.cpp \
source/core/rendering/surface_dissolve_sprite.cpp \
source/core/rendering/opengl/opengl_shader.cpp \
source/core/resources/resource_list.cpp \
source/core/resources/resource_cache.cpp \
@@ -184,13 +186,6 @@ windows:
g++ $(ALL_SOURCES) $(RESOURCE_FILE) $(INCLUDES) $(CXXFLAGS) $(LDFLAGS) -o "$(WIN_TARGET_FILE).exe"
strip -s -R .comment -R .gnu.version "$(WIN_TARGET_FILE).exe" --strip-unneeded
windows_debug:
@echo off
@echo Generando version.h...
@powershell -Command "$$GIT_HASH = (git rev-parse --short=7 HEAD 2>$$null); if (-not $$GIT_HASH) { $$GIT_HASH = 'unknown' }; (Get-Content source/version.h.in) -replace '@GIT_HASH@', $$GIT_HASH | Set-Content source/version.h"
@echo Compilando version debug para Windows: "$(WIN_TARGET_FILE)_debug.exe"
g++ $(ALL_SOURCES) $(INCLUDES) -DDEBUG -DVERBOSE $(CXXFLAGS_DEBUG) $(LDFLAGS) -o "$(WIN_TARGET_FILE)_debug.exe"
windows_release:
@$(MAKE) pack_tool
@$(MAKE) resources.pack
@@ -201,7 +196,8 @@ windows_release:
@echo "Generando version.h..."
@powershell -Command "$$GIT_HASH = (git rev-parse --short=7 HEAD 2>$$null); if (-not $$GIT_HASH) { $$GIT_HASH = 'unknown' }; (Get-Content source/version.h.in) -replace '@GIT_HASH@', $$GIT_HASH | Set-Content source/version.h"
# Crea carpeta temporal 'RELEASE_FOLDER'
# Crea carpeta de distribución y carpeta temporal 'RELEASE_FOLDER'
powershell if (-not (Test-Path "$(DIST_DIR)")) {New-Item "$(DIST_DIR)" -ItemType Directory}
powershell if (Test-Path "$(RELEASE_FOLDER)") {Remove-Item "$(RELEASE_FOLDER)" -Recurse -Force}
powershell if (-not (Test-Path "$(RELEASE_FOLDER)")) {New-Item "$(RELEASE_FOLDER)" -ItemType Directory}
@@ -220,7 +216,6 @@ windows_release:
strip -s -R .comment -R .gnu.version "$(WIN_RELEASE_FILE).exe" --strip-unneeded
# Crea el fichero .zip
powershell if (-not (Test-Path "$(DIST_DIR)")) {New-Item "$(DIST_DIR)" -ItemType Directory}
powershell if (Test-Path "$(WINDOWS_RELEASE)") {Remove-Item "$(WINDOWS_RELEASE)"}
powershell Compress-Archive -Path "$(RELEASE_FOLDER)"/* -DestinationPath "$(WINDOWS_RELEASE)"
@echo Release creado: $(WINDOWS_RELEASE)
@@ -237,12 +232,6 @@ macos:
@echo "Compilando para macOS: $(TARGET_NAME)"
clang++ $(ALL_SOURCES) $(INCLUDES) $(CXXFLAGS) $(LDFLAGS) -o "$(TARGET_FILE)"
macos_debug:
@GIT_HASH=$$(git rev-parse --short=7 HEAD 2>/dev/null || echo "unknown"); \
sed "s/@GIT_HASH@/$$GIT_HASH/g" source/version.h.in > source/version.h
@echo "Compilando version debug para macOS: $(TARGET_NAME)_debug"
clang++ $(ALL_SOURCES) $(INCLUDES) -DDEBUG -DVERBOSE $(CXXFLAGS_DEBUG) $(LDFLAGS) -o "$(TARGET_FILE)_debug"
macos_release:
@$(MAKE) pack_tool
@$(MAKE) resources.pack
@@ -258,21 +247,18 @@ macos_release:
# Elimina datos de compilaciones anteriores
$(RMDIR) "$(RELEASE_FOLDER)"
$(RMDIR) Frameworks
$(RMFILE) tmp.dmg
$(RMFILE) "$(DIST_DIR)"/rw.*
# Crea la carpeta temporal para hacer el trabajo y las carpetas obligatorias para crear una app de macOS
$(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Frameworks"
$(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS"
$(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
$(MKDIR) Frameworks
# Copia carpetas y ficheros
cp resources.pack "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
cp -R release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64/SDL3.framework "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Frameworks"
cp -R release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64/SDL3.framework Frameworks
cp release/icons/*.icns "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
cp release/macos/Info.plist "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents"
cp LICENSE "$(RELEASE_FOLDER)"
@@ -285,7 +271,7 @@ macos_release:
sed -i '' '/<key>CFBundleVersion<\/key>/{n;s|<string>.*</string>|<string>'"$$RAW_VERSION"'</string>|;}' "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Info.plist"
# Compila la versión para procesadores Intel
clang++ $(ALL_SOURCES) $(INCLUDES) -DMACOS_BUNDLE -DRELEASE_BUILD -std=$(CPP_STANDARD) -Wall -Os -framework SDL3 -F ./Frameworks -framework OpenGL -Wno-deprecated -ffunction-sections -fdata-sections -o "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)" -rpath @executable_path/../Frameworks/ -target x86_64-apple-macos10.15
clang++ $(ALL_SOURCES) $(INCLUDES) -DMACOS_BUNDLE -DRELEASE_BUILD -std=$(CPP_STANDARD) -Wall -Os -framework SDL3 -F release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64 -framework OpenGL -Wno-deprecated -ffunction-sections -fdata-sections -o "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)" -rpath @executable_path/../Frameworks/ -target x86_64-apple-macos10.15
# Firma la aplicación
codesign --deep --force --sign - --timestamp=none "$(RELEASE_FOLDER)/$(APP_NAME).app"
@@ -309,7 +295,7 @@ macos_release:
@echo "Release Intel creado: $(MACOS_INTEL_RELEASE)"
# Compila la versión para procesadores Apple Silicon
clang++ $(ALL_SOURCES) $(INCLUDES) -DMACOS_BUNDLE -DRELEASE_BUILD -std=$(CPP_STANDARD) -Wall -Os -framework SDL3 -F ./Frameworks -framework OpenGL -Wno-deprecated -ffunction-sections -fdata-sections -o "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)" -rpath @executable_path/../Frameworks/ -target arm64-apple-macos11
clang++ $(ALL_SOURCES) $(INCLUDES) -DMACOS_BUNDLE -DRELEASE_BUILD -std=$(CPP_STANDARD) -Wall -Os -framework SDL3 -F release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64 -framework OpenGL -Wno-deprecated -ffunction-sections -fdata-sections -o "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)" -rpath @executable_path/../Frameworks/ -target arm64-apple-macos11
# Firma la aplicación
codesign --deep --force --sign - --timestamp=none "$(RELEASE_FOLDER)/$(APP_NAME).app"
@@ -332,8 +318,8 @@ macos_release:
@echo "Release Apple Silicon creado: $(MACOS_APPLE_SILICON_RELEASE)"
# Elimina las carpetas temporales
$(RMDIR) Frameworks
$(RMDIR) "$(RELEASE_FOLDER)"
$(RMFILE) "$(DIST_DIR)"/rw.*
# ==============================================================================
# COMPILACIÓN PARA LINUX
@@ -345,12 +331,6 @@ linux:
g++ $(ALL_SOURCES) $(INCLUDES) $(CXXFLAGS) $(LDFLAGS) -o "$(TARGET_FILE)"
strip -s -R .comment -R .gnu.version "$(TARGET_FILE)" --strip-unneeded
linux_debug:
@GIT_HASH=$$(git rev-parse --short=7 HEAD 2>/dev/null || echo "unknown"); \
sed "s/@GIT_HASH@/$$GIT_HASH/g" source/version.h.in > source/version.h
@echo "Compilando version debug para Linux: $(TARGET_NAME)_debug"
g++ $(ALL_SOURCES) $(INCLUDES) -DDEBUG -DVERBOSE $(CXXFLAGS_DEBUG) $(LDFLAGS) -o "$(TARGET_FILE)_debug"
linux_release:
@$(MAKE) pack_tool
@$(MAKE) resources.pack
@@ -398,13 +378,10 @@ help:
@echo "Makefile para JailDoctor's Dilemma"
@echo "Comandos disponibles:"
@echo " windows - Compilar para Windows"
@echo " windows_debug - Compilar debug para Windows"
@echo " windows_release - Crear release completo para Windows"
@echo " linux - Compilar para Linux"
@echo " linux_debug - Compilar debug para Linux"
@echo " linux_release - Crear release completo para Linux"
@echo " macos - Compilar para macOS"
@echo " macos_debug - Compilar debug para macOS"
@echo " macos_release - Crear release completo para macOS"
@echo " pack_tool - Compilar herramienta de empaquetado"
@echo " resources.pack - Generar pack de recursos desde data/"
@@ -413,4 +390,4 @@ help:
FORCE:
.PHONY: windows windows_debug windows_release macos macos_debug macos_release linux linux_debug linux_release pack_tool resources.pack show_version help
.PHONY: windows windows_release macos macos_release linux linux_release pack_tool resources.pack show_version help

View File

@@ -90,6 +90,10 @@ assets:
path: ${SYSTEM_FOLDER}/cheevos.bin
required: false
absolute: true
- type: DATA
path: ${SYSTEM_FOLDER}/postfx.yaml
required: false
absolute: true
# ROOMS
rooms:

View File

@@ -31,6 +31,10 @@ out vec4 FragColor;
// Uniforms
uniform sampler2D Texture;
uniform vec2 TextureSize;
uniform float uVignette; // 0 = sin viñeta, 1 = máxima
uniform float uScanlines; // 0 = desactivadas, 1 = plenas
uniform float uChroma; // 0 = sin aberración, 1 = máxima
uniform float uOutputHeight; // altura del viewport en pixels de pantalla
#if defined(CURVATURE)
vec2 Distort(vec2 coord)
@@ -100,8 +104,20 @@ void main()
#else
float tempY = floor(texcoordInPixels.y) + 0.5;
float yCoord = tempY / TextureSize.y;
// Scanline en espacio de pantalla (subpíxel)
float scaleY = uOutputHeight / TextureSize.y;
float screenY = vTexCoord.y * uOutputHeight;
float posInRow = mod(screenY, scaleY);
float scanLineDY = posInRow / scaleY - 0.5;
float localFilterWidth = 1.0 / scaleY;
float scanLineWeight = CalcScanLineWeight(scanLineDY);
scanLineWeight += CalcScanLineWeight(scanLineDY - localFilterWidth);
scanLineWeight += CalcScanLineWeight(scanLineDY + localFilterWidth);
scanLineWeight *= 0.3333333;
// Phosphor blur en espacio textura (sin cambios)
float dy = texcoordInPixels.y - tempY;
float scanLineWeight = CalcScanLine(dy);
float signY = sign(dy);
dy = dy * dy;
dy = dy * dy;
@@ -111,7 +127,11 @@ void main()
vec2 tc = vec2(texcoord.x, yCoord + dy);
#endif
vec3 colour = texture(Texture, tc).rgb;
float ca = uChroma * 0.005;
vec3 colour;
colour.r = texture(Texture, tc + vec2(ca, 0.0)).r;
colour.g = texture(Texture, tc).g;
colour.b = texture(Texture, tc - vec2(ca, 0.0)).b;
#if defined(SCANLINES)
#if defined(GAMMA)
@@ -122,7 +142,7 @@ void main()
#endif
#endif
scanLineWeight *= BLOOM_FACTOR;
colour *= scanLineWeight;
colour *= mix(1.0, scanLineWeight, uScanlines);
#if defined(GAMMA)
#if defined(FAKE_GAMMA)
@@ -133,6 +153,12 @@ void main()
#endif
#endif
if (uVignette > 0.0) {
vec2 uv = texcoord - vec2(0.5);
float vig = 1.0 - dot(uv, uv) * uVignette * 4.0;
colour *= clamp(vig, 0.0, 1.0);
}
#if MASK_TYPE == 0
FragColor = vec4(colour, 1.0);
#elif MASK_TYPE == 1

View File

@@ -34,6 +34,10 @@ out vec4 FragColor;
// Uniforms
uniform sampler2D Texture;
uniform vec2 TextureSize;
uniform float uVignette; // 0 = sin viñeta, 1 = máxima
uniform float uScanlines; // 0 = desactivadas, 1 = plenas
uniform float uChroma; // 0 = sin aberración, 1 = máxima
uniform float uOutputHeight; // altura del viewport en pixels de pantalla
#if defined(CURVATURE)
vec2 Distort(vec2 coord)
@@ -103,8 +107,20 @@ void main()
#else
float tempY = floor(texcoordInPixels.y) + 0.5;
float yCoord = tempY / TextureSize.y;
// Scanline en espacio de pantalla (subpíxel)
float scaleY = uOutputHeight / TextureSize.y;
float screenY = vTexCoord.y * uOutputHeight;
float posInRow = mod(screenY, scaleY);
float scanLineDY = posInRow / scaleY - 0.5;
float localFilterWidth = 1.0 / scaleY;
float scanLineWeight = CalcScanLineWeight(scanLineDY);
scanLineWeight += CalcScanLineWeight(scanLineDY - localFilterWidth);
scanLineWeight += CalcScanLineWeight(scanLineDY + localFilterWidth);
scanLineWeight *= 0.3333333;
// Phosphor blur en espacio textura (sin cambios)
float dy = texcoordInPixels.y - tempY;
float scanLineWeight = CalcScanLine(dy);
float signY = sign(dy);
dy = dy * dy;
dy = dy * dy;
@@ -114,7 +130,11 @@ void main()
vec2 tc = vec2(texcoord.x, yCoord + dy);
#endif
vec3 colour = texture(Texture, tc).rgb;
float ca = uChroma * 0.005;
vec3 colour;
colour.r = texture(Texture, tc + vec2(ca, 0.0)).r;
colour.g = texture(Texture, tc).g;
colour.b = texture(Texture, tc - vec2(ca, 0.0)).b;
#if defined(SCANLINES)
#if defined(GAMMA)
@@ -125,7 +145,7 @@ void main()
#endif
#endif
scanLineWeight *= BLOOM_FACTOR;
colour *= scanLineWeight;
colour *= mix(1.0, scanLineWeight, uScanlines);
#if defined(GAMMA)
#if defined(FAKE_GAMMA)
@@ -136,6 +156,12 @@ void main()
#endif
#endif
if (uVignette > 0.0) {
vec2 uv = texcoord - vec2(0.5);
float vig = 1.0 - dot(uv, uv) * uVignette * 4.0;
colour *= clamp(vig, 0.0, 1.0);
}
#if MASK_TYPE == 0
FragColor = vec4(colour, 1.0);
#elif MASK_TYPE == 1

View File

@@ -91,9 +91,17 @@ void handleIncWindowZoom() {
}
}
void handleToggleShaders() {
Screen::get()->toggleShaders();
Notifier::get()->show({"SHADERS " + std::string(Options::video.shaders ? "ENABLED" : "DISABLED")});
void handleTogglePostFX() {
Screen::get()->togglePostFX();
Notifier::get()->show({"POSTFX " + std::string(Options::video.postfx ? "ENABLED" : "DISABLED")});
}
void handleNextPostFXPreset() {
if (!Options::postfx_presets.empty()) {
Options::current_postfx_preset = (Options::current_postfx_preset + 1) % static_cast<int>(Options::postfx_presets.size());
Screen::get()->reloadPostFX();
Notifier::get()->show({"POSTFX " + Options::postfx_presets[static_cast<size_t>(Options::current_postfx_preset)].name});
}
}
void handleNextPalette() {
@@ -152,8 +160,11 @@ auto getPressedAction() -> InputAction {
return InputAction::WINDOW_INC_ZOOM;
}
}
if (Input::get()->checkAction(InputAction::TOGGLE_SHADERS, Input::DO_NOT_ALLOW_REPEAT)) {
return InputAction::TOGGLE_SHADERS;
if (Input::get()->checkAction(InputAction::TOGGLE_POSTFX, Input::DO_NOT_ALLOW_REPEAT)) {
if (Options::video.postfx && (SDL_GetModState() & SDL_KMOD_SHIFT)) {
return InputAction::NEXT_POSTFX_PRESET;
}
return InputAction::TOGGLE_POSTFX;
}
if (Input::get()->checkAction(InputAction::NEXT_PALETTE, Input::DO_NOT_ALLOW_REPEAT)) {
return InputAction::NEXT_PALETTE;
@@ -221,8 +232,12 @@ void handle() {
handleIncWindowZoom();
break;
case InputAction::TOGGLE_SHADERS:
handleToggleShaders();
case InputAction::TOGGLE_POSTFX:
handleTogglePostFX();
break;
case InputAction::NEXT_POSTFX_PRESET:
handleNextPostFXPreset();
break;
case InputAction::NEXT_PALETTE:

View File

@@ -43,7 +43,7 @@ Input::Input(std::string game_controller_db_path)
{Action::WINDOW_DEC_ZOOM, KeyState{.scancode = SDL_SCANCODE_F1}},
{Action::WINDOW_INC_ZOOM, KeyState{.scancode = SDL_SCANCODE_F2}},
{Action::TOGGLE_FULLSCREEN, KeyState{.scancode = SDL_SCANCODE_F3}},
{Action::TOGGLE_SHADERS, KeyState{.scancode = SDL_SCANCODE_F4}},
{Action::TOGGLE_POSTFX, KeyState{.scancode = SDL_SCANCODE_F4}},
{Action::NEXT_PALETTE, KeyState{.scancode = SDL_SCANCODE_F5}},
{Action::PREVIOUS_PALETTE, KeyState{.scancode = SDL_SCANCODE_F6}},
{Action::TOGGLE_INTEGER_SCALE, KeyState{.scancode = SDL_SCANCODE_F7}},

View File

@@ -20,7 +20,8 @@ const std::unordered_map<InputAction, std::string> ACTION_TO_STRING = {
{InputAction::TOGGLE_MUSIC, "TOGGLE_MUSIC"},
{InputAction::NEXT_PALETTE, "NEXT_PALETTE"},
{InputAction::PREVIOUS_PALETTE, "PREVIOUS_PALETTE"},
{InputAction::TOGGLE_SHADERS, "TOGGLE_SHADERS"},
{InputAction::TOGGLE_POSTFX, "TOGGLE_POSTFX"},
{InputAction::NEXT_POSTFX_PRESET, "NEXT_POSTFX_PRESET"},
{InputAction::SHOW_DEBUG_INFO, "SHOW_DEBUG_INFO"},
{InputAction::TOGGLE_DEBUG, "TOGGLE_DEBUG"},
{InputAction::NONE, "NONE"}};
@@ -42,7 +43,8 @@ const std::unordered_map<std::string, InputAction> STRING_TO_ACTION = {
{"TOGGLE_MUSIC", InputAction::TOGGLE_MUSIC},
{"NEXT_PALETTE", InputAction::NEXT_PALETTE},
{"PREVIOUS_PALETTE", InputAction::PREVIOUS_PALETTE},
{"TOGGLE_SHADERS", InputAction::TOGGLE_SHADERS},
{"TOGGLE_POSTFX", InputAction::TOGGLE_POSTFX},
{"NEXT_POSTFX_PRESET", InputAction::NEXT_POSTFX_PRESET},
{"SHOW_DEBUG_INFO", InputAction::SHOW_DEBUG_INFO},
{"TOGGLE_DEBUG", InputAction::TOGGLE_DEBUG},
{"NONE", InputAction::NONE}};

View File

@@ -24,7 +24,8 @@ enum class InputAction : int { // Acciones de entrada posibles en el juego
TOGGLE_FULLSCREEN,
TOGGLE_VSYNC,
TOGGLE_INTEGER_SCALE,
TOGGLE_SHADERS,
TOGGLE_POSTFX,
NEXT_POSTFX_PRESET,
TOGGLE_BORDER,
TOGGLE_MUSIC,
NEXT_PALETTE,

View File

@@ -30,6 +30,7 @@ auto OpenGLShader::initGLExtensions() -> bool {
glUseProgram = (PFNGLUSEPROGRAMPROC)SDL_GL_GetProcAddress("glUseProgram");
glDeleteProgram = (PFNGLDELETEPROGRAMPROC)SDL_GL_GetProcAddress("glDeleteProgram");
glGetUniformLocation = (PFNGLGETUNIFORMLOCATIONPROC)SDL_GL_GetProcAddress("glGetUniformLocation");
glUniform1f = (PFNGLUNIFORM1FPROC)SDL_GL_GetProcAddress("glUniform1f");
glUniform2f = (PFNGLUNIFORM2FPROC)SDL_GL_GetProcAddress("glUniform2f");
glGenVertexArrays = (PFNGLGENVERTEXARRAYSPROC)SDL_GL_GetProcAddress("glGenVertexArrays");
glBindVertexArray = (PFNGLBINDVERTEXARRAYPROC)SDL_GL_GetProcAddress("glBindVertexArray");
@@ -44,7 +45,8 @@ auto OpenGLShader::initGLExtensions() -> bool {
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) &&
(glUseProgram != nullptr) && (glDeleteProgram != nullptr) && (glGetUniformLocation != nullptr) &&
(glUniform1f != nullptr) && (glUniform2f != nullptr) &&
(glGenVertexArrays != nullptr) && (glBindVertexArray != nullptr) && (glDeleteVertexArrays != nullptr) &&
(glGenBuffers != nullptr) && (glBindBuffer != nullptr) && (glBufferData != nullptr) && (glDeleteBuffers != nullptr) &&
(glVertexAttribPointer != nullptr) && (glEnableVertexAttribArray != nullptr);
@@ -320,7 +322,7 @@ auto OpenGLShader::init(SDL_Window* window,
// Crear geometría del quad
createQuadGeometry();
// Obtener ubicación del uniform TextureSize
// Obtener ubicaciones de uniforms y configurar valores iniciales
glUseProgram(program_id_);
texture_size_location_ = glGetUniformLocation(program_id_, "TextureSize");
if (texture_size_location_ != -1) {
@@ -334,6 +336,15 @@ auto OpenGLShader::init(SDL_Window* window,
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Uniform 'TextureSize' not found in shader");
}
// Uniforms PostFX
vignette_location_ = glGetUniformLocation(program_id_, "uVignette");
scanlines_location_ = glGetUniformLocation(program_id_, "uScanlines");
chroma_location_ = glGetUniformLocation(program_id_, "uChroma");
output_height_location_ = glGetUniformLocation(program_id_, "uOutputHeight");
if (vignette_location_ != -1) { glUniform1f(vignette_location_, postfx_vignette_); }
if (scanlines_location_ != -1) { glUniform1f(scanlines_location_, postfx_scanlines_); }
if (chroma_location_ != -1) { glUniform1f(chroma_location_, postfx_chroma_); }
glUseProgram(0);
is_initialized_ = true;
@@ -388,6 +399,11 @@ void OpenGLShader::render() {
glUseProgram(program_id_);
checkGLError("glUseProgram");
// Pasar uniforms PostFX
if (vignette_location_ != -1) { glUniform1f(vignette_location_, postfx_vignette_); }
if (scanlines_location_ != -1) { glUniform1f(scanlines_location_, postfx_scanlines_); }
if (chroma_location_ != -1) { glUniform1f(chroma_location_, postfx_chroma_); }
// Configurar viewport (obtener tamaño lógico de SDL)
int logical_w;
int logical_h;
@@ -431,6 +447,10 @@ void OpenGLShader::render() {
glViewport(viewport_x, viewport_y, viewport_w, viewport_h);
checkGLError("glViewport");
if (output_height_location_ != -1) {
glUniform1f(output_height_location_, static_cast<float>(viewport_h));
}
// Dibujar quad usando VAO
glBindVertexArray(vao_);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr);
@@ -449,6 +469,12 @@ void OpenGLShader::render() {
glViewport(old_viewport[0], old_viewport[1], old_viewport[2], old_viewport[3]);
}
void OpenGLShader::setPostFXParams(float vignette, float scanlines, float chroma) {
postfx_vignette_ = vignette;
postfx_scanlines_ = scanlines;
postfx_chroma_ = chroma;
}
void OpenGLShader::setTextureSize(float width, float height) {
if (!is_initialized_ || program_id_ == 0) {
return;

View File

@@ -30,6 +30,7 @@ class OpenGLShader : public ShaderBackend {
void render() override;
void setTextureSize(float width, float height) override;
void setPostFXParams(float vignette, float scanlines, float chroma) override;
void cleanup() final;
[[nodiscard]] auto isHardwareAccelerated() const -> bool override { return is_initialized_; }
@@ -55,6 +56,15 @@ class OpenGLShader : public ShaderBackend {
// Ubicaciones de uniforms
GLint texture_size_location_ = -1;
GLint vignette_location_ = -1;
GLint scanlines_location_ = -1;
GLint chroma_location_ = -1;
GLint output_height_location_ = -1;
// Valores cacheados de PostFX
float postfx_vignette_ = 0.6F;
float postfx_scanlines_ = 0.7F;
float postfx_chroma_ = 0.15F;
// Tamaños
int window_width_ = 0;
@@ -83,6 +93,7 @@ class OpenGLShader : public ShaderBackend {
PFNGLUSEPROGRAMPROC glUseProgram = nullptr;
PFNGLDELETEPROGRAMPROC glDeleteProgram = nullptr;
PFNGLGETUNIFORMLOCATIONPROC glGetUniformLocation = nullptr;
PFNGLUNIFORM1FPROC glUniform1f = nullptr;
PFNGLUNIFORM2FPROC glUniform2f = nullptr;
PFNGLGENVERTEXARRAYSPROC glGenVertexArrays = nullptr;
PFNGLBINDVERTEXARRAYPROC glBindVertexArray = nullptr;

View File

@@ -10,7 +10,10 @@
#include <string> // Para char_traits, string, operator+, operator==
#include "core/input/mouse.hpp" // Para updateCursorVisibility
#ifndef __APPLE__
#include "core/rendering/opengl/opengl_shader.hpp" // Para OpenGLShader
#endif
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp" // Para SDL3GPUShader
#include "core/rendering/surface.hpp" // Para Surface, readPalFile
#include "core/rendering/text.hpp" // Para Text
#include "core/resources/resource_cache.hpp" // Para Resource
@@ -124,13 +127,16 @@ void Screen::start() { setRendererSurface(nullptr); }
void Screen::render() {
fps_.increment();
// Renderiza todos los overlays
// Renderiza todos los overlays (escribe en game_surface_ CPU-side)
renderOverlays();
// Copia la surface a la textura
surfaceToTexture();
// En el path SDL3GPU, los píxeles se suben directamente desde la Surface.
// En el path SDL_Renderer, primero copiamos la surface a la SDL_Texture.
if (!(Options::video.postfx && shader_backend_ && shader_backend_->isHardwareAccelerated())) {
surfaceToTexture();
}
// Copia la textura al renderizador
// Copia la textura al renderizador (o hace el present GPU)
textureToRenderer();
}
@@ -203,10 +209,24 @@ void Screen::renderNotifications() const {
}
}
// Cambia el estado de los shaders
void Screen::toggleShaders() {
Options::video.shaders = !Options::video.shaders;
initShaders();
// Cambia el estado del PostFX
void Screen::togglePostFX() {
Options::video.postfx = !Options::video.postfx;
if (!Options::video.postfx && shader_backend_) {
// Al desactivar PostFX, limpiar el backend para liberar el swapchain de GPU
shader_backend_->cleanup();
} else {
initShaders();
}
}
// Recarga el shader del preset actual sin toggle
void Screen::reloadPostFX() {
if (Options::video.postfx) {
vertex_shader_source_.clear();
fragment_shader_source_.clear();
initShaders();
}
}
// Actualiza la lógica de la clase (versión nueva con delta_time para escenas migradas)
@@ -304,13 +324,42 @@ void Screen::surfaceToTexture() {
}
}
// Copia la textura al renderizador
// Copia la textura al renderizador (o hace el present GPU)
void Screen::textureToRenderer() {
SDL_Texture* texture_to_render = Options::video.border.enabled ? border_texture_ : game_texture_;
if (Options::video.shaders && shader_backend_) {
if (Options::video.postfx && shader_backend_ && shader_backend_->isHardwareAccelerated()) {
// ---- SDL3 GPU path: convertir Surface → ARGB → upload → PostFX → present ----
if (Options::video.border.enabled) {
// El border_surface_ solo tiene el color de borde; hay que componer encima el game_surface_
const int BORDER_W = static_cast<int>(border_surface_->getWidth());
const int BORDER_H = static_cast<int>(border_surface_->getHeight());
pixel_buffer_.resize(static_cast<size_t>(BORDER_W * BORDER_H));
border_surface_->toARGBBuffer(pixel_buffer_.data());
// Compositar game_surface_ en la posición correcta dentro del buffer
const int GAME_W = static_cast<int>(game_surface_->getWidth());
const int GAME_H = static_cast<int>(game_surface_->getHeight());
const int OFF_X = static_cast<int>(game_surface_dstrect_.x);
const int OFF_Y = static_cast<int>(game_surface_dstrect_.y);
std::vector<Uint32> game_pixels(static_cast<size_t>(GAME_W * GAME_H));
game_surface_->toARGBBuffer(game_pixels.data());
for (int y = 0; y < GAME_H; ++y) {
for (int x = 0; x < GAME_W; ++x) {
pixel_buffer_[static_cast<size_t>(((OFF_Y + y) * BORDER_W) + (OFF_X + x))] = game_pixels[static_cast<size_t>((y * GAME_W) + x)];
}
}
shader_backend_->uploadPixels(pixel_buffer_.data(), BORDER_W, BORDER_H);
} else {
const int GAME_W = static_cast<int>(game_surface_->getWidth());
const int GAME_H = static_cast<int>(game_surface_->getHeight());
pixel_buffer_.resize(static_cast<size_t>(GAME_W * GAME_H));
game_surface_->toARGBBuffer(pixel_buffer_.data());
shader_backend_->uploadPixels(pixel_buffer_.data(), GAME_W, GAME_H);
}
shader_backend_->render();
} else {
// ---- SDL_Renderer path (fallback / no-shader) ----
SDL_SetRenderTarget(renderer_, nullptr);
SDL_SetRenderDrawColor(renderer_, 0x00, 0x00, 0x00, 0xFF);
SDL_RenderClear(renderer_);
@@ -402,13 +451,10 @@ 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 = loadData(Resource::List::get()->get(vertex_file));
auto data = loadData(Resource::List::get()->get("crtpi_vertex_es.glsl"));
if (data.empty()) {
// Si no existe versión ES, usar versión Desktop
vertex_file = "crtpi_vertex.glsl";
data = loadData(Resource::List::get()->get(vertex_file));
data = loadData(Resource::List::get()->get("crtpi_vertex.glsl"));
std::cout << "Usando shaders OpenGL Desktop 3.3\n";
} else {
std::cout << "Usando shaders OpenGL ES 3.0 (Raspberry Pi)\n";
@@ -420,13 +466,10 @@ void Screen::loadShaders() {
}
if (fragment_shader_source_.empty()) {
// Intentar cargar versión ES primero si existe
std::string fragment_file = "crtpi_fragment_es.glsl";
auto data = loadData(Resource::List::get()->get(fragment_file));
auto data = loadData(Resource::List::get()->get("crtpi_fragment_es.glsl"));
if (data.empty()) {
// Si no existe versión ES, usar versión Desktop
fragment_file = "crtpi_fragment.glsl";
data = loadData(Resource::List::get()->get(fragment_file));
data = loadData(Resource::List::get()->get("crtpi_fragment.glsl"));
}
if (!data.empty()) {
@@ -435,22 +478,38 @@ void Screen::loadShaders() {
}
}
// Aplica los parámetros del preset actual al backend de shaders
void Screen::applyCurrentPostFXPreset() {
if (shader_backend_ && !Options::postfx_presets.empty()) {
const auto& p = Options::postfx_presets[static_cast<size_t>(Options::current_postfx_preset)];
shader_backend_->setPostFXParams(p.vignette, p.scanlines, p.chroma);
}
}
// 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_, Options::video.border.enabled ? border_texture_ : game_texture_, vertex_shader_source_, fragment_shader_source_);
// shader_backend_->init(window_, shaders_texture_, vertex_shader_source_, fragment_shader_source_);
if (!Options::video.postfx) {
return;
}
SDL_Texture* tex = Options::video.border.enabled ? border_texture_ : game_texture_;
#ifdef __APPLE__
// macOS: usar SDL3 GPU API (Metal) via SDL3GPUShader
if (!shader_backend_) {
shader_backend_ = std::make_unique<Rendering::SDL3GPUShader>();
}
shader_backend_->init(window_, tex, "", "");
#else
// En macOS, OpenGL está deprecated y rinde mal
// TODO: Implementar backend de Metal para shaders en macOS
std::cout << "WARNING: Shaders no disponibles en macOS (OpenGL deprecated). Usa Metal backend.\n";
// Win/Linux: usar OpenGL + GLSL
loadShaders();
if (!shader_backend_) {
shader_backend_ = std::make_unique<Rendering::OpenGLShader>();
}
shader_backend_->init(window_, tex, vertex_shader_source_, fragment_shader_source_);
#endif
applyCurrentPostFXPreset();
}
// Obtiene información sobre la pantalla
@@ -537,7 +596,9 @@ auto Screen::initSDLVideo() -> bool {
const auto WINDOW_WIDTH = Options::video.border.enabled ? Options::game.width + (Options::video.border.width * 2) : Options::game.width;
const auto WINDOW_HEIGHT = Options::video.border.enabled ? Options::game.height + (Options::video.border.height * 2) : Options::game.height;
#ifdef __APPLE__
SDL_WindowFlags window_flags = SDL_WINDOW_METAL;
// SDL_WINDOW_METAL no es necesario: SDL3GPU autodetecta Metal via SDL_CreateGPUDevice.
// SDL_Renderer también usará Metal si está disponible (via hint o autoselección).
SDL_WindowFlags window_flags = 0;
#elif defined(LINUX_BUILD)
// En Linux, SDL_WINDOW_OPENGL puede entrar en conflicto con el backend del renderer;
// el hint SDL_HINT_RENDER_DRIVER="opengl" es suficiente para seleccionar OpenGL.
@@ -571,8 +632,8 @@ auto Screen::initSDLVideo() -> bool {
return false;
}
// Sin OpenGL garantizado, deshabilitar shaders
Options::video.shaders = false;
std::cout << "WARNING: Shaders disabled (OpenGL not available)\n";
Options::video.postfx = false;
std::cout << "WARNING: PostFX disabled (OpenGL not available)\n";
}
// Configurar renderer

View File

@@ -7,6 +7,8 @@
#include <string> // Para string
#include <vector> // Para vector
#include <SDL3/SDL_pixels.h> // Para Uint32
#include "utils/utils.hpp" // Para Color
class Surface;
class Text;
@@ -51,11 +53,12 @@ class Screen {
static void setBorderEnabled(bool value); // Establece si se ha de ver el borde
void toggleBorder(); // Cambia entre borde visible y no visible
// Paletas y shaders
// Paletas y PostFX
void nextPalette(); // Cambia a la siguiente paleta
void previousPalette(); // Cambia a la paleta anterior
void setPalete(); // Establece la paleta actual
void toggleShaders(); // Cambia el estado de los shaders
void togglePostFX(); // Cambia el estado del PostFX
void reloadPostFX(); // Recarga el shader del preset actual sin toggle
// Surfaces y notificaciones
void setRendererSurface(const std::shared_ptr<Surface>& surface = nullptr); // Establece el renderizador para las surfaces
@@ -114,6 +117,7 @@ class Screen {
auto findPalette(const std::string& name) -> size_t; // Localiza la paleta dentro del vector de paletas
void initShaders(); // Inicializa los shaders
void loadShaders(); // Carga el contenido del archivo GLSL
void applyCurrentPostFXPreset(); // Aplica los parámetros del preset actual al backend
void renderInfo(); // Muestra información por pantalla
void getDisplayInfo(); // Obtiene información sobre la pantalla
auto initSDLVideo() -> bool; // Arranca SDL VIDEO y crea la ventana
@@ -155,6 +159,7 @@ class Screen {
std::string info_resolution_; // Texto con la información de la pantalla
std::string vertex_shader_source_; // Almacena el vertex shader
std::string fragment_shader_source_; // Almacena el fragment shader
std::vector<Uint32> pixel_buffer_; // Buffer intermedio para SDL3GPU path (surface → ARGB)
#ifdef _DEBUG
bool show_debug_info_{true}; // Indica si ha de mostrar la información de debug

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,432 @@
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp"
#include <SDL3/SDL_log.h>
#include <cstring> // memcpy, strlen
#ifndef __APPLE__
#include "core/rendering/sdl3gpu/postfx_frag_spv.h"
#include "core/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;
};
fragment float4 postfx_fs(PostVOut in [[stage_in]],
texture2d<float> scene [[texture(0)]],
sampler samp [[sampler(0)]],
constant PostFXUniforms& u [[buffer(0)]]) {
float ca = u.chroma_strength * 0.005;
float4 color;
color.r = scene.sample(samp, in.uv + float2( ca, 0.0)).r;
color.g = scene.sample(samp, in.uv).g;
color.b = scene.sample(samp, in.uv - float2( ca, 0.0)).b;
color.a = scene.sample(samp, in.uv).a;
float texHeight = float(scene.get_height());
float scaleY = u.screen_height / texHeight;
float screenY = in.uv.y * u.screen_height;
float posInRow = fmod(screenY, scaleY);
float scanLineDY = posInRow / scaleY - 0.5;
float scan = max(1.0 - scanLineDY * scanLineDY * 6.0, 0.12) * 3.5;
color.rgb *= mix(1.0, scan, u.scanline_strength);
float2 d = in.uv - float2(0.5, 0.5);
float vignette = 1.0 - dot(d, d) * u.vignette_strength;
color.rgb *= clamp(vignette, 0.0, 1.0);
return color;
}
)";
// NOLINTEND(readability-identifier-naming)
#endif // __APPLE__
namespace Rendering {
// ---------------------------------------------------------------------------
// Destructor
// ---------------------------------------------------------------------------
SDL3GPUShader::~SDL3GPUShader() {
cleanup();
}
// ---------------------------------------------------------------------------
// 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, limpiar antes de reinicializar (p.ej. al cambiar borde)
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);
tex_width_ = static_cast<int>(fw);
tex_height_ = static_cast<int>(fh);
uniforms_.screen_height = fh;
// ----------------------------------------------------------------
// 1. Create GPU device
// ----------------------------------------------------------------
#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 (GPU device owns the swapchain from now on)
// ----------------------------------------------------------------
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,
SDL_GPU_PRESENTMODE_VSYNC);
// ----------------------------------------------------------------
// 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 nearest-neighbour sampler (retro pixel art)
// ----------------------------------------------------------------
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;
}
// ----------------------------------------------------------------
// 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
// ---------------------------------------------------------------------------
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;
}
std::memcpy(mapped, pixels, static_cast<size_t>(width * height * 4));
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;
}
uniforms_.screen_height = static_cast<float>(sh);
// ---- 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_);
SDL_GPUTextureSamplerBinding binding = {};
binding.texture = scene_texture_;
binding.sampler = 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
// ---------------------------------------------------------------------------
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;
}
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,
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(float vignette, float scanlines, float chroma) {
uniforms_.vignette_strength = vignette;
uniforms_.scanline_strength = scanlines;
uniforms_.chroma_strength = chroma;
}
} // namespace Rendering

View File

@@ -0,0 +1,79 @@
#pragma once
#include <SDL3/SDL.h>
#include <SDL3/SDL_gpu.h>
#include "core/rendering/shader_backend.hpp"
// PostFX uniforms pushed to fragment stage each frame.
// Must match the MSL struct and GLSL uniform block layout.
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 (for resolution-independent scanlines)
};
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() override;
[[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(float vignette, float scanlines, float chroma) 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;
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;
PostFXUniforms uniforms_{.vignette_strength = 0.6F, .chroma_strength = 0.15F, .scanline_strength = 0.7F, .screen_height = 192.0F};
int tex_width_ = 0;
int tex_height_ = 0;
bool is_initialized_ = false;
};
} // namespace Rendering

View File

@@ -46,6 +46,20 @@ class ShaderBackend {
*/
virtual void cleanup() = 0;
/**
* @brief Sube píxeles ARGB8888 desde la CPU al backend de shaders
* Usado por SDL3GPUShader para evitar pasar por SDL_Texture
*/
virtual void uploadPixels(const Uint32* /*pixels*/, int /*width*/, int /*height*/) {}
/**
* @brief Establece los parámetros de intensidad de los efectos PostFX
* @param vignette Intensidad de la viñeta (0.0 = ninguna, 1.0 = máxima)
* @param scanlines Intensidad de las scanlines (0.0 = desactivadas, 1.0 = máximas)
* @param chroma Intensidad de la aberración cromática (0.0 = ninguna, 1.0 = máxima)
*/
virtual void setPostFXParams(float /*vignette*/, float /*scanlines*/, float /*chroma*/) {}
/**
* @brief Verifica si el backend está usando aceleración por hardware
* @return true si usa aceleración (OpenGL/Metal/Vulkan)

View File

@@ -524,6 +524,19 @@ void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height
}
}
// Vuelca los píxeles como ARGB8888 a un buffer externo (sin SDL_Texture ni SDL_Renderer)
void Surface::toARGBBuffer(Uint32* buffer) const {
if (!surface_data_ || (surface_data_->data == nullptr)) { return; }
const int WIDTH = static_cast<int>(surface_data_->width);
const int HEIGHT = static_cast<int>(surface_data_->height);
const Uint8* src = surface_data_->data.get();
for (int y = 0; y < HEIGHT; ++y) {
for (int x = 0; x < WIDTH; ++x) {
buffer[(y * WIDTH) + x] = palette_[src[(y * WIDTH) + x]];
}
}
}
// Vuelca la superficie a una textura
void Surface::copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture) {
if ((renderer == nullptr) || (texture == nullptr) || !surface_data_) {

View File

@@ -106,6 +106,9 @@ class Surface {
auto fadePalette() -> bool;
auto fadeSubPalette(Uint32 delay = 0) -> bool;
// Vuelca los píxeles como ARGB8888 a un buffer externo (sin SDL_Texture)
void toARGBBuffer(Uint32* buffer) const;
// Pone un pixel en la SurfaceData
void putPixel(int x, int y, Uint8 color);

View File

@@ -122,6 +122,10 @@ Director::Director(std::vector<std::string> const& args) {
Options::setConfigFile(Resource::List::get()->get("config.yaml"));
Options::loadFromFile();
// Configura la ruta y carga los presets de PostFX
Options::setPostFXFile(Resource::List::get()->get("postfx.yaml"));
Options::loadPostFXFromFile();
// En mode quiosc, forçar pantalla completa independentment de la configuració
if (Options::kiosk.enabled) {
Options::video.fullscreen = true;

View File

@@ -31,7 +31,7 @@ namespace Video {
constexpr bool FULLSCREEN = false; // Modo de pantalla completa por defecto (false = ventana)
constexpr Screen::Filter FILTER = Screen::Filter::NEAREST; // Filtro por defecto
constexpr bool VERTICAL_SYNC = true; // Vsync activado por defecto
constexpr bool SHADERS = false; // Shaders desactivados por defecto
constexpr bool POSTFX = false; // PostFX desactivado por defecto
constexpr bool INTEGER_SCALE = true; // Escalado entero activado por defecto
constexpr bool KEEP_ASPECT = true; // Mantener aspecto activado por defecto
constexpr const char* PALETTE_NAME = "zx-spectrum"; // Paleta por defecto

View File

@@ -329,11 +329,11 @@ void loadBasicVideoFieldsFromYaml(const fkyaml::node& vid) {
}
}
if (vid.contains("shaders")) {
if (vid.contains("postfx")) {
try {
video.shaders = vid["shaders"].get_value<bool>();
video.postfx = vid["postfx"].get_value<bool>();
} catch (...) {
video.shaders = Defaults::Video::SHADERS;
video.postfx = Defaults::Video::POSTFX;
}
}
@@ -606,7 +606,7 @@ auto saveToFile() -> bool {
file << "video:\n";
file << " fullscreen: " << (video.fullscreen ? "true" : "false") << "\n";
file << " filter: " << filterToString(video.filter) << " # filter: nearest (pixel perfect) | linear (smooth)\n";
file << " shaders: " << (video.shaders ? "true" : "false") << "\n";
file << " postfx: " << (video.postfx ? "true" : "false") << "\n";
file << " vertical_sync: " << (video.vertical_sync ? "true" : "false") << "\n";
file << " integer_scale: " << (video.integer_scale ? "true" : "false") << "\n";
file << " keep_aspect: " << (video.keep_aspect ? "true" : "false") << "\n";
@@ -649,4 +649,113 @@ auto saveToFile() -> bool {
return true;
}
// Establece la ruta del fichero de PostFX
void setPostFXFile(const std::string& path) {
postfx_file_path = path;
}
// Carga los presets de PostFX desde el fichero
auto loadPostFXFromFile() -> bool {
postfx_presets.clear();
current_postfx_preset = 0;
std::ifstream file(postfx_file_path);
if (!file.good()) {
if (console) {
std::cout << "PostFX file not found, creating default: " << postfx_file_path << '\n';
}
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 (size_t i = 0; i < presets.size(); ++i) {
const auto& p = presets[i];
PostFXPreset preset;
if (p.contains("name")) {
preset.name = p["name"].get_value<std::string>();
}
if (p.contains("vignette")) {
try { preset.vignette = p["vignette"].get_value<float>(); } catch (...) {}
}
if (p.contains("scanlines")) {
try { preset.scanlines = p["scanlines"].get_value<float>(); } catch (...) {}
}
if (p.contains("chroma")) {
try { preset.chroma = p["chroma"].get_value<float>(); } catch (...) {}
}
postfx_presets.push_back(preset);
}
}
if (console) {
std::cout << "PostFX file loaded: " << postfx_presets.size() << " preset(s)\n";
}
return true;
} catch (const fkyaml::exception& e) {
if (console) {
std::cerr << "Error parsing PostFX YAML: " << e.what() << '\n';
}
return savePostFXToFile();
}
}
// Guarda los presets de PostFX por defecto
auto savePostFXToFile() -> bool {
if (postfx_file_path.empty()) {
return false;
}
std::ofstream file(postfx_file_path);
if (!file.is_open()) {
if (console) {
std::cerr << "Error: Unable to open file " << postfx_file_path << " for writing\n";
}
return false;
}
file << "# JailDoctor's Dilemma - 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 << "\n";
file << "presets:\n";
file << " - name: \"CRT\"\n";
file << " vignette: 0.6\n";
file << " scanlines: 0.7\n";
file << " chroma: 0.15\n";
file << " - name: \"SCANLINES\"\n";
file << " vignette: 0.0\n";
file << " scanlines: 0.8\n";
file << " chroma: 0.0\n";
file << " - name: \"SUBTLE\"\n";
file << " vignette: 0.3\n";
file << " scanlines: 0.4\n";
file << " chroma: 0.05\n";
file.close();
if (console) {
std::cout << "PostFX file created with defaults: " << postfx_file_path << '\n';
}
// Cargar los presets recién creados
postfx_presets.clear();
postfx_presets.push_back({"CRT", 0.6F, 0.7F, 0.15F});
postfx_presets.push_back({"SCANLINES", 0.0F, 0.8F, 0.0F});
postfx_presets.push_back({"SUBTLE", 0.3F, 0.4F, 0.05F});
current_postfx_preset = 0;
return true;
}
} // namespace Options

View File

@@ -5,6 +5,7 @@
#include <algorithm>
#include <string> // Para string, basic_string
#include <utility>
#include <vector> // Para vector
#include "core/rendering/screen.hpp" // Para Screen::Filter
#include "game/defaults.hpp"
@@ -79,7 +80,7 @@ struct Video {
bool fullscreen{Defaults::Video::FULLSCREEN}; // Contiene el valor del modo de pantalla completa
Screen::Filter filter{Defaults::Video::FILTER}; // Filtro usado para el escalado de la imagen
bool vertical_sync{Defaults::Video::VERTICAL_SYNC}; // Indica si se quiere usar vsync o no
bool shaders{Defaults::Video::SHADERS}; // Indica si se van a usar shaders o no
bool postfx{Defaults::Video::POSTFX}; // Indica si se van a usar efectos PostFX o no
bool integer_scale{Defaults::Video::INTEGER_SCALE}; // Indica si el escalado de la imagen ha de ser entero en el modo a pantalla completa
bool keep_aspect{Defaults::Video::KEEP_ASPECT}; // Indica si se ha de mantener la relación de aspecto al poner el modo a pantalla completa
Border border{}; // Borde de la pantalla
@@ -113,6 +114,14 @@ struct Game {
float height{Defaults::Canvas::HEIGHT}; // Alto de la resolucion del juego
};
// Estructura para un preset de PostFX
struct PostFXPreset {
std::string name; // Nombre del preset
float vignette{0.6F}; // Intensidad de la viñeta (0.0 = ninguna, 1.0 = máxima)
float scanlines{0.7F}; // Intensidad de las scanlines (0.0 = desactivadas, 1.0 = máximas)
float chroma{0.15F}; // Intensidad de la aberración cromática (0.0 = ninguna, 1.0 = máxima)
};
// --- Variables globales ---
inline std::string version{}; // Versión del fichero de configuración. Sirve para saber si las opciones son compatibles
inline bool console{false}; // Indica si ha de mostrar información por la consola de texto
@@ -129,10 +138,18 @@ inline Kiosk kiosk{}; // Opciones del modo kiosko
// Ruta completa del fichero de configuración (establecida mediante setConfigFile)
inline std::string config_file_path{};
// --- Variables PostFX ---
inline std::vector<PostFXPreset> postfx_presets{}; // Lista de presets de PostFX
inline int current_postfx_preset{0}; // Índice del preset de PostFX actual
inline std::string postfx_file_path{}; // Ruta del fichero postfx.yaml
// --- Funciones públicas ---
void init(); // Crea e inicializa las opciones del programa
void setConfigFile(const std::string& path); // Establece la ruta del fichero de configuración
auto loadFromFile() -> bool; // Carga las opciones desde el fichero configurado
auto saveToFile() -> bool; // Guarda las opciones al fichero configurado
void init(); // Crea e inicializa las opciones del programa
void setConfigFile(const std::string& path); // Establece la ruta del fichero de configuración
auto loadFromFile() -> bool; // Carga las opciones desde el fichero configurado
auto saveToFile() -> bool; // Guarda las opciones al fichero configurado
void setPostFXFile(const std::string& path); // Establece la ruta del fichero de PostFX
auto loadPostFXFromFile() -> bool; // Carga los presets de PostFX desde el fichero
auto savePostFXToFile() -> bool; // Guarda los presets de PostFX por defecto
} // namespace Options

View File

@@ -34,7 +34,7 @@ enum class Options {
// --- Variables de estado globales ---
#ifdef _DEBUG
inline Scene current = Scene::ENDING2; // Escena actual
inline Scene current = Scene::GAME; // Escena actual
inline Options options = Options::LOGO_TO_LOADING_SCREEN; // Opciones de la escena actual
#else
inline Scene current = Scene::LOGO; // Escena actual

View File

@@ -4,7 +4,7 @@
// Textos
namespace Texts {
constexpr const char* WINDOW_CAPTION = "JailDoctor's Dilemma";
constexpr const char* WINDOW_CAPTION = "© 2022 JailDoctor's Dilemma — JailDesigner";
constexpr const char* COPYRIGHT = "@2022 JailDesigner";
constexpr const char* VERSION = "1.10"; // Versión por defecto
} // namespace Texts