This commit is contained in:
2026-04-17 19:04:44 +02:00
parent 5fec0110b3
commit 1bb0ebdef8
30 changed files with 45791 additions and 23 deletions

View File

@@ -11,14 +11,15 @@ Coffee Crisis is a C++20 arcade game built with SDL3. The player controls a char
Dependencies: `libsdl3-dev` and `g++` (Linux) or `clang++` (macOS). Build system is CMake (driven by `Makefile` wrappers).
```bash
make # Release build
make debug # Debug build (defines DEBUG and PAUSE)
make release # Empaqueta .tar.gz / .dmg / .zip segons SO
make pack # Regenera resources.pack
make controllerdb # Descarga gamecontrollerdb.txt
make format # clang-format -i
make tidy # clang-tidy
make cppcheck # cppcheck
make # Release build
make debug # Debug build (defines DEBUG and PAUSE)
make release # Empaqueta .tar.gz / .dmg / .zip segons SO
make pack # Regenera resources.pack
make compile_shaders # Compila shaders GLSL → headers SPIR-V (requereix glslc)
make controllerdb # Descarga gamecontrollerdb.txt
make format # clang-format -i
make tidy # clang-tidy
make cppcheck # cppcheck
```
## Architecture
@@ -33,6 +34,10 @@ source/
│ ├── input/ input.*, mouse.*
│ ├── locale/ lang.*
│ ├── rendering/ screen, fade, text, writer, texture, sprite + animated/moving/smart
│ │ ├── shader_backend.hpp (interfície abstracta de post-procesado)
│ │ └── sdl3gpu/ (pipeline SDL3 GPU)
│ │ ├── sdl3gpu_shader.* (implementació del backend GPU)
│ │ └── spv/ (headers SPIR-V generats — protegits amb dummies `.clang-*`)
│ ├── resources/ asset, resource, resource_pack, resource_loader, resource_helper
│ └── system/ director
├── game/
@@ -66,6 +71,24 @@ Flux general controlat per la classe **Director** (`core/system/director.h`): in
**jail_audio** (`core/audio/jail_audio.hpp`): wrapper audio SDL3 first-party. Usa stb_vorbis per OGG. API `JA_*` per música i efectes amb mesclat per canals.
### GPU / shaders (post-procesado)
Pipeline SDL3 GPU portat de `coffee_crisis_arcade_edition`. El canvas 256×192 es pot passar per un backend GPU que aplica PostFX (vinyeta, scanlines, chroma, gamma, mask, curvatura, bleeding, flicker) o CrtPi (scanlines continues amb bloom). Fallback transparent al `SDL_Renderer` clàssic si la GPU falla o si es desactiva.
- **Interfície**: `core/rendering/shader_backend.hpp` (`Rendering::ShaderBackend`).
- **Implementació**: `core/rendering/sdl3gpu/sdl3gpu_shader.*` + shaders GLSL a `data/shaders/` compilats a `spv/*_spv.h` via `glslc` (o precompilats si no hi ha `glslc`).
- **Emscripten**: compile-time `NO_SHADERS` → sempre ruta clàssica.
- **macOS**: shaders Metal (MSL) inline dins `sdl3gpu_shader.cpp`; no SPIR-V.
- **Opcions persistents** a `config.txt` (migració a YAML pendent):
- `videoGpuAcceleration` (bool)
- `videoGpuPreferredDriver` (string, buit = auto)
- `videoShaderEnabled` (bool)
- `videoShaderType` (0=POSTFX, 1=CRTPI)
- **Hotkeys** (provisionals fins que hi hagi menú d'opcions): `F9` toggle GPU · `F10` toggle shader · `F11` alterna POSTFX ↔ CRTPI.
- **API** a `Screen`: `setGpuAcceleration`/`toggleGpuAcceleration`/`isGpuAccelerated`, `setShaderEnabled`/`toggleShaderEnabled`/`isShaderEnabled`, `setActiveShader`/`toggleActiveShader`/`getActiveShader`.
Presets PostFX/CrtPi i cicle de presets encara **no** estan implementats — arribaran amb la migració a YAML. Per ara, valors per defecte hardcoded.
## Data Directory
- `data/gfx/` — PNG spritesheets and `.ani` animation definition files
@@ -74,6 +97,7 @@ Flux general controlat per la classe **Director** (`core/system/director.h`): in
- `data/lang/` — language files (es_ES, ba_BA, en_UK)
- `data/demo/` — demo recording data (gamecontrollerdb.txt vive en la raíz del proyecto, fuera del pack)
- `data/menu/` — menu definition files
- `data/shaders/` — fonts GLSL per al post-procesado SDL3 GPU (no van al pack; s'empotren al binari via headers SPIR-V)
## Language

View File

@@ -30,6 +30,12 @@ set(DIR_SOURCES "${CMAKE_SOURCE_DIR}/source")
file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "${DIR_SOURCES}/*.cpp")
list(FILTER SOURCES EXCLUDE REGEX "${DIR_SOURCES}/external/.*")
# En Emscripten no compilamos sdl3gpu_shader (SDL3 GPU no está soportado en WebGL2).
# Define NO_SHADERS más abajo y filtra el fuente aquí.
if(EMSCRIPTEN)
list(REMOVE_ITEM SOURCES "${DIR_SOURCES}/core/rendering/sdl3gpu/sdl3gpu_shader.cpp")
endif()
# Verificar si se encontraron archivos fuente
if(NOT SOURCES)
message(FATAL_ERROR "No se encontraron archivos fuente en ${DIR_SOURCES}.")
@@ -55,9 +61,78 @@ else()
message(STATUS "SDL3 encontrado: ${SDL3_INCLUDE_DIRS}")
endif()
# --- SHADER COMPILATION (Linux/Windows only - macOS uses Metal, Emscripten no soporta SDL3 GPU) ---
if(NOT APPLE AND NOT EMSCRIPTEN)
find_program(GLSLC_EXE NAMES glslc)
set(SHADER_VERT_SRC "${CMAKE_SOURCE_DIR}/data/shaders/postfx.vert")
set(SHADER_FRAG_SRC "${CMAKE_SOURCE_DIR}/data/shaders/postfx.frag")
set(SHADER_CRTPI_SRC "${CMAKE_SOURCE_DIR}/data/shaders/crtpi_frag.glsl")
set(SHADER_UPSCALE_SRC "${CMAKE_SOURCE_DIR}/data/shaders/upscale.frag")
set(SHADER_DOWNSCALE_SRC "${CMAKE_SOURCE_DIR}/data/shaders/downscale.frag")
set(SHADER_VERT_H "${CMAKE_SOURCE_DIR}/source/core/rendering/sdl3gpu/spv/postfx_vert_spv.h")
set(SHADER_FRAG_H "${CMAKE_SOURCE_DIR}/source/core/rendering/sdl3gpu/spv/postfx_frag_spv.h")
set(SHADER_CRTPI_H "${CMAKE_SOURCE_DIR}/source/core/rendering/sdl3gpu/spv/crtpi_frag_spv.h")
set(SHADER_UPSCALE_H "${CMAKE_SOURCE_DIR}/source/core/rendering/sdl3gpu/spv/upscale_frag_spv.h")
set(SHADER_DOWNSCALE_H "${CMAKE_SOURCE_DIR}/source/core/rendering/sdl3gpu/spv/downscale_frag_spv.h")
set(ALL_SHADER_HEADERS "${SHADER_VERT_H}" "${SHADER_FRAG_H}" "${SHADER_CRTPI_H}" "${SHADER_UPSCALE_H}" "${SHADER_DOWNSCALE_H}")
if(GLSLC_EXE)
set(COMPILE_SHADER_SCRIPT "${CMAKE_SOURCE_DIR}/tools/shaders/compile_shader.cmake")
macro(add_shader SRC_FILE OUT_H VAR_NAME)
cmake_parse_arguments(S "" "STAGE" "" ${ARGN})
add_custom_command(
OUTPUT "${OUT_H}"
COMMAND ${CMAKE_COMMAND}
"-DGLSLC=${GLSLC_EXE}"
"-DSRC=${SRC_FILE}"
"-DOUT_H=${OUT_H}"
"-DVAR=${VAR_NAME}"
"-DSTAGE=${S_STAGE}"
-P "${COMPILE_SHADER_SCRIPT}"
DEPENDS "${SRC_FILE}" "${COMPILE_SHADER_SCRIPT}"
COMMENT "Compilando shader: ${VAR_NAME}"
)
endmacro()
add_shader("${SHADER_VERT_SRC}" "${SHADER_VERT_H}" "postfx_vert_spv")
add_shader("${SHADER_FRAG_SRC}" "${SHADER_FRAG_H}" "postfx_frag_spv")
add_shader("${SHADER_CRTPI_SRC}" "${SHADER_CRTPI_H}" "crtpi_frag_spv" STAGE fragment)
add_shader("${SHADER_UPSCALE_SRC}" "${SHADER_UPSCALE_H}" "upscale_frag_spv")
add_shader("${SHADER_DOWNSCALE_SRC}" "${SHADER_DOWNSCALE_H}" "downscale_frag_spv")
add_custom_target(shaders DEPENDS ${ALL_SHADER_HEADERS})
message(STATUS "glslc encontrado: shaders se compilarán automáticamente")
else()
foreach(_h IN LISTS ALL_SHADER_HEADERS)
if(NOT EXISTS "${_h}")
message(FATAL_ERROR
"glslc no encontrado y header SPIR-V no existe: ${_h}\n"
" Instala glslc: sudo apt install glslang-tools (Linux)\n"
" choco install vulkan-sdk (Windows)"
)
endif()
endforeach()
message(STATUS "glslc no encontrado - usando headers SPIR-V precompilados")
endif()
else()
if(EMSCRIPTEN)
message(STATUS "Emscripten: shaders SPIR-V omitidos (SDL3 GPU no soportado en WebGL2)")
else()
message(STATUS "macOS: shaders SPIR-V omitidos (usa Metal inline)")
endif()
endif()
# Añadir ejecutable principal
add_executable(${PROJECT_NAME} ${SOURCES})
if(NOT APPLE AND NOT EMSCRIPTEN AND GLSLC_EXE)
add_dependencies(${PROJECT_NAME} shaders)
endif()
# Includes relatius a source/ (p.e. `#include "core/rendering/texture.h"`)
target_include_directories(${PROJECT_NAME} PRIVATE ${DIR_SOURCES})
@@ -96,7 +171,7 @@ elseif(APPLE)
)
endif()
elseif(EMSCRIPTEN)
target_compile_definitions(${PROJECT_NAME} PRIVATE EMSCRIPTEN_BUILD)
target_compile_definitions(${PROJECT_NAME} PRIVATE EMSCRIPTEN_BUILD NO_SHADERS)
# En wasm NO empaquetamos un resources.pack: el propio --preload-file de
# emscripten ya hace el mismo trabajo (bundle del directorio en un .data),
# así que metemos directamente 'data' y dejamos que el Resource lea por

View File

@@ -349,6 +349,11 @@ cppcheck:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target cppcheck
# SHADERS (SPIR-V) — sólo Linux/Windows. Requiere glslc en el PATH.
compile_shaders:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target shaders
# DESCARGA DE GAMECONTROLLERDB
# ==============================================================================
controllerdb:
@@ -378,6 +383,7 @@ help:
@echo ""
@echo " Herramientas:"
@echo " make pack - Empaquetar recursos a resources.pack"
@echo " make compile_shaders - Compilar shaders GLSL a headers SPIR-V (requiere glslc)"
@echo " make controllerdb - Descargar gamecontrollerdb.txt actualizado"
@echo ""
@echo " Calidad de codigo:"
@@ -391,4 +397,4 @@ help:
@echo " make show_version - Mostrar version actual ($(VERSION))"
@echo " make help - Mostrar esta ayuda"
.PHONY: all debug release _windows_release _macos_release _linux_release wasm wasm_debug controllerdb pack format format-check tidy tidy-fix cppcheck show_version help
.PHONY: all debug release _windows_release _macos_release _linux_release wasm wasm_debug controllerdb pack format format-check tidy tidy-fix cppcheck compile_shaders show_version help

View File

@@ -0,0 +1,152 @@
#version 450
// Vulkan GLSL fragment shader — CRT-Pi PostFX
// Algoritmo de scanlines continuas con pesos gaussianos, bloom y máscara de fósforo.
// Basado en el shader CRT-Pi original (GLSL 3.3), portado a GLSL 4.50 con parámetros uniformes.
//
// Compile: glslc -fshader-stage=frag --target-env=vulkan1.0 crtpi_frag.glsl -o crtpi_frag.spv
// xxd -i crtpi_frag.spv > ../../source/core/rendering/sdl3gpu/crtpi_frag_spv.h
layout(location = 0) in vec2 v_uv;
layout(location = 0) out vec4 out_color;
layout(set = 2, binding = 0) uniform sampler2D Texture;
layout(set = 3, binding = 0) uniform CrtPiBlock {
// vec4 #0
float scanline_weight; // Ajuste gaussiano de scanlines (default 6.0)
float scanline_gap_brightness; // Brillo mínimo entre scanlines (default 0.12)
float bloom_factor; // Factor de brillo en zonas iluminadas (default 3.5)
float input_gamma; // Gamma de entrada — linealización (default 2.4)
// vec4 #1
float output_gamma; // Gamma de salida — codificación (default 2.2)
float mask_brightness; // Brillo sub-píxeles de la máscara (default 0.80)
float curvature_x; // Distorsión barrel eje X (default 0.05)
float curvature_y; // Distorsión barrel eje Y (default 0.10)
// vec4 #2
int mask_type; // 0=ninguna, 1=verde/magenta, 2=RGB fósforo
int enable_scanlines; // 0 = off, 1 = on
int enable_multisample; // 0 = off, 1 = on (antialiasing analítico de scanlines)
int enable_gamma; // 0 = off, 1 = on
// vec4 #3
int enable_curvature; // 0 = off, 1 = on
int enable_sharper; // 0 = off, 1 = on
float texture_width; // Ancho del canvas lógico en píxeles
float texture_height; // Alto del canvas lógico en píxeles
} u;
// Distorsión barrel CRT
vec2 distort(vec2 coord, vec2 screen_scale) {
vec2 curvature = vec2(u.curvature_x, u.curvature_y);
vec2 barrel_scale = 1.0 - (0.23 * curvature);
coord *= screen_scale;
coord -= vec2(0.5);
float rsq = coord.x * coord.x + coord.y * coord.y;
coord += coord * (curvature * rsq);
coord *= barrel_scale;
if (abs(coord.x) >= 0.5 || abs(coord.y) >= 0.5) {
return vec2(-1.0); // fuera de pantalla
}
coord += vec2(0.5);
coord /= screen_scale;
return coord;
}
float calcScanLineWeight(float dist) {
return max(1.0 - dist * dist * u.scanline_weight, u.scanline_gap_brightness);
}
float calcScanLine(float dy, float filter_width) {
float weight = calcScanLineWeight(dy);
if (u.enable_multisample != 0) {
weight += calcScanLineWeight(dy - filter_width);
weight += calcScanLineWeight(dy + filter_width);
weight *= 0.3333333;
}
return weight;
}
void main() {
vec2 tex_size = vec2(u.texture_width, u.texture_height);
// filterWidth: equivalente al original (768.0 / TextureSize.y) / 3.0
float filter_width = (768.0 / u.texture_height) / 3.0;
vec2 texcoord = v_uv;
// Curvatura barrel opcional
if (u.enable_curvature != 0) {
texcoord = distort(texcoord, vec2(1.0, 1.0));
if (texcoord.x < 0.0) {
out_color = vec4(0.0, 0.0, 0.0, 1.0);
return;
}
}
vec2 texcoord_in_pixels = texcoord * tex_size;
vec2 tc;
float scan_line_weight;
if (u.enable_sharper != 0) {
// Modo SHARPER: filtrado bicúbico-like con subpixel sharpen
vec2 temp_coord = floor(texcoord_in_pixels) + 0.5;
tc = temp_coord / tex_size;
vec2 deltas = texcoord_in_pixels - temp_coord;
scan_line_weight = calcScanLine(deltas.y, filter_width);
vec2 signs = sign(deltas);
deltas.x *= 2.0;
deltas = deltas * deltas;
deltas.y = deltas.y * deltas.y;
deltas.x *= 0.5;
deltas.y *= 8.0;
deltas /= tex_size;
deltas *= signs;
tc = tc + deltas;
} else {
// Modo estándar
float temp_y = floor(texcoord_in_pixels.y) + 0.5;
float y_coord = temp_y / tex_size.y;
float dy = texcoord_in_pixels.y - temp_y;
scan_line_weight = calcScanLine(dy, filter_width);
float sign_y = sign(dy);
dy = dy * dy;
dy = dy * dy;
dy *= 8.0;
dy /= tex_size.y;
dy *= sign_y;
tc = vec2(texcoord.x, y_coord + dy);
}
vec3 colour = texture(Texture, tc).rgb;
if (u.enable_scanlines != 0) {
if (u.enable_gamma != 0) {
colour = pow(colour, vec3(u.input_gamma));
}
colour *= scan_line_weight * u.bloom_factor;
if (u.enable_gamma != 0) {
colour = pow(colour, vec3(1.0 / u.output_gamma));
}
}
// Máscara de fósforo
if (u.mask_type == 1) {
float which_mask = fract(gl_FragCoord.x * 0.5);
vec3 mask = (which_mask < 0.5)
? vec3(u.mask_brightness, 1.0, u.mask_brightness)
: vec3(1.0, u.mask_brightness, 1.0);
colour *= mask;
} else if (u.mask_type == 2) {
float which_mask = fract(gl_FragCoord.x * 0.3333333);
vec3 mask = vec3(u.mask_brightness);
if (which_mask < 0.3333333)
mask.x = 1.0;
else if (which_mask < 0.6666666)
mask.y = 1.0;
else
mask.z = 1.0;
colour *= mask;
}
out_color = vec4(colour, 1.0);
}

View File

@@ -0,0 +1,48 @@
#version 450
layout(location = 0) in vec2 v_uv;
layout(location = 0) out vec4 out_color;
layout(set = 2, binding = 0) uniform sampler2D source;
layout(set = 3, binding = 0) uniform DownscaleUniforms {
int algorithm; // 0 = Lanczos2 (ventana 2, ±2 taps), 1 = Lanczos3 (ventana 3, ±3 taps)
float pad0;
float pad1;
float pad2;
} u;
// Kernel Lanczos normalizado: sinc(t) * sinc(t/a) para |t| < a, 0 fuera.
float lanczos(float t, float a) {
t = abs(t);
if (t < 0.0001) { return 1.0; }
if (t >= a) { return 0.0; }
const float PI = 3.14159265358979;
float pt = PI * t;
return (a * sin(pt) * sin(pt / a)) / (pt * pt);
}
void main() {
vec2 src_size = vec2(textureSize(source, 0));
// Posición en coordenadas de texel (centros de texel en N+0.5)
vec2 p = v_uv * src_size;
vec2 p_floor = floor(p);
float a = (u.algorithm == 0) ? 2.0 : 3.0;
int win = int(a);
vec4 color = vec4(0.0);
float weight_sum = 0.0;
for (int j = -win; j <= win; j++) {
for (int i = -win; i <= win; i++) {
// Centro del texel (i,j) relativo a p_floor
vec2 tap_center = p_floor + vec2(float(i), float(j)) + 0.5;
vec2 offset = tap_center - p;
float w = lanczos(offset.x, a) * lanczos(offset.y, a);
color += texture(source, tap_center / src_size) * w;
weight_sum += w;
}
}
out_color = (weight_sum > 0.0) ? (color / weight_sum) : vec4(0.0, 0.0, 0.0, 1.0);
}

142
data/shaders/postfx.frag Normal file
View File

@@ -0,0 +1,142 @@
#version 450
// Vulkan GLSL fragment shader — PostFX effects
// Used for SDL3 GPU API (SPIR-V path, Win/Linux).
// Compile: glslc postfx.frag -o postfx.frag.spv
// xxd -i postfx.frag.spv > ../../source/core/rendering/sdl3gpu/postfx_frag_spv.h
//
// PostFXUniforms must match exactly the C++ struct in sdl3gpu_shader.hpp
// (8 floats, 32 bytes, std140/scalar layout).
layout(location = 0) in vec2 v_uv;
layout(location = 0) out vec4 out_color;
layout(set = 2, binding = 0) uniform sampler2D scene;
layout(set = 3, binding = 0) uniform 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; // physical pixels per logical pixel (vh / tex_height_)
float time; // seconds since SDL init
float oversample; // supersampling factor (1.0 = off, 3.0 = 3×SS)
float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz — 48 bytes total (3 × 16)
} u;
// YCbCr helpers for NTSC bleeding
vec3 rgb_to_ycc(vec3 rgb) {
return vec3(
0.299*rgb.r + 0.587*rgb.g + 0.114*rgb.b,
-0.169*rgb.r - 0.331*rgb.g + 0.500*rgb.b + 0.5,
0.500*rgb.r - 0.419*rgb.g - 0.081*rgb.b + 0.5
);
}
vec3 ycc_to_rgb(vec3 ycc) {
float y = ycc.x;
float cb = ycc.y - 0.5;
float cr = ycc.z - 0.5;
return clamp(vec3(
y + 1.402*cr,
y - 0.344*cb - 0.714*cr,
y + 1.772*cb
), 0.0, 1.0);
}
void main() {
vec2 uv = v_uv;
// Curvatura barrel CRT
if (u.curvature > 0.0) {
vec2 c = uv - 0.5;
float rsq = dot(c, c);
vec2 dist = vec2(0.05, 0.1) * u.curvature;
vec2 barrelScale = vec2(1.0) - 0.23 * dist;
c += c * (dist * rsq);
c *= barrelScale;
if (abs(c.x) >= 0.5 || abs(c.y) >= 0.5) {
out_color = vec4(0.0, 0.0, 0.0, 1.0);
return;
}
uv = c + 0.5;
}
// Muestra base
vec3 base = texture(scene, uv).rgb;
// Sangrado NTSC — difuminado horizontal de crominancia.
// step = 1 pixel lógico de juego en UV (corrige SS: textureSize.x = game_w * oversample).
vec3 colour;
if (u.bleeding > 0.0) {
float tw = float(textureSize(scene, 0).x);
float step = u.oversample / tw; // 1 pixel lógico en UV
vec3 ycc = rgb_to_ycc(base);
vec3 ycc_l2 = rgb_to_ycc(texture(scene, uv - vec2(2.0*step, 0.0)).rgb);
vec3 ycc_l1 = rgb_to_ycc(texture(scene, uv - vec2(1.0*step, 0.0)).rgb);
vec3 ycc_r1 = rgb_to_ycc(texture(scene, uv + vec2(1.0*step, 0.0)).rgb);
vec3 ycc_r2 = rgb_to_ycc(texture(scene, uv + vec2(2.0*step, 0.0)).rgb);
ycc.yz = (ycc_l2.yz + ycc_l1.yz*2.0 + ycc.yz*2.0 + ycc_r1.yz*2.0 + ycc_r2.yz) / 8.0;
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.005 * (1.0 + 0.15 * sin(u.time * 7.3));
colour.r = texture(scene, uv + vec2(ca, 0.0)).r;
colour.b = texture(scene, uv - vec2(ca, 0.0)).b;
// Corrección gamma (linealizar antes de scanlines, codificar después)
if (u.gamma_strength > 0.0) {
vec3 lin = pow(colour, vec3(2.4));
colour = mix(colour, lin, u.gamma_strength);
}
// Scanlines — 1 pixel físico oscuro por fila lógica.
// Modelo sustractivo: las filas de scanline se oscurecen, las demás no cambian.
// Esto evita el efecto de sobrebrillo en contenido con colores vivos.
if (u.scanline_strength > 0.0) {
float ps = max(1.0, 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.0, row_pos);
float scan = mix(1.0, 0.0, is_dark);
colour *= mix(1.0, scan, u.scanline_strength);
}
if (u.gamma_strength > 0.0) {
vec3 enc = pow(colour, vec3(1.0 / 2.2));
colour = mix(colour, enc, u.gamma_strength);
}
// Viñeta
vec2 d = uv - 0.5;
float vignette = 1.0 - dot(d, d) * u.vignette_strength;
colour *= clamp(vignette, 0.0, 1.0);
// 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.0) {
float whichMask = fract(gl_FragCoord.x * 0.3333333);
vec3 mask = vec3(0.80);
if (whichMask < 0.3333333)
mask.x = 1.0;
else if (whichMask < 0.6666666)
mask.y = 1.0;
else
mask.z = 1.0;
colour = mix(colour, colour * mask, u.mask_strength);
}
// Parpadeo de fósforo CRT (~50 Hz)
if (u.flicker > 0.0) {
float flicker_wave = sin(u.time * 100.0) * 0.5 + 0.5;
colour *= 1.0 - u.flicker * 0.04 * flicker_wave;
}
out_color = vec4(colour, 1.0);
}

24
data/shaders/postfx.vert Normal file
View File

@@ -0,0 +1,24 @@
#version 450
// Vulkan GLSL vertex shader — postfx full-screen triangle
// Used for SDL3 GPU API (SPIR-V path, Win/Linux).
// Compile: glslc postfx.vert -o postfx.vert.spv
// xxd -i postfx.vert.spv > ../../source/core/rendering/sdl3gpu/postfx_vert_spv.h
layout(location = 0) out vec2 v_uv;
void main() {
// Full-screen triangle (no vertex buffer needed)
const vec2 positions[3] = vec2[3](
vec2(-1.0, -1.0),
vec2( 3.0, -1.0),
vec2(-1.0, 3.0)
);
const vec2 uvs[3] = vec2[3](
vec2(0.0, 1.0),
vec2(2.0, 1.0),
vec2(0.0,-1.0)
);
gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
v_uv = uvs[gl_VertexIndex];
}

15
data/shaders/upscale.frag Normal file
View File

@@ -0,0 +1,15 @@
#version 450
// Vulkan GLSL fragment shader — Nearest-neighbour upscale pass
// Used as the first render pass when supersampling is active.
// Compile: glslc upscale.frag -o upscale.frag.spv
// xxd -i upscale.frag.spv > ../../source/core/rendering/sdl3gpu/upscale_frag_spv.h
layout(location = 0) in vec2 v_uv;
layout(location = 0) out vec4 out_color;
layout(set = 2, binding = 0) uniform sampler2D scene;
void main() {
out_color = texture(scene, v_uv);
}

View File

@@ -34,6 +34,11 @@ enum inputs_e {
input_window_inc_size,
input_window_dec_size,
// GPU / shaders (hotkeys provisionales hasta que haya menú de opciones)
input_toggle_gpu,
input_toggle_shader,
input_toggle_shader_type,
// Input obligatorio
input_number_of_inputs
};

View File

@@ -3,6 +3,7 @@
#include <SDL3/SDL.h>
#include <algorithm> // for max, min
#include <cstring> // for memcpy
#include <iostream> // for basic_ostream, operator<<, cout, endl
#include <string> // for basic_string, char_traits, string
@@ -11,6 +12,10 @@
#include "core/resources/asset.h" // for Asset
#include "core/resources/resource.h"
#ifndef NO_SHADERS
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp" // for Rendering::SDL3GPUShader
#endif
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <emscripten/html5.h>
@@ -69,8 +74,23 @@ Screen::Screen(SDL_Window *window, SDL_Renderer *renderer, Asset *asset, options
// Define el color del borde para el modo de pantalla completa
borderColor = {0x00, 0x00, 0x00};
// Crea la textura donde se dibujan los graficos del juego
gameCanvas = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, gameCanvasWidth, gameCanvasHeight);
// Establece el modo de video (fullscreen/ventana + logical presentation)
// ANTES de crear la textura — SDL3 GPU necesita la logical presentation
// del renderer ya aplicada al swapchain quan es reclama la ventana per a GPU.
// Mirror del pattern de jaildoctors_dilemma (que usa exactament 256×192 i
// funciona) on `initSDLVideo` configura la presentation abans de crear cap
// textura.
setVideoMode(options->videoMode != 0);
// Força al window manager a completar el resize/posicionat abans de passar
// la ventana al dispositiu GPU. Sense açò en Linux/X11 hi ha un race
// condition que deixa el swapchain en estat inestable i fa crashear el
// driver Vulkan en `SDL_CreateGPUGraphicsPipeline`.
SDL_SyncWindow(window);
// Crea la textura donde se dibujan los graficos del juego.
// ARGB8888 per simplificar el readback cap al pipeline SDL3 GPU.
gameCanvas = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, gameCanvasWidth, gameCanvasHeight);
if (gameCanvas != nullptr) {
SDL_SetTextureScaleMode(gameCanvas, options->filter == FILTER_NEAREST ? SDL_SCALEMODE_NEAREST : SDL_SCALEMODE_LINEAR);
}
@@ -80,11 +100,24 @@ Screen::Screen(SDL_Window *window, SDL_Renderer *renderer, Asset *asset, options
}
}
// Establece el modo de video
setVideoMode(options->videoMode != 0);
#ifndef NO_SHADERS
// Buffer de readback del gameCanvas (lo dimensionamos una vez)
pixel_buffer_.resize(static_cast<size_t>(gameCanvasWidth) * static_cast<size_t>(gameCanvasHeight));
#endif
// Inicializa el sistema de notificaciones (Text compartido de Resource)
notificationText = Resource::get()->getText("8bithud");
// Renderiza una vez la textura vacía al renderer abans d'inicialitzar els
// shaders: jaildoctors_dilemma ho fa així i evita que el driver Vulkan
// crashegi en la creació del pipeline gràfic. `initShaders()` es crida
// després des de `Director` amb el swapchain ja estable.
SDL_RenderTexture(renderer, gameCanvas, nullptr, nullptr);
// Estado inicial de las notificaciones. El Text real se enlaza después vía
// `initNotifications()` quan `Resource` ja estigui inicialitzat. Dividim
// això del constructor perquè `initShaders()` (GPU) ha de cridar-se ABANS
// de carregar recursos: si el SDL_Renderer ha fet abans moltes
// allocacions (carrega de textures), el driver Vulkan crasheja quan
// després es reclama la ventana per al dispositiu GPU.
notificationText = nullptr;
notificationMessage = "";
notificationTextColor = {0xFF, 0xFF, 0xFF};
notificationOutlineColor = {0x00, 0x00, 0x00};
@@ -95,9 +128,18 @@ Screen::Screen(SDL_Window *window, SDL_Renderer *renderer, Asset *asset, options
registerEmscriptenEventCallbacks();
}
// Enllaça el Text de les notificacions amb el recurs compartit de `Resource`.
// S'ha de cridar després de `Resource::init(...)`.
void Screen::initNotifications() {
notificationText = Resource::get()->getText("8bithud");
}
// Destructor
Screen::~Screen() {
// notificationText es propiedad de Resource — no liberar.
#ifndef NO_SHADERS
shutdownShaders();
#endif
SDL_DestroyTexture(gameCanvas);
}
@@ -118,6 +160,51 @@ void Screen::blit() {
SDL_SetRenderTarget(renderer, gameCanvas);
renderNotification();
#ifndef NO_SHADERS
// Si el backend GPU està viu i accelerat, passem sempre per ell (tant amb
// shaders com sense). Seguim el mateix pattern que aee_plus: quan shader
// està desactivat, forcem POSTFX + params a zero només per a aquest frame
// i restaurem el shader actiu, així CRTPI no aplica les seues scanlines
// quan l'usuari ho ha desactivat.
if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
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);
if (options->videoShaderEnabled) {
// Ruta normal: shader amb els seus params.
shader_backend_->uploadPixels(pixel_buffer_.data(), gameCanvasWidth, gameCanvasHeight);
shader_backend_->render();
} else {
// Shader off: POSTFX amb params zero (passa-per-aquí). CRTPI no
// val perque sempre aplica els seus efectes interns; salvem i
// restaurem el shader actiu.
const auto PREV_SHADER = shader_backend_->getActiveShader();
if (PREV_SHADER != Rendering::ShaderType::POSTFX) {
shader_backend_->setActiveShader(Rendering::ShaderType::POSTFX);
}
shader_backend_->setPostFXParams(Rendering::PostFXParams{});
shader_backend_->uploadPixels(pixel_buffer_.data(), gameCanvasWidth, gameCanvasHeight);
shader_backend_->render();
if (PREV_SHADER != Rendering::ShaderType::POSTFX) {
shader_backend_->setActiveShader(PREV_SHADER);
}
}
return;
}
#endif
// Vuelve a dejar el renderizador en modo normal
SDL_SetRenderTarget(renderer, nullptr);
@@ -199,6 +286,11 @@ void Screen::toggleIntegerScale() {
void Screen::setVSync(bool enabled) {
options->vSync = enabled;
SDL_SetRenderVSync(renderer, enabled ? 1 : SDL_RENDERER_VSYNC_DISABLED);
#ifndef NO_SHADERS
if (shader_backend_) {
shader_backend_->setVSync(enabled);
}
#endif
}
// Alterna el V-Sync
@@ -321,7 +413,7 @@ void Screen::clearNotification() {
// Dibuja la notificación activa (si la hay) sobre el gameCanvas
void Screen::renderNotification() {
if (SDL_GetTicks() >= notificationEndTime) {
if (notificationText == nullptr || SDL_GetTicks() >= notificationEndTime) {
return;
}
notificationText->writeDX(TXT_CENTER | TXT_COLOR | TXT_STROKE,
@@ -367,3 +459,144 @@ void Screen::registerEmscriptenEventCallbacks() {
emscripten_set_orientationchange_callback(nullptr, EM_TRUE, onEmOrientationChange);
#endif
}
// ============================================================================
// GPU / shaders (SDL3 GPU post-procesado). En builds con NO_SHADERS (Emscripten)
// las operaciones son no-op; la ruta clásica sigue siendo la única disponible.
// ============================================================================
#ifndef NO_SHADERS
// Aplica al backend el preset del shader actiu segons options.
// Només s'ha de cridar quan `videoShaderEnabled=true` (en cas contrari el
// blit() ja força POSTFX+zero params per a desactivar els efectes sense
// tocar els paràmetres emmagatzemats).
void Screen::applyShaderParams() {
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) {
return;
}
const Rendering::ShaderType ACTIVE = options->videoShaderType == 1
? Rendering::ShaderType::CRTPI
: Rendering::ShaderType::POSTFX;
shader_backend_->setActiveShader(ACTIVE);
// Preset per defecte (carregador YAML pendent). Valors estil "CRT" de CCAE.
Rendering::PostFXParams POSTFX;
POSTFX.vignette = 0.15F;
POSTFX.scanlines = 0.7F;
POSTFX.chroma = 0.2F;
shader_backend_->setPostFXParams(POSTFX);
// CrtPi: defaults del struct ja raonables (scanline_weight=6.0, bloom=3.5…).
shader_backend_->setCrtPiParams(Rendering::CrtPiParams{});
}
#endif
void Screen::initShaders() {
#ifndef NO_SHADERS
if (!shader_backend_) {
shader_backend_ = std::make_unique<Rendering::SDL3GPUShader>();
const std::string FALLBACK_DRIVER = "none";
shader_backend_->setPreferredDriver(
options->videoGpuAcceleration ? options->videoGpuPreferredDriver : FALLBACK_DRIVER);
}
if (!shader_backend_->isHardwareAccelerated()) {
const bool ok = shader_backend_->init(window, gameCanvas, "", "");
if (options->console) {
std::cout << "Screen::initShaders: SDL3GPUShader::init() = " << (ok ? "OK" : "FAILED") << '\n';
}
}
if (shader_backend_->isHardwareAccelerated()) {
shader_backend_->setScaleMode(options->integerScale);
shader_backend_->setVSync(options->vSync);
applyShaderParams(); // aplica preset del shader actiu
}
#endif
}
void Screen::shutdownShaders() {
#ifndef NO_SHADERS
// Només es crida des del destructor de Screen. Els toggles runtime NO la
// poden cridar: destruir + recrear el dispositiu SDL3 GPU amb la ventana
// ja reclamada és inestable (Vulkan/Radeon crasheja en el següent claim).
if (shader_backend_) {
shader_backend_->cleanup();
shader_backend_.reset();
}
#endif
}
void Screen::setGpuAcceleration(bool enabled) {
if (options->videoGpuAcceleration == enabled) { return; }
options->videoGpuAcceleration = enabled;
// Soft toggle: el backend es manté viu (vegeu shutdownShaders). El canvi
// s'aplica al proper arrencada. S'emet una notificació perquè l'usuari
// sap que ha tocat la tecla però el canvi no és immediat.
const color_t YELLOW = {0xFF, 0xFF, 0x00};
const color_t BLACK = {0x00, 0x00, 0x00};
const Uint32 DUR_MS = 2500;
notify(enabled ? "GPU: ON (restart)" : "GPU: OFF (restart)", YELLOW, BLACK, DUR_MS);
}
void Screen::toggleGpuAcceleration() {
setGpuAcceleration(!options->videoGpuAcceleration);
}
auto Screen::isGpuAccelerated() const -> bool {
#ifndef NO_SHADERS
return shader_backend_ && shader_backend_->isHardwareAccelerated();
#else
return false;
#endif
}
void Screen::setShaderEnabled(bool enabled) {
if (options->videoShaderEnabled == enabled) { return; }
options->videoShaderEnabled = enabled;
#ifndef NO_SHADERS
if (enabled) {
applyShaderParams(); // restaura preset del shader actiu
}
// Si enabled=false, blit() forçarà POSTFX+zero per frame — no cal tocar
// res ara.
#endif
const color_t CYAN = {0x00, 0xFF, 0xFF};
const color_t BLACK = {0x00, 0x00, 0x00};
const Uint32 DUR_MS = 1500;
notify(enabled ? "Shader: ON" : "Shader: OFF", CYAN, BLACK, DUR_MS);
}
void Screen::toggleShaderEnabled() {
setShaderEnabled(!options->videoShaderEnabled);
}
auto Screen::isShaderEnabled() const -> bool {
return options->videoShaderEnabled;
}
#ifndef NO_SHADERS
void Screen::setActiveShader(Rendering::ShaderType type) {
options->videoShaderType = type == Rendering::ShaderType::CRTPI ? 1 : 0;
if (options->videoShaderEnabled) {
applyShaderParams();
}
const color_t MAGENTA = {0xFF, 0x00, 0xFF};
const color_t BLACK = {0x00, 0x00, 0x00};
const Uint32 DUR_MS = 1500;
notify(type == Rendering::ShaderType::CRTPI ? "Shader: CRTPI" : "Shader: POSTFX", MAGENTA, BLACK, DUR_MS);
}
auto Screen::getActiveShader() const -> Rendering::ShaderType {
return options->videoShaderType == 1 ? Rendering::ShaderType::CRTPI : Rendering::ShaderType::POSTFX;
}
#endif
void Screen::toggleActiveShader() {
#ifndef NO_SHADERS
const Rendering::ShaderType NEXT = getActiveShader() == Rendering::ShaderType::POSTFX
? Rendering::ShaderType::CRTPI
: Rendering::ShaderType::POSTFX;
setActiveShader(NEXT);
#else
options->videoShaderType = options->videoShaderType == 1 ? 0 : 1;
#endif
}

View File

@@ -2,9 +2,19 @@
#include <SDL3/SDL.h>
#include <memory> // for unique_ptr
#include <string> // for string
#include <vector> // for vector
#include "utils/utils.h" // for color_t
#ifndef NO_SHADERS
#include "core/rendering/shader_backend.hpp" // for Rendering::ShaderType
namespace Rendering {
class ShaderBackend;
}
#endif
class Asset;
class Text;
@@ -50,9 +60,25 @@ class Screen {
void setBorderColor(color_t color); // Cambia el color del borde
// Notificaciones
void initNotifications(); // Enllaça el Text de notificacions amb `Resource`. A cridar després de `Resource::init(...)`.
void notify(const std::string &text, color_t textColor, color_t outlineColor, Uint32 durationMs); // Muestra una notificación en la línea superior del canvas durante durationMs. Sobrescribe cualquier notificación activa (sin apilación).
void clearNotification(); // Limpia la notificación actual
// GPU / shaders (post-procesado). En builds con NO_SHADERS (Emscripten) son no-op.
void initShaders(); // Crea el backend GPU si no existe y lo inicializa
void shutdownShaders(); // Libera el backend GPU
void setGpuAcceleration(bool enabled); // Crea/destruye el backend según valor, persiste options
void toggleGpuAcceleration(); // Alterna aceleración GPU
auto isGpuAccelerated() const -> bool; // true si el backend existe y reporta hardware OK
void setShaderEnabled(bool enabled); // Activa o desactiva el post-procesado (persiste)
void toggleShaderEnabled(); // Alterna post-procesado
auto isShaderEnabled() const -> bool; // Estado actual (lee options)
#ifndef NO_SHADERS
void setActiveShader(Rendering::ShaderType type); // POSTFX o CRTPI
auto getActiveShader() const -> Rendering::ShaderType;
#endif
void toggleActiveShader(); // Alterna POSTFX ↔ CRTPI
private:
// Helpers internos de setVideoMode
void applyFullscreen(bool fullscreen); // SDL_SetWindowFullscreen + visibilidad del cursor
@@ -67,6 +93,12 @@ class Screen {
// Notificaciones
void renderNotification(); // Dibuja la notificación activa (si la hay) sobre el gameCanvas
#ifndef NO_SHADERS
// Aplica els paràmetres actuals del shader al backend segons options
// (pass-through si `videoShaderEnabled==false`, preset per defecte si true).
void applyShaderParams();
#endif
// Objetos y punteros
SDL_Window *window; // Ventana de la aplicación
SDL_Renderer *renderer; // El renderizador de la ventana
@@ -91,4 +123,10 @@ class Screen {
color_t notificationOutlineColor; // Color del outline
Uint32 notificationEndTime; // SDL_GetTicks() hasta el cual se muestra
int notificationY; // Fila vertical en el canvas virtual
#ifndef NO_SHADERS
// GPU / shaders
std::unique_ptr<Rendering::ShaderBackend> shader_backend_; // Backend GPU (nullptr si inactivo)
std::vector<Uint32> pixel_buffer_; // Buffer de readback del gameCanvas (ARGB8888)
#endif
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,178 @@
#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.
// 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)
};
// CrtPi uniforms pushed to fragment stage each frame.
// Must match the MSL struct and GLSL uniform block layout.
// 14 fields (8 floats + 6 ints) + 2 floats (texture size) = 16 fields = 64 bytes — 4 × 16-byte alignment.
struct CrtPiUniforms {
// vec4 #0
float scanline_weight; // Ajuste gaussiano (default 6.0)
float scanline_gap_brightness; // Brillo mínimo entre scanlines (default 0.12)
float bloom_factor; // Factor brillo zonas iluminadas (default 3.5)
float input_gamma; // Gamma de entrada (default 2.4)
// vec4 #1
float output_gamma; // Gamma de salida (default 2.2)
float mask_brightness; // Brillo sub-píxeles máscara (default 0.80)
float curvature_x; // Distorsión barrel X (default 0.05)
float curvature_y; // Distorsión barrel Y (default 0.10)
// vec4 #2
int mask_type; // 0=ninguna, 1=verde/magenta, 2=RGB fósforo
int enable_scanlines; // 0 = off, 1 = on
int enable_multisample; // 0 = off, 1 = on (antialiasing analítico)
int enable_gamma; // 0 = off, 1 = on
// vec4 #3
int enable_curvature; // 0 = off, 1 = on
int enable_sharper; // 0 = off, 1 = on
float texture_width; // Ancho del canvas en píxeles (inyectado en render)
float texture_height; // Alto del canvas en píxeles (inyectado en render)
};
// Downscale uniforms pushed to the Lanczos downscale fragment stage.
// 1 int + 3 floats = 16 bytes — meets Metal/Vulkan alignment.
struct DownscaleUniforms {
int algorithm; // 0 = Lanczos2 (ventana 2), 1 = Lanczos3 (ventana 3)
float pad0;
float pad1;
float pad2;
};
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_; }
[[nodiscard]] auto getDriverName() const -> std::string override { return driver_name_; }
// Establece el driver GPU preferido (vacío = auto). Debe llamarse antes de init().
void setPreferredDriver(const std::string& driver) override { preferred_driver_ = driver; }
// 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;
// Activa/desactiva interpolación LINEAR en el upscale (false = NEAREST)
void setLinearUpscale(bool linear) override;
// Selecciona algoritmo de downscale: 0=bilinear legacy, 1=Lanczos2, 2=Lanczos3
void setDownscaleAlgo(int algo) override;
// Devuelve las dimensiones de la textura de supersampling (0,0 si SS desactivado)
[[nodiscard]] auto getSsTextureSize() const -> std::pair<int, int> override;
// Selecciona el shader de post-procesado activo (POSTFX o CRTPI)
void setActiveShader(ShaderType type) override;
// Actualiza los parámetros del shader CRT-Pi
void setCrtPiParams(const CrtPiParams& p) override;
// Devuelve el shader activo
[[nodiscard]] auto getActiveShader() const -> ShaderType override { return active_shader_; }
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 createCrtPiPipeline() -> bool; // Pipeline dedicado para el shader CrtPi
auto reinitTexturesAndBuffer() -> bool; // Recrea scene_texture_ y upload_buffer_
auto recreateScaledTexture(int factor) -> bool; // Recrea scaled_texture_ para factor dado
static auto calcSsFactor(float zoom) -> int; // Primer múltiplo de 3 >= zoom (mín 3)
// Devuelve el mejor present mode disponible: IMMEDIATE > MAILBOX > VSYNC
[[nodiscard]] auto bestPresentMode(bool vsync) const -> SDL_GPUPresentMode;
SDL_Window* window_ = nullptr;
SDL_GPUDevice* device_ = nullptr;
SDL_GPUGraphicsPipeline* pipeline_ = nullptr; // PostFX pass (→ swapchain o → postfx_texture_)
SDL_GPUGraphicsPipeline* crtpi_pipeline_ = nullptr; // CrtPi pass (→ swapchain directo, sin SS)
SDL_GPUGraphicsPipeline* postfx_offscreen_pipeline_ = nullptr; // PostFX → postfx_texture_ (B8G8R8A8, solo con Lanczos)
SDL_GPUGraphicsPipeline* upscale_pipeline_ = nullptr; // Upscale pass (solo con SS)
SDL_GPUGraphicsPipeline* downscale_pipeline_ = nullptr; // Lanczos downscale (solo con SS + algo > 0)
SDL_GPUTexture* scene_texture_ = nullptr; // Canvas del juego (game_width_ × game_height_)
SDL_GPUTexture* scaled_texture_ = nullptr; // Upscale target (game×factor), solo con SS
SDL_GPUTexture* postfx_texture_ = nullptr; // PostFX output a resolución escalada, solo con Lanczos
SDL_GPUTransferBuffer* upload_buffer_ = nullptr;
SDL_GPUSampler* sampler_ = nullptr; // NEAREST
SDL_GPUSampler* linear_sampler_ = nullptr; // LINEAR
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};
CrtPiUniforms crtpi_uniforms_{.scanline_weight = 6.0F, .scanline_gap_brightness = 0.12F, .bloom_factor = 3.5F, .input_gamma = 2.4F, .output_gamma = 2.2F, .mask_brightness = 0.80F, .curvature_x = 0.05F, .curvature_y = 0.10F, .mask_type = 2, .enable_scanlines = 1, .enable_multisample = 1, .enable_gamma = 1};
ShaderType active_shader_ = ShaderType::POSTFX; // Shader de post-procesado activo
int game_width_ = 0; // Dimensiones originales del canvas
int game_height_ = 0;
int ss_factor_ = 0; // Factor SS activo (3, 6, 9...) o 0 si SS desactivado
int oversample_ = 1; // SS on/off (1 = off, >1 = on)
int downscale_algo_ = 1; // 0 = bilinear legacy, 1 = Lanczos2, 2 = Lanczos3
std::string driver_name_;
std::string preferred_driver_; // Driver preferido; vacío = auto (SDL elige)
bool is_initialized_ = false;
bool vsync_ = true;
bool integer_scale_ = false;
bool linear_upscale_ = false; // Upscale NEAREST (false) o LINEAR (true)
};
} // namespace Rendering

View File

@@ -0,0 +1,2 @@
DisableFormat: true
SortIncludes: Never

View File

@@ -0,0 +1,4 @@
# source/core/rendering/sdl3gpu/spv/.clang-tidy
Checks: '-*'
WarningsAsErrors: ''
HeaderFilterRegex: ''

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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,633 @@
#pragma once
#include <cstddef>
#include <cstdint>
static const uint8_t kupscale_frag_spv[] = {
0x03,
0x02,
0x23,
0x07,
0x00,
0x00,
0x01,
0x00,
0x0b,
0x00,
0x0d,
0x00,
0x14,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x11,
0x00,
0x02,
0x00,
0x01,
0x00,
0x00,
0x00,
0x0b,
0x00,
0x06,
0x00,
0x01,
0x00,
0x00,
0x00,
0x47,
0x4c,
0x53,
0x4c,
0x2e,
0x73,
0x74,
0x64,
0x2e,
0x34,
0x35,
0x30,
0x00,
0x00,
0x00,
0x00,
0x0e,
0x00,
0x03,
0x00,
0x00,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x0f,
0x00,
0x07,
0x00,
0x04,
0x00,
0x00,
0x00,
0x04,
0x00,
0x00,
0x00,
0x6d,
0x61,
0x69,
0x6e,
0x00,
0x00,
0x00,
0x00,
0x09,
0x00,
0x00,
0x00,
0x11,
0x00,
0x00,
0x00,
0x10,
0x00,
0x03,
0x00,
0x04,
0x00,
0x00,
0x00,
0x07,
0x00,
0x00,
0x00,
0x03,
0x00,
0x03,
0x00,
0x02,
0x00,
0x00,
0x00,
0xc2,
0x01,
0x00,
0x00,
0x04,
0x00,
0x0a,
0x00,
0x47,
0x4c,
0x5f,
0x47,
0x4f,
0x4f,
0x47,
0x4c,
0x45,
0x5f,
0x63,
0x70,
0x70,
0x5f,
0x73,
0x74,
0x79,
0x6c,
0x65,
0x5f,
0x6c,
0x69,
0x6e,
0x65,
0x5f,
0x64,
0x69,
0x72,
0x65,
0x63,
0x74,
0x69,
0x76,
0x65,
0x00,
0x00,
0x04,
0x00,
0x08,
0x00,
0x47,
0x4c,
0x5f,
0x47,
0x4f,
0x4f,
0x47,
0x4c,
0x45,
0x5f,
0x69,
0x6e,
0x63,
0x6c,
0x75,
0x64,
0x65,
0x5f,
0x64,
0x69,
0x72,
0x65,
0x63,
0x74,
0x69,
0x76,
0x65,
0x00,
0x05,
0x00,
0x04,
0x00,
0x04,
0x00,
0x00,
0x00,
0x6d,
0x61,
0x69,
0x6e,
0x00,
0x00,
0x00,
0x00,
0x05,
0x00,
0x05,
0x00,
0x09,
0x00,
0x00,
0x00,
0x6f,
0x75,
0x74,
0x5f,
0x63,
0x6f,
0x6c,
0x6f,
0x72,
0x00,
0x00,
0x00,
0x05,
0x00,
0x04,
0x00,
0x0d,
0x00,
0x00,
0x00,
0x73,
0x63,
0x65,
0x6e,
0x65,
0x00,
0x00,
0x00,
0x05,
0x00,
0x04,
0x00,
0x11,
0x00,
0x00,
0x00,
0x76,
0x5f,
0x75,
0x76,
0x00,
0x00,
0x00,
0x00,
0x47,
0x00,
0x04,
0x00,
0x09,
0x00,
0x00,
0x00,
0x1e,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x47,
0x00,
0x04,
0x00,
0x0d,
0x00,
0x00,
0x00,
0x21,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x47,
0x00,
0x04,
0x00,
0x0d,
0x00,
0x00,
0x00,
0x22,
0x00,
0x00,
0x00,
0x02,
0x00,
0x00,
0x00,
0x47,
0x00,
0x04,
0x00,
0x11,
0x00,
0x00,
0x00,
0x1e,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x13,
0x00,
0x02,
0x00,
0x02,
0x00,
0x00,
0x00,
0x21,
0x00,
0x03,
0x00,
0x03,
0x00,
0x00,
0x00,
0x02,
0x00,
0x00,
0x00,
0x16,
0x00,
0x03,
0x00,
0x06,
0x00,
0x00,
0x00,
0x20,
0x00,
0x00,
0x00,
0x17,
0x00,
0x04,
0x00,
0x07,
0x00,
0x00,
0x00,
0x06,
0x00,
0x00,
0x00,
0x04,
0x00,
0x00,
0x00,
0x20,
0x00,
0x04,
0x00,
0x08,
0x00,
0x00,
0x00,
0x03,
0x00,
0x00,
0x00,
0x07,
0x00,
0x00,
0x00,
0x3b,
0x00,
0x04,
0x00,
0x08,
0x00,
0x00,
0x00,
0x09,
0x00,
0x00,
0x00,
0x03,
0x00,
0x00,
0x00,
0x19,
0x00,
0x09,
0x00,
0x0a,
0x00,
0x00,
0x00,
0x06,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x1b,
0x00,
0x03,
0x00,
0x0b,
0x00,
0x00,
0x00,
0x0a,
0x00,
0x00,
0x00,
0x20,
0x00,
0x04,
0x00,
0x0c,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x0b,
0x00,
0x00,
0x00,
0x3b,
0x00,
0x04,
0x00,
0x0c,
0x00,
0x00,
0x00,
0x0d,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x17,
0x00,
0x04,
0x00,
0x0f,
0x00,
0x00,
0x00,
0x06,
0x00,
0x00,
0x00,
0x02,
0x00,
0x00,
0x00,
0x20,
0x00,
0x04,
0x00,
0x10,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x0f,
0x00,
0x00,
0x00,
0x3b,
0x00,
0x04,
0x00,
0x10,
0x00,
0x00,
0x00,
0x11,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x36,
0x00,
0x05,
0x00,
0x02,
0x00,
0x00,
0x00,
0x04,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x03,
0x00,
0x00,
0x00,
0xf8,
0x00,
0x02,
0x00,
0x05,
0x00,
0x00,
0x00,
0x3d,
0x00,
0x04,
0x00,
0x0b,
0x00,
0x00,
0x00,
0x0e,
0x00,
0x00,
0x00,
0x0d,
0x00,
0x00,
0x00,
0x3d,
0x00,
0x04,
0x00,
0x0f,
0x00,
0x00,
0x00,
0x12,
0x00,
0x00,
0x00,
0x11,
0x00,
0x00,
0x00,
0x57,
0x00,
0x05,
0x00,
0x07,
0x00,
0x00,
0x00,
0x13,
0x00,
0x00,
0x00,
0x0e,
0x00,
0x00,
0x00,
0x12,
0x00,
0x00,
0x00,
0x3e,
0x00,
0x03,
0x00,
0x09,
0x00,
0x00,
0x00,
0x13,
0x00,
0x00,
0x00,
0xfd,
0x00,
0x01,
0x00,
0x38,
0x00,
0x01,
0x00};
static const size_t kupscale_frag_spv_size = 628;

View File

@@ -0,0 +1,175 @@
#pragma once
#include <SDL3/SDL.h>
#include <string>
#include <utility>
namespace Rendering {
/** @brief Identificador del shader de post-procesado activo */
enum class ShaderType { POSTFX,
CRTPI };
/**
* @brief Parámetros de intensidad de los efectos PostFX
* Definido a nivel de namespace para facilitar el uso desde subclases y screen.cpp
*/
struct PostFXParams {
float vignette = 0.0F; // Intensidad de la viñeta
float scanlines = 0.0F; // Intensidad de las scanlines
float chroma = 0.0F; // Aberración cromática
float mask = 0.0F; // Máscara de fósforo RGB
float gamma = 0.0F; // Corrección gamma (blend 0=off, 1=full)
float curvature = 0.0F; // Curvatura barrel CRT
float bleeding = 0.0F; // Sangrado de color NTSC
float flicker = 0.0F; // Parpadeo de fósforo CRT ~50 Hz
};
/**
* @brief Parámetros del shader CRT-Pi (algoritmo de scanlines continuas)
* Diferente al PostFX: usa pesos gaussianos por distancia subpixel y bloom.
*/
struct CrtPiParams {
float scanline_weight{6.0F}; // Ajuste gaussiano (mayor = scanlines más estrechas)
float scanline_gap_brightness{0.12F}; // Brillo mínimo en las ranuras entre scanlines
float bloom_factor{3.5F}; // Factor de brillo para zonas iluminadas
float input_gamma{2.4F}; // Gamma de entrada (linealización)
float output_gamma{2.2F}; // Gamma de salida (codificación)
float mask_brightness{0.80F}; // Sub-píxeles tenues en la máscara de fósforo
float curvature_x{0.05F}; // Distorsión barrel eje X
float curvature_y{0.10F}; // Distorsión barrel eje Y
int mask_type{2}; // 0=ninguna, 1=verde/magenta, 2=RGB fósforo
bool enable_scanlines{true}; // Activar efecto de scanlines
bool enable_multisample{true}; // Antialiasing analítico de scanlines
bool enable_gamma{true}; // Corrección gamma
bool enable_curvature{false}; // Distorsión barrel CRT
bool enable_sharper{false}; // Submuestreo más nítido (modo SHARPER)
};
/**
* @brief Interfaz abstracta para backends de renderizado con shaders
*
* Esta interfaz define el contrato que todos los backends de shaders
* deben cumplir (OpenGL, Metal, Vulkan, etc.)
*/
class ShaderBackend {
public:
virtual ~ShaderBackend() = default;
/**
* @brief Inicializa el backend de shaders
* @param window Ventana SDL
* @param texture Textura de backbuffer a la que aplicar shaders
* @param vertex_source Código fuente del vertex shader
* @param fragment_source Código fuente del fragment shader
* @return true si la inicialización fue exitosa
*/
virtual auto init(SDL_Window* window,
SDL_Texture* texture,
const std::string& vertex_source,
const std::string& fragment_source) -> bool = 0;
/**
* @brief Renderiza la textura con los shaders aplicados
*/
virtual void render() = 0;
/**
* @brief Establece el tamaño de la textura como parámetro del shader
* @param width Ancho de la textura
* @param height Alto de la textura
*/
virtual void setTextureSize(float width, float height) = 0;
/**
* @brief Limpia y libera recursos del backend
*/
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 p Struct con todos los parámetros PostFX
*/
virtual void setPostFXParams(const PostFXParams& /*p*/) {}
/**
* @brief Activa o desactiva VSync en el swapchain del GPU device
*/
virtual void setVSync(bool /*vsync*/) {}
/**
* @brief Activa o desactiva el escalado entero (integer scale)
*/
virtual void setScaleMode(bool /*integer_scale*/) {}
/**
* @brief Establece el factor de supersampling (1 = off, 3 = 3× SS)
* Con factor > 1, la textura GPU se crea a game×factor resolución y
* las scanlines se hornean en CPU (uploadPixels). El sampler usa LINEAR.
*/
virtual void setOversample(int /*factor*/) {}
/**
* @brief Activa/desactiva interpolación LINEAR en el paso de upscale (SS).
* Por defecto NEAREST (false). Solo tiene efecto con supersampling activo.
*/
virtual void setLinearUpscale(bool /*linear*/) {}
[[nodiscard]] virtual auto isLinearUpscale() const -> bool { return false; }
/**
* @brief Selecciona el algoritmo de downscale tras el PostFX (SS activo).
* 0 = bilinear legacy (comportamiento actual, sin textura intermedia),
* 1 = Lanczos2 (ventana 2, ~25 muestras), 2 = Lanczos3 (ventana 3, ~49 muestras).
*/
virtual void setDownscaleAlgo(int /*algo*/) {}
[[nodiscard]] virtual auto getDownscaleAlgo() const -> int { return 0; }
/**
* @brief Devuelve las dimensiones de la textura de supersampling.
* @return Par (ancho, alto) en píxeles; (0, 0) si SS está desactivado.
*/
[[nodiscard]] virtual auto getSsTextureSize() const -> std::pair<int, int> { return {0, 0}; }
/**
* @brief Verifica si el backend está usando aceleración por hardware
* @return true si usa aceleración (OpenGL/Metal/Vulkan)
*/
[[nodiscard]] virtual auto isHardwareAccelerated() const -> bool = 0;
/**
* @brief Nombre del driver GPU activo (p.ej. "vulkan", "metal", "direct3d12")
* @return Cadena vacía si no disponible
*/
[[nodiscard]] virtual auto getDriverName() const -> std::string { return {}; }
/**
* @brief Establece el driver GPU preferido antes de init().
* Vacío = selección automática de SDL. Implementado en SDL3GPUShader.
*/
virtual void setPreferredDriver(const std::string& /*driver*/) {}
/**
* @brief Selecciona el shader de post-procesado activo (POSTFX o CRTPI).
* Debe llamarse antes de render(). No recrea pipelines.
*/
virtual void setActiveShader(ShaderType /*type*/) {}
/**
* @brief Establece los parámetros del shader CRT-Pi.
*/
virtual void setCrtPiParams(const CrtPiParams& /*p*/) {}
/**
* @brief Devuelve el shader de post-procesado activo.
*/
[[nodiscard]] virtual auto getActiveShader() const -> ShaderType { return ShaderType::POSTFX; }
};
} // namespace Rendering

View File

@@ -114,12 +114,29 @@ Director::Director(int argc, const char *argv[]) {
#endif
initInput();
// Precarga todos los recursos en memoria (texturas, sonidos, música, ...).
// Vivirán durante toda la vida de la app; las escenas leen handles compartidos.
// Debe ir antes de crear Screen porque Screen usa Text (propiedad de Resource).
// Orden importante: Screen + initShaders ANTES de Resource::init.
// Si `Resource::init` se ejecuta primero, carga ~100 texturas vía
// `SDL_CreateTexture` que dejan el SDL_Renderer con el swapchain en un
// estado que hace crashear al driver Vulkan cuando después `initShaders`
// intenta reclamar la ventana para el dispositivo SDL3 GPU.
//
// Por eso el constructor de Screen NO carga notificationText desde
// Resource; se enlaza después vía `screen->initNotifications()`.
screen = new Screen(window, renderer, asset, options);
#ifndef NO_SHADERS
if (options->videoGpuAcceleration) {
screen->initShaders();
}
#endif
// Ahora sí, precarga todos los recursos en memoria (texturas, sonidos,
// música, ...). Vivirán durante toda la vida de la app.
Resource::init(renderer, asset, input);
screen = new Screen(window, renderer, asset, options);
// Completa el enlazado de Screen con recursos que necesitan Resource
// inicializado (actualmente sólo el Text de las notificaciones).
screen->initNotifications();
activeSection = ActiveSection::None;
}
@@ -183,6 +200,9 @@ void Director::initInput() {
input->bindKey(input_window_dec_size, SDL_SCANCODE_F1);
input->bindKey(input_window_inc_size, SDL_SCANCODE_F2);
input->bindKey(input_window_fullscreen, SDL_SCANCODE_F3);
input->bindKey(input_toggle_gpu, SDL_SCANCODE_F9);
input->bindKey(input_toggle_shader, SDL_SCANCODE_F10);
input->bindKey(input_toggle_shader_type, SDL_SCANCODE_F11);
// Mando - Movimiento del jugador
input->bindGameControllerButton(input_up, SDL_GAMEPAD_BUTTON_DPAD_UP);
@@ -250,6 +270,10 @@ bool Director::initSDL() {
}
success = false;
} else {
// Modo de blending por defecto (consistente con CCAE):
// permite alpha blending para fades y notificaciones.
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
// Activa vsync si es necesario
if (options->vSync) {
SDL_SetRenderVSync(renderer, 1);
@@ -626,6 +650,12 @@ bool Director::saveConfigFile() {
file << "borderWidth=" + std::to_string(options->borderWidth) + "\n";
file << "borderHeight=" + std::to_string(options->borderHeight) + "\n";
// Opciones de GPU / shaders (post-procesado)
file << "videoGpuAcceleration=" + boolToString(options->videoGpuAcceleration) + "\n";
file << "videoGpuPreferredDriver=" + options->videoGpuPreferredDriver + "\n";
file << "videoShaderEnabled=" + boolToString(options->videoShaderEnabled) + "\n";
file << "videoShaderType=" + std::to_string(options->videoShaderType) + "\n";
// Otras opciones del programa
file << "\n## OTHER OPTIONS\n";
file << "language=" + std::to_string(options->language) + "\n";
@@ -835,6 +865,26 @@ bool Director::setOptions(options_t *options, std::string var, std::string value
options->borderHeight = std::stoi(value);
}
// Opciones de GPU / shaders
else if (var == "videoGpuAcceleration") {
options->videoGpuAcceleration = stringToBool(value);
}
else if (var == "videoGpuPreferredDriver") {
options->videoGpuPreferredDriver = value;
}
else if (var == "videoShaderEnabled") {
options->videoShaderEnabled = stringToBool(value);
}
else if (var == "videoShaderType") {
options->videoShaderType = std::stoi(value);
if (options->videoShaderType < 0 || options->videoShaderType > 1) {
options->videoShaderType = 0;
}
}
// Opciones varias
else if (var == "language") {
options->language = std::stoi(value);

14726
source/external/fkyaml_node.hpp vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -2530,6 +2530,18 @@ void Game::checkGameInput() {
screen->incWindowZoom();
}
else if (input->checkInput(input_toggle_gpu, REPEAT_FALSE)) {
screen->toggleGpuAcceleration();
}
else if (input->checkInput(input_toggle_shader, REPEAT_FALSE)) {
screen->toggleShaderEnabled();
}
else if (input->checkInput(input_toggle_shader_type, REPEAT_FALSE)) {
screen->toggleActiveShader();
}
// Modo Demo activo
if (demo.enabled) {
const int index = 0;

View File

@@ -232,6 +232,18 @@ void Instructions::checkInput() {
screen->incWindowZoom();
}
else if (input->checkInput(input_toggle_gpu, REPEAT_FALSE)) {
screen->toggleGpuAcceleration();
}
else if (input->checkInput(input_toggle_shader, REPEAT_FALSE)) {
screen->toggleShaderEnabled();
}
else if (input->checkInput(input_toggle_shader_type, REPEAT_FALSE)) {
screen->toggleActiveShader();
}
else if (input->checkInput(input_pause, REPEAT_FALSE) || input->checkInput(input_accept, REPEAT_FALSE) || input->checkInput(input_fire_left, REPEAT_FALSE) || input->checkInput(input_fire_center, REPEAT_FALSE) || input->checkInput(input_fire_right, REPEAT_FALSE)) {
if (mode == m_auto) {
finished = true;

View File

@@ -208,6 +208,18 @@ void Intro::checkInput() {
screen->incWindowZoom();
}
else if (input->checkInput(input_toggle_gpu, REPEAT_FALSE)) {
screen->toggleGpuAcceleration();
}
else if (input->checkInput(input_toggle_shader, REPEAT_FALSE)) {
screen->toggleShaderEnabled();
}
else if (input->checkInput(input_toggle_shader_type, REPEAT_FALSE)) {
screen->toggleActiveShader();
}
else if (input->checkInput(input_pause, REPEAT_FALSE) || input->checkInput(input_accept, REPEAT_FALSE) || input->checkInput(input_fire_left, REPEAT_FALSE) || input->checkInput(input_fire_center, REPEAT_FALSE) || input->checkInput(input_fire_right, REPEAT_FALSE)) {
JA_StopMusic();
section->name = SECTION_PROG_TITLE;

View File

@@ -91,6 +91,18 @@ void Logo::checkInput() {
screen->incWindowZoom();
}
else if (input->checkInput(input_toggle_gpu, REPEAT_FALSE)) {
screen->toggleGpuAcceleration();
}
else if (input->checkInput(input_toggle_shader, REPEAT_FALSE)) {
screen->toggleShaderEnabled();
}
else if (input->checkInput(input_toggle_shader_type, REPEAT_FALSE)) {
screen->toggleActiveShader();
}
else if (input->checkInput(input_pause, REPEAT_FALSE) || input->checkInput(input_accept, REPEAT_FALSE) || input->checkInput(input_fire_left, REPEAT_FALSE) || input->checkInput(input_fire_center, REPEAT_FALSE) || input->checkInput(input_fire_right, REPEAT_FALSE)) {
section->name = SECTION_PROG_TITLE;
section->subsection = SUBSECTION_TITLE_1;

View File

@@ -236,10 +236,14 @@ void Title::update() {
if (coffeeBitmap->hasFinished() && crisisBitmap->hasFinished()) {
section->subsection = SUBSECTION_TITLE_2;
// Pantallazo blanco
// Pantallazo blanco: pintar sobre el gameCanvas y dejar
// que Screen::blit() presente por la ruta activa (GPU o
// SDL_Renderer). Un `SDL_RenderPresent(renderer)` directe
// crasheja quan el SDL3 GPU ha reclamat la ventana.
screen->start();
SDL_SetRenderDrawColor(renderer, 0xFF, 0xFF, 0xFF, 0xFF);
SDL_RenderClear(renderer);
SDL_RenderPresent(renderer);
screen->blit();
// Reproduce el efecto sonoro
JA_PlaySound(crashSound);
@@ -650,6 +654,18 @@ void Title::checkInput() {
else if (input->checkInput(input_window_inc_size, REPEAT_FALSE)) {
screen->incWindowZoom();
}
else if (input->checkInput(input_toggle_gpu, REPEAT_FALSE)) {
screen->toggleGpuAcceleration();
}
else if (input->checkInput(input_toggle_shader, REPEAT_FALSE)) {
screen->toggleShaderEnabled();
}
else if (input->checkInput(input_toggle_shader_type, REPEAT_FALSE)) {
screen->toggleActiveShader();
}
}
// Actualiza el tileado de fondo

View File

@@ -109,6 +109,13 @@ struct options_t {
palette_e palette; // Paleta de colores a usar en el juego
bool console; // Indica si ha de mostrar información por la consola de texto
// GPU / shaders (persistent; also toggled via F9/F10/F11 hotkeys).
// YAML migration pending — for now they live as flat keys in config.txt.
bool videoGpuAcceleration = true; // Intenta usar SDL3 GPU si está disponible
std::string videoGpuPreferredDriver; // Driver preferido para SDL3 GPU (vacío = auto)
bool videoShaderEnabled = false; // Activa el post-procesado (PostFX / CrtPi)
int videoShaderType = 0; // 0 = POSTFX, 1 = CRTPI
op_screen_t screen; // Opciones relativas a la clase screen
};

View File

@@ -0,0 +1,57 @@
# compile_shader.cmake
# Compila un shader GLSL a header C++ embebible con datos SPIR-V.
# Funciona en Windows, Linux y macOS sin bash ni herramientas Unix.
#
# Variables requeridas (pasar con -D al invocar cmake -P):
# GLSLC - ruta al ejecutable glslc
# SRC - archivo fuente GLSL
# OUT_H - archivo header de salida
# VAR - nombre base de la variable C++ (e.g. "postfx_vert_spv")
# STAGE - (opcional) stage explícito para glslc (e.g. "fragment")
cmake_minimum_required(VERSION 3.10)
get_filename_component(SRC_NAME "${SRC}" NAME_WE)
get_filename_component(OUT_DIR "${OUT_H}" DIRECTORY)
set(OUT_SPV "${OUT_DIR}/${SRC_NAME}.spv")
# Compilar GLSL → SPIR-V
if(DEFINED STAGE AND NOT STAGE STREQUAL "")
execute_process(
COMMAND "${GLSLC}" "-fshader-stage=${STAGE}" "${SRC}" -o "${OUT_SPV}"
RESULT_VARIABLE RESULT
ERROR_VARIABLE ERROR_MSG
)
else()
execute_process(
COMMAND "${GLSLC}" "${SRC}" -o "${OUT_SPV}"
RESULT_VARIABLE RESULT
ERROR_VARIABLE ERROR_MSG
)
endif()
if(NOT RESULT EQUAL 0)
message(FATAL_ERROR "glslc falló compilando ${SRC}:\n${ERROR_MSG}")
endif()
# Leer binario SPIR-V como cadena hexadecimal
file(READ "${OUT_SPV}" SPV_HEX HEX)
file(REMOVE "${OUT_SPV}")
string(LENGTH "${SPV_HEX}" HEX_LEN)
math(EXPR BYTE_COUNT "${HEX_LEN} / 2")
# Convertir a array C++ con formato " 0xXX,\n" por byte
string(REGEX REPLACE "([0-9a-f][0-9a-f])" " 0x\\1,\n" SPV_BYTES "${SPV_HEX}")
# Eliminar la última coma y sustituir por el "}" de cierre
string(REGEX REPLACE ",\n$" "}" SPV_BYTES "${SPV_BYTES}")
# Escribir el header
file(WRITE "${OUT_H}"
"#pragma once\n"
"#include <cstddef>\n"
"#include <cstdint>\n"
"static const uint8_t k${VAR}[] = {\n"
"${SPV_BYTES};\n"
"static const size_t k${VAR}_size = ${BYTE_COUNT};\n"
)