Compare commits
25 Commits
tidy-cleanup
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| eb8af006b0 | |||
| 4af56d9bce | |||
| 998bc6f1fa | |||
| 753fff5f59 | |||
| 02bc4de6d5 | |||
| 5349c60c39 | |||
| 25a6832351 | |||
| 28fc8456e0 | |||
| 6b6d5f1f6d | |||
| f3371c33b0 | |||
| 441a2122bc | |||
| 502127283b | |||
| 0fb9be931f | |||
| 1d46c4f3bd | |||
| dbef9c558d | |||
| 64586f4a86 | |||
| ab20e98663 | |||
| ae6e72e0d9 | |||
| bd5683d498 | |||
| 11eec8f222 | |||
| 2c1673d2dd | |||
| e58b7d36fb | |||
| 0d14e10de5 | |||
| a39cd45bf1 | |||
| ab858aacb8 |
@@ -4,6 +4,22 @@ Historial de canvis i novetats de Coffee Crisis Arcade Edition.
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-17
|
||||
|
||||
- **Migració del shader PostFX a la versió d'aee_arcade**: filtratge analític de scanlines amb `smoothstep` (paràmetres `scan_dark_ratio`, `scan_dark_floor`, `scan_edge_soft` exposats per preset). Eliminat tot el pipeline de supersampling 3× + Lanczos downscale (shaders, render targets, opcions de menu, claus de YAML, keybinding F0, llengües). 1 sol render pass vs 3 anteriors.
|
||||
- **Chroma `min`/`max` amb mostreig bilinear subpíxel**: substitueix l'antic `chroma_strength`. Amb min==max queda estàtic; amb min!=max pulsa sinusoidalment entre els dos valors. Adéu al *tic-tac* del NEAREST amb offsets fraccionals.
|
||||
- **MSL extret a headers separats** sota `source/core/rendering/sdl3gpu/msl/` (postfx_vert, postfx_frag, crtpi_frag).
|
||||
- **Service Menu: VSync ara funciona de veritat**. `Screen::applySettings()` propaga al backend GPU (abans només tocava `SDL_SetRenderVSync` i amb backend GPU el present real el fa el swapchain — el canvi era invisible). Separat el setter aplicat al hardware del setter de preferència persistent: `Resource::beginLoad` ja no clobera `Options::video.vsync` durant el preload, així la preferència de l'usuari es preserva entre llançaments.
|
||||
- **Demo player anti-crash**: guarda amb modul al lector (`demo_.data.at(index).at(demo_.index % size)`) i trigger del fade `>=` (era `==`, fragil a frame skips per canvis de preset des del service menu) — evita `vector::_M_range_check` quan el frame de trigger 1800 saltava i `demo_.index` arribava a 2000.
|
||||
- **Reinici real des del service menu** via `execv`.
|
||||
- **Opció de preset de paràmetres al service menu** (`params_file` → `params_preset`: classic / arcade / red). Presets `red` i `classic` arreglats.
|
||||
- **Pack inclou ara la carpeta `config/`** (assets, params, stages, pools, formations).
|
||||
- **Bullet "fire up" reposicionada** (-1 px x, -2 px y) i pintada sobre el jugador en lloc de sota.
|
||||
- **Defaults**: zoom de finestra a 3 (era 2).
|
||||
- Neteja de codi: passades exhaustives de clang-tidy i cppcheck (de 105 warnings a 0).
|
||||
|
||||
---
|
||||
|
||||
## 2026-04-03
|
||||
|
||||
- **Nova intro cinematogràfica**: les tarjetes s'llancen des dels costats de la pantalla amb zoom, rotació i rebot, simulant tirar cartes sobre una mesa. Les anteriors ixen despedides girant quan arriba la següent. Sombra amb efecte de perspectiva 2D→3D.
|
||||
|
||||
+2
-6
@@ -164,15 +164,11 @@ if(NOT APPLE AND NOT EMSCRIPTEN)
|
||||
set(ALL_SHADER_HEADERS
|
||||
"${HEADERS_DIR}/postfx_vert_spv.h"
|
||||
"${HEADERS_DIR}/postfx_frag_spv.h"
|
||||
"${HEADERS_DIR}/upscale_frag_spv.h"
|
||||
"${HEADERS_DIR}/downscale_frag_spv.h"
|
||||
"${HEADERS_DIR}/crtpi_frag_spv.h"
|
||||
)
|
||||
set(ALL_SHADER_SOURCES
|
||||
"${SHADERS_DIR}/postfx.vert"
|
||||
"${SHADERS_DIR}/postfx.frag"
|
||||
"${SHADERS_DIR}/upscale.frag"
|
||||
"${SHADERS_DIR}/downscale.frag"
|
||||
"${SHADERS_DIR}/crtpi_frag.glsl"
|
||||
)
|
||||
|
||||
@@ -236,7 +232,7 @@ target_link_libraries(${PROJECT_NAME} PRIVATE SDL3::SDL3)
|
||||
|
||||
# --- 4. CONFIGURACIÓN PLATAFORMAS Y COMPILADOR ---
|
||||
# Configuración de flags de compilación
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE -Wall)
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic)
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE $<$<CONFIG:RELEASE>:-Os -ffunction-sections -fdata-sections>)
|
||||
|
||||
# Definir _DEBUG en modo Debug
|
||||
@@ -397,7 +393,7 @@ if(NOT EMSCRIPTEN)
|
||||
source/core/resources/resource_pack.cpp
|
||||
)
|
||||
target_include_directories(pack_resources PRIVATE "${CMAKE_SOURCE_DIR}/source")
|
||||
target_compile_options(pack_resources PRIVATE -Wall)
|
||||
target_compile_options(pack_resources PRIVATE -Wall -Wextra -Wpedantic)
|
||||
|
||||
# Regeneració automàtica de resources.pack en cada build si canvia data/.
|
||||
file(GLOB_RECURSE DATA_FILES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/data/*")
|
||||
|
||||
@@ -20,7 +20,7 @@ RESOURCE_FILE := release/windows/coffee.res
|
||||
# ==============================================================================
|
||||
SHADER_CMAKE := $(DIR_ROOT)tools/shaders/compile_spirv.cmake
|
||||
SHADERS_DIR := $(DIR_ROOT)data/shaders
|
||||
HEADERS_DIR := $(DIR_ROOT)source/core/rendering/sdl3gpu
|
||||
HEADERS_DIR := $(DIR_ROOT)source/core/rendering/sdl3gpu/spv
|
||||
ifeq ($(OS),Windows_NT)
|
||||
GLSLC := $(shell where glslc 2>NUL)
|
||||
else
|
||||
@@ -254,7 +254,6 @@ _macos-release:
|
||||
$(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
||||
|
||||
# Copia carpetas y ficheros
|
||||
cp -R config "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
||||
cp build/resources.pack "$(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 release/icons/*.icns "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
||||
@@ -358,7 +357,6 @@ _linux-release:
|
||||
$(MKDIR) "$(RELEASE_FOLDER)"
|
||||
|
||||
# Copia ficheros
|
||||
cp -R config "$(RELEASE_FOLDER)"
|
||||
cp build/resources.pack "$(RELEASE_FOLDER)"
|
||||
cp LICENSE "$(RELEASE_FOLDER)"
|
||||
cp README.md "$(RELEASE_FOLDER)"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,14 +10,15 @@ DATA|${SYSTEM_FOLDER}/postfx.yaml|optional,absolute
|
||||
DATA|${SYSTEM_FOLDER}/crtpi.yaml|optional,absolute
|
||||
DATA|${SYSTEM_FOLDER}/score.bin|optional,absolute
|
||||
|
||||
# Archivos de configuración del juego
|
||||
DATA|${PREFIX}/config/formations.txt
|
||||
DATA|${PREFIX}/config/gamecontrollerdb.txt
|
||||
DATA|${PREFIX}/config/param_320x240.txt
|
||||
DATA|${PREFIX}/config/param_320x256.txt
|
||||
DATA|${PREFIX}/config/param_red.txt
|
||||
DATA|${PREFIX}/config/pools.txt
|
||||
DATA|${PREFIX}/config/stages.txt
|
||||
# Dades de gameplay (viatgen dins el pack)
|
||||
DATA|/data/config/gameplay/formations.txt
|
||||
DATA|/data/config/gameplay/pools.txt
|
||||
DATA|/data/config/gameplay/stages.txt
|
||||
|
||||
# Presets de paràmetres (viatgen dins el pack)
|
||||
DATA|/data/config/presets/classic.txt
|
||||
DATA|/data/config/presets/arcade.txt
|
||||
DATA|/data/config/presets/red.txt
|
||||
|
||||
# Archivos con los datos de la demo
|
||||
DEMODATA|/data/demo/demo1.bin
|
||||
@@ -22,7 +22,7 @@ fade.venetian_size 12 # Tamaño de las bandas para el efecto v
|
||||
|
||||
# --- SCOREBOARD ---
|
||||
scoreboard.rect.x 0 # Posición X del marcador
|
||||
scoreboard.rect.y 216 # Posición Y del marcador
|
||||
scoreboard.rect.y 200 # Posición Y del marcador
|
||||
scoreboard.rect.w 320 # Ancho del marcador
|
||||
scoreboard.rect.h 40 # Alto del marcador
|
||||
scoreboard.separator_autocolor true # ¿El separador usa color automático?
|
||||
@@ -99,8 +99,7 @@ intro.text_distance_from_bottom 48 # Posicion del texto
|
||||
debug.color 00FFFF # Color para elementos de depuración
|
||||
|
||||
# --- RESOURCE ---
|
||||
resource.color FFFFFF # Color de recurso 1
|
||||
resource.color FFFFFF # Color de recurso 2
|
||||
resource.color FFFFFF # Color de recursos
|
||||
|
||||
# --- TABE ---
|
||||
tabe.min_spawn_time 2.0f # Tiempo mínimo en minutos para que aparezca el Tabe
|
||||
@@ -2,7 +2,6 @@
|
||||
# Formato: PARAMETRO VALOR
|
||||
|
||||
# --- GAME ---
|
||||
game.item_size 20 # Tamaño de los items del juego (en píxeles)
|
||||
game.item_text_outline_color FFB8B8F0 # Color del outline del texto de los items (RGBA hex) - Rojo claro
|
||||
game.width 320 # Ancho de la resolución nativa del juego (en píxeles)
|
||||
game.height 256 # Alto de la resolución nativa del juego (en píxeles)
|
||||
@@ -12,8 +11,6 @@ game.play_area.rect.w 320 # Ancho de la zona jugable
|
||||
game.play_area.rect.h 216 # Alto de la zona jugable
|
||||
game.name_entry_idle_time 10 # Segundos para introducir el nombre al finalizar la partida si no se pulsa nada
|
||||
game.name_entry_total_time 60 # Segundos totales para introducir el nombre al finalizar la partida
|
||||
game.hit_stop false # Indica si debe haber un paro cuando el jugador es golpeado por un globo
|
||||
game.hit_stop_ms 500 # Cantidad de milisegundos que dura el hit_stop
|
||||
|
||||
# --- FADE ---
|
||||
fade.color 5C1F1F # Color hexadecimal para el efecto de fundido - Rojo oscuro
|
||||
@@ -39,24 +36,24 @@ scoreboard.text_color2 FFE6E6 # Color secundario del texto del marca
|
||||
scoreboard.skip_countdown_value 8 # Valor para saltar la cuenta atrás (segundos)
|
||||
|
||||
# --- TITLE ---
|
||||
title.press_start_position 180 # Posición Y del texto "Press Start"
|
||||
title.title_duration 800 # Duración de la pantalla de título (frames)
|
||||
title.arcade_edition_position 123 # Posición Y del subtítulo "Arcade Edition"
|
||||
title.title_c_c_position 80 # Posición Y del título principal
|
||||
title.bg_color 8B4A3A # Color de fondo en la sección titulo - Marrón rojizo
|
||||
title.press_start_position 180 # Posición Y del texto "Press Start"
|
||||
title.title_duration 14 # Duración de la pantalla de título (segundos)
|
||||
title.arcade_edition_position 123 # Posición Y del subtítulo "Arcade Edition"
|
||||
title.title_c_c_position 80 # Posición Y del título principal
|
||||
title.bg_color 8B4A3A # Color de fondo en la sección titulo - Marrón rojizo
|
||||
|
||||
# --- BACKGROUND ---
|
||||
background.attenuate_color FF4A3A40 # Color de atenuación del fondo (RGBA hexadecimal) - Blanco rosado
|
||||
|
||||
# --- BALLOONS ---
|
||||
balloon.settings[0].vel 2.75f # Velocidad inicial del globo 1
|
||||
balloon.settings[0].grav 0.09f # Gravedad aplicada al globo 1
|
||||
balloon.settings[1].vel 3.70f # Velocidad inicial del globo 2
|
||||
balloon.settings[1].grav 0.10f # Gravedad aplicada al globo 2
|
||||
balloon.settings[2].vel 4.70f # Velocidad inicial del globo 3
|
||||
balloon.settings[2].grav 0.10f # Gravedad aplicada al globo 3
|
||||
balloon.settings[3].vel 5.45f # Velocidad inicial del globo 4
|
||||
balloon.settings[3].grav 0.10f # Gravedad aplicada al globo 4
|
||||
# --- BALLOONS --- (deltaTime en segundos: vel en pixels/s, grav en pixels/s²)
|
||||
balloon.settings[0].vel 165.0f # Velocidad inicial del globo 1 (pixels/s)
|
||||
balloon.settings[0].grav 320.0f # Gravedad aplicada al globo 1 (pixels/s²)
|
||||
balloon.settings[1].vel 222.0f # Velocidad inicial del globo 2 (pixels/s)
|
||||
balloon.settings[1].grav 360.0f # Gravedad aplicada al globo 2 (pixels/s²)
|
||||
balloon.settings[2].vel 282.0f # Velocidad inicial del globo 3 (pixels/s)
|
||||
balloon.settings[2].grav 360.0f # Gravedad aplicada al globo 3 (pixels/s²)
|
||||
balloon.settings[3].vel 327.0f # Velocidad inicial del globo 4 (pixels/s)
|
||||
balloon.settings[3].grav 360.0f # Gravedad aplicada al globo 4 (pixels/s²)
|
||||
|
||||
balloon.color[0] orange # Color de creación del globo normal
|
||||
balloon.color[1] red # Color del globo normal
|
||||
@@ -83,7 +83,6 @@
|
||||
"[SERVICE_MENU] SHADER": "Shader",
|
||||
"[SERVICE_MENU] SHADER_DISABLED": "Desactivat",
|
||||
"[SERVICE_MENU] SHADER_PRESET": "Preset",
|
||||
"[SERVICE_MENU] SUPERSAMPLING": "Supermostreig",
|
||||
"[SERVICE_MENU] VSYNC": "Sincronisme vertical",
|
||||
"[SERVICE_MENU] INTEGER_SCALE": "Escalat sencer",
|
||||
"[SERVICE_MENU] FILTER": "Filtre",
|
||||
@@ -105,6 +104,10 @@
|
||||
"[SERVICE_MENU] EASY": "Facil",
|
||||
"[SERVICE_MENU] NORMAL": "Normal",
|
||||
"[SERVICE_MENU] HARD": "Dificil",
|
||||
"[SERVICE_MENU] GAME_PRESET": "Perfil",
|
||||
"[SERVICE_MENU] PRESET_CLASSIC": "Classic",
|
||||
"[SERVICE_MENU] PRESET_ARCADE": "Arcade",
|
||||
"[SERVICE_MENU] PRESET_RED": "Roig",
|
||||
"[SERVICE_MENU] NEED_RESTART_MESSAGE": "Reiniciar per aplicar canvis",
|
||||
"[SERVICE_MENU] ENABLE_SHUTDOWN": "Permetre apagar el sistema",
|
||||
"[SERVICE_MENU] CONTROLS": "Controls",
|
||||
|
||||
@@ -82,7 +82,6 @@
|
||||
"[SERVICE_MENU] SHADER": "Shader",
|
||||
"[SERVICE_MENU] SHADER_DISABLED": "Disabled",
|
||||
"[SERVICE_MENU] SHADER_PRESET": "Preset",
|
||||
"[SERVICE_MENU] SUPERSAMPLING": "Supersampling",
|
||||
"[SERVICE_MENU] VSYNC": "V-Sync",
|
||||
"[SERVICE_MENU] INTEGER_SCALE": "Integer Scale",
|
||||
"[SERVICE_MENU] FILTER": "Filter",
|
||||
@@ -104,6 +103,10 @@
|
||||
"[SERVICE_MENU] EASY": "Easy",
|
||||
"[SERVICE_MENU] NORMAL": "Normal",
|
||||
"[SERVICE_MENU] HARD": "Hard",
|
||||
"[SERVICE_MENU] GAME_PRESET": "Preset",
|
||||
"[SERVICE_MENU] PRESET_CLASSIC": "Classic",
|
||||
"[SERVICE_MENU] PRESET_ARCADE": "Arcade",
|
||||
"[SERVICE_MENU] PRESET_RED": "Red",
|
||||
"[SERVICE_MENU] NEED_RESTART_MESSAGE": "Restart to apply changes",
|
||||
"[SERVICE_MENU] ENABLE_SHUTDOWN": "Allow system shutdown",
|
||||
"[SERVICE_MENU] CONTROLS": "Controls",
|
||||
|
||||
@@ -82,7 +82,6 @@
|
||||
"[SERVICE_MENU] SHADER": "Shader",
|
||||
"[SERVICE_MENU] SHADER_DISABLED": "Desactivado",
|
||||
"[SERVICE_MENU] SHADER_PRESET": "Preset",
|
||||
"[SERVICE_MENU] SUPERSAMPLING": "Supersampling",
|
||||
"[SERVICE_MENU] VSYNC": "Sincronismo vertical",
|
||||
"[SERVICE_MENU] INTEGER_SCALE": "Escalado proporcional",
|
||||
"[SERVICE_MENU] FILTER": "Filtro",
|
||||
@@ -104,6 +103,10 @@
|
||||
"[SERVICE_MENU] EASY": "Facil",
|
||||
"[SERVICE_MENU] NORMAL": "Normal",
|
||||
"[SERVICE_MENU] HARD": "Dificil",
|
||||
"[SERVICE_MENU] GAME_PRESET": "Perfil",
|
||||
"[SERVICE_MENU] PRESET_CLASSIC": "Clasico",
|
||||
"[SERVICE_MENU] PRESET_ARCADE": "Arcade",
|
||||
"[SERVICE_MENU] PRESET_RED": "Rojo",
|
||||
"[SERVICE_MENU] NEED_RESTART_MESSAGE": "Reiniciar para aplicar cambios",
|
||||
"[SERVICE_MENU] ENABLE_SHUTDOWN": "Permitir apagar el sistema",
|
||||
"[SERVICE_MENU] CONTROLS": "Controles",
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
#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);
|
||||
}
|
||||
+47
-18
@@ -6,7 +6,9 @@
|
||||
// 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).
|
||||
// (16 floats = 4 × vec4 = 64 bytes, std140/scalar layout).
|
||||
// IMPORTANT: Qualsevol canvi ací cal replicar-lo a mà a
|
||||
// source/core/rendering/sdl3gpu/msl/postfx_frag.msl.h (no hi ha generador).
|
||||
|
||||
layout(location = 0) in vec2 v_uv;
|
||||
layout(location = 0) out vec4 out_color;
|
||||
@@ -15,7 +17,7 @@ layout(set = 2, binding = 0) uniform sampler2D scene;
|
||||
|
||||
layout(set = 3, binding = 0) uniform PostFXUniforms {
|
||||
float vignette_strength;
|
||||
float chroma_strength;
|
||||
float chroma_min; // intensitat mínima de l'aberració cromàtica
|
||||
float scanline_strength;
|
||||
float screen_height;
|
||||
float mask_strength;
|
||||
@@ -24,10 +26,28 @@ layout(set = 3, binding = 0) uniform PostFXUniforms {
|
||||
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)
|
||||
float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz
|
||||
float chroma_max; // intensitat màxima; si == chroma_min → chroma estàtic
|
||||
// vec4 #3 — paràmetres de scanlines (exposats per preset YAML)
|
||||
float scan_dark_ratio; // fracció de subfila fosca per fila lògica (1/3 ≈ 0.333)
|
||||
float scan_dark_floor; // multiplicador de brillantor de la subfila fosca
|
||||
float scan_edge_soft; // 0 = step dur; 1 = suavitzat d'1 píxel físic (estil crtpi)
|
||||
float pad3; // padding per tancar a 64 bytes (4 × vec4)
|
||||
} u;
|
||||
|
||||
// Mostreig bilinear horitzontal d'un canal RGB. Evita el "tic-tac" del sampler
|
||||
// NEAREST quan l'offset de chroma és subpíxel: sense interpolar, l'offset
|
||||
// arrodonia entre 1 i 2 píxels i el drift temporal feia un parpelleig discret.
|
||||
float sampleBilinearX(vec2 uv_target, int channel) {
|
||||
vec2 tex_size = vec2(textureSize(scene, 0));
|
||||
float px = uv_target.x * tex_size.x - 0.5;
|
||||
float p_floor = floor(px);
|
||||
float f = px - p_floor;
|
||||
vec4 c0 = texture(scene, vec2((p_floor + 0.5) / tex_size.x, uv_target.y));
|
||||
vec4 c1 = texture(scene, vec2((p_floor + 1.5) / tex_size.x, uv_target.y));
|
||||
return mix(c0[channel], c1[channel], f);
|
||||
}
|
||||
|
||||
// YCbCr helpers for NTSC bleeding
|
||||
vec3 rgb_to_ycc(vec3 rgb) {
|
||||
return vec3(
|
||||
@@ -69,11 +89,11 @@ void main() {
|
||||
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).
|
||||
// step = 1 pixel lógico de juego en UV.
|
||||
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
|
||||
float step = 1.0 / 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);
|
||||
@@ -85,10 +105,14 @@ void main() {
|
||||
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;
|
||||
// Aberración cromática — intensitat varia entre chroma_min i chroma_max amb
|
||||
// una sinusoidal (si min == max, queda estàtica). Mostreig bilinear horitzontal
|
||||
// per evitar el "tic-tac" del NEAREST sampler quan l'offset és subpíxel.
|
||||
if (u.chroma_min > 0.0 || u.chroma_max > 0.0) {
|
||||
float ca = mix(u.chroma_min, u.chroma_max, 0.5 + 0.5 * sin(u.time * 7.3)) * 0.005;
|
||||
colour.r = sampleBilinearX(uv + vec2(ca, 0.0), 0);
|
||||
colour.b = sampleBilinearX(uv - vec2(ca, 0.0), 2);
|
||||
}
|
||||
|
||||
// Corrección gamma (linealizar antes de scanlines, codificar después)
|
||||
if (u.gamma_strength > 0.0) {
|
||||
@@ -96,15 +120,20 @@ void main() {
|
||||
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.
|
||||
// Scanlines — tècnica dels 3 subpíxels verticals per píxel lògic (aee/projecte_2026):
|
||||
// franja fosca ocupant `scan_dark_ratio` al final de cada fila lògica. La transició es
|
||||
// suavitza amb smoothstep d'ample ≈ 1 píxel físic (estil crtpi: filtratge analític
|
||||
// continu), controlat per `scan_edge_soft`. A 0 és equivalent al step dur antic.
|
||||
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);
|
||||
float ps = max(u.pixel_scale, 1.0);
|
||||
float sub = fract(uv.y * u.screen_height); // [0,1) dins la fila lògica
|
||||
float dark_center = 1.0 - u.scan_dark_ratio * 0.5; // centre de la franja fosca
|
||||
float d = abs(sub - dark_center);
|
||||
d = min(d, 1.0 - d); // wrap a la fila següent
|
||||
float half_width = u.scan_dark_ratio * 0.5;
|
||||
float softness = u.scan_edge_soft * 0.5 / ps; // mig píxel físic a cada costat
|
||||
float band = 1.0 - smoothstep(half_width - softness, half_width + softness, d);
|
||||
float scan = mix(1.0, u.scan_dark_floor, band);
|
||||
colour *= mix(1.0, scan, u.scanline_strength);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
#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);
|
||||
}
|
||||
@@ -0,0 +1,642 @@
|
||||
# Arquitectura de **Coffee Crisis Arcade Edition**
|
||||
|
||||
> Guía de orientación para un desarrollador nuevo en el proyecto.
|
||||
>
|
||||
> Cada afirmación está anclada a código real: se cita el fichero (y, cuando
|
||||
> ayuda, la función o el número de línea) que la respalda. Donde no he
|
||||
> encontrado algo, lo digo explícitamente en lugar de inventarlo.
|
||||
>
|
||||
> **Coffee Crisis Arcade Edition** es un *shooter* arcade cooperativo de 2
|
||||
> jugadores escrito en **C++20 sobre SDL3**: los jugadores defienden el café
|
||||
> de globos gigantes. Apunta a Windows, Linux, macOS (Intel/Apple Silicon),
|
||||
> Raspberry Pi, Anbernic y web (Emscripten). Los comentarios del código están
|
||||
> en español/valenciano; este documento está en castellano.
|
||||
|
||||
---
|
||||
|
||||
## Índice
|
||||
|
||||
1. [Visión general](#1-visión-general)
|
||||
2. [Punto de entrada y bucle principal](#2-punto-de-entrada-y-bucle-principal)
|
||||
3. [Secciones y flujo de la aplicación](#3-secciones-y-flujo-de-la-aplicación)
|
||||
4. [Renderizado: de la lógica al píxel](#4-renderizado-de-la-lógica-al-píxel)
|
||||
5. [Entrada](#5-entrada)
|
||||
6. [Lógica del juego: la clase `Game`](#6-lógica-del-juego-la-clase-game)
|
||||
7. [Entidades y managers de gameplay](#7-entidades-y-managers-de-gameplay)
|
||||
8. [Modo demo y attract mode](#8-modo-demo-y-attract-mode)
|
||||
9. [Recursos](#9-recursos)
|
||||
10. [Audio](#10-audio)
|
||||
11. [Configuración, parámetros y constantes](#11-configuración-parámetros-y-constantes)
|
||||
12. [Localización](#12-localización)
|
||||
13. [Convenciones y patrones recurrentes](#13-convenciones-y-patrones-recurrentes)
|
||||
14. [Guía de navegación: "si quieres tocar X, mira Y"](#14-guía-de-navegación-si-quieres-tocar-x-mira-y)
|
||||
|
||||
---
|
||||
|
||||
## 1. Visión general
|
||||
|
||||
El árbol `source/` separa **motor** y **juego**:
|
||||
|
||||
- **`source/core/`** — motor genérico: `system` (arranque, secciones, demo,
|
||||
eventos globales), `rendering` (+ `sdl3gpu`, `sprite`), `input`, `resources`,
|
||||
`audio`, `locale`.
|
||||
- **`source/game/`** — el juego concreto: `scenes` (las secciones), `gameplay`
|
||||
(managers), `entities` (jugador, globos, balas, ítems…), `ui`, y `options`.
|
||||
- **`source/utils/`** — `color`, `param`, `utils`, `defines`.
|
||||
- **`source/external/`** — vendorizado: `nlohmann/json`, `fkyaml`, `stb_image`,
|
||||
`stb_vorbis`.
|
||||
|
||||
Los `#include` son **absolutos respecto a `source/`** (p.ej.
|
||||
`#include "core/audio/audio.hpp"`); CMake añade un único `-I.../source`
|
||||
(ver `CLAUDE.md`). ~150 ficheros C++, ~32.000 líneas.
|
||||
|
||||
**Cuatro ideas-fuerza que conviene interiorizar:**
|
||||
|
||||
1. **El flujo de la aplicación se conduce con una variable global**
|
||||
(`Section::name`), no con objetos de transición. El `Director` reacciona a
|
||||
sus cambios (§3).
|
||||
2. **El render usa texturas GPU vía `SDL_Renderer`** dibujadas sobre una
|
||||
*render-target texture*, con post-procesado opcional vía un backend SDL3
|
||||
GPU. No es un blitter de software (§4).
|
||||
3. **El gameplay se organiza con managers/pools** (`BalloonManager`,
|
||||
`BulletManager`, `StageManager`) coordinados por una clase `Game` muy grande
|
||||
(§6, §7).
|
||||
4. **Sí hay modo demo** (*attract mode*): es **reproducción de input grabado**,
|
||||
no IA (§8).
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
SDL[SDL3 callbacks · main.cpp] --> DIR[Director]
|
||||
DIR -->|lee Section::name| ST{handleSectionTransition}
|
||||
ST --> SEC["Section activa (Logo/Intro/Title/Game/…)"]
|
||||
DIR --> GE[GlobalEvents] --> INP[Input] & SVC[ServiceMenu]
|
||||
SEC --> GAME[Game]
|
||||
GAME --> BM[BalloonManager] & BLM[BulletManager] & STG[StageManager]
|
||||
GAME --> PL[Players] -->|Input::Action| INP
|
||||
GAME --> DEMO["Demo (playback de input grabado)"] -.->|setInput| PL
|
||||
GAME -->|SDL_RenderTexture| CV["game_canvas_ (render target)"]
|
||||
CV -->|RenderReadPixels → uploadPixels| SB[ShaderBackend SDL3 GPU] --> WIN[Ventana]
|
||||
RES["Resource / Asset"] -.-> GAME & SEC
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Punto de entrada y bucle principal
|
||||
|
||||
### 2.1. SDL conduce el bucle (callbacks)
|
||||
|
||||
`source/main.cpp` define `SDL_MAIN_USE_CALLBACKS`: no hay `while` propio. SDL
|
||||
llama a cuatro funciones, todas delegando en el `Director`:
|
||||
|
||||
```cpp
|
||||
SDL_AppInit → *appstate = new Director(argc, argv); // main.cpp:15
|
||||
SDL_AppIterate→ Director::iterate(); // un frame
|
||||
SDL_AppEvent → Director::handleEvent(event); // un evento
|
||||
SDL_AppQuit → delete Director;
|
||||
```
|
||||
|
||||
### 2.2. El `Director`
|
||||
|
||||
`source/core/system/director.{hpp,cpp}` es el orquestador. Mantiene **un
|
||||
`unique_ptr` por cada sección** (`preload_`, `logo_`, `intro_`, `title_`,
|
||||
`game_`, …) de los que **solo uno está vivo** a la vez (`director.hpp:60`).
|
||||
|
||||
**Constructor (`director.cpp:82`)**: fija la semilla aleatoria, crea la carpeta
|
||||
de sistema (`jailgames/coffee_crisis_arcade_edition`), decide la sección
|
||||
inicial según el build (`RECORDING` → GAME; `_DEBUG` → lee `debug.yaml`; Release
|
||||
→ LOGO) y llama a `init()`.
|
||||
|
||||
**`init()` (`director.cpp:131`)** inicializa en orden: `Asset` (índice de
|
||||
ficheros), sistema de recursos (`resources.pack`, con/ sin *fallback* a disco
|
||||
según build), `Input`, `Options` (config.yaml, controllers.json, presets de
|
||||
shaders), parámetros y *scores*, `Lang`, `Screen`, `Audio` y `Resource`.
|
||||
|
||||
### 2.3. Arranque NO bloqueante (PRELOAD incremental)
|
||||
|
||||
Un detalle importante: si el modo de carga es `PRELOAD`, **no se cargan todos
|
||||
los recursos de golpe**. `init()` redirige el arranque a la sección `PRELOAD`
|
||||
guardando el destino real en `Section::post_preload` (`director.cpp:191`):
|
||||
|
||||
```cpp
|
||||
Section::post_preload = Section::name; // destino real
|
||||
Section::name = Section::Name::PRELOAD; // mientras tanto, barra de progreso
|
||||
Resource::get()->beginLoad();
|
||||
```
|
||||
|
||||
Cada frame, `Director::iterate()` (`director.cpp:489`) llama a
|
||||
`Resource::loadStep(50 /*ms*/)`: carga recursos hasta agotar un presupuesto de
|
||||
50 ms por frame, manteniendo la ventana y el bucle vivos. Cuando termina,
|
||||
`finishBoot()` inicializa lo que depende de los recursos (`ServiceMenu`,
|
||||
`Notifier`, singletons de `Screen`) y salta a `post_preload`.
|
||||
|
||||
### 2.4. Orden del frame y gestión del tiempo
|
||||
|
||||
`Director::iterate()` (`director.cpp:489`):
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant SDL
|
||||
participant Dir as Director::iterate
|
||||
participant Sec as Section activa
|
||||
SDL->>Dir: SDL_AppIterate
|
||||
alt boot_loading_
|
||||
Dir->>Dir: Resource::loadStep(50ms) → finishBoot() al terminar
|
||||
end
|
||||
Dir->>Dir: handleSectionTransition() (destruye/crea sección)
|
||||
Dir->>Sec: iterate() (la sección hace su propio update+render)
|
||||
```
|
||||
|
||||
El tiempo es **time-based**: cada sección calcula su propio `delta_time`. En
|
||||
`Game` es `calculateDeltaTime()` (`game.hpp:210`), usado en todos los `update`.
|
||||
Los eventos llegan por separado vía `Director::handleEvent` →
|
||||
`GlobalEvents::handle` + reenvío a la sección activa (`director.cpp:539`).
|
||||
|
||||
### 2.5. Reinicio en caliente
|
||||
|
||||
Igual que su proyecto hermano, soporta reinicio real vía `execv`
|
||||
(`Director::relaunch()`, `director.cpp:254`): la sección `RESET` (p.ej. tras
|
||||
F10 o cambio de idioma) reemplaza el proceso por sí mismo; si no se puede
|
||||
(Emscripten, `argv` inválido), cae a un `reset()` interno que recarga recursos y
|
||||
vuelve a `LOGO`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Secciones y flujo de la aplicación
|
||||
|
||||
### 3.1. Variables globales de estado
|
||||
|
||||
`source/core/system/section.hpp` define el estado del flujo como **variables
|
||||
globales `inline`** en el namespace `Section`:
|
||||
|
||||
```cpp
|
||||
inline Name name = Name::RESET; // sección a la que ir
|
||||
inline Name post_preload = Name::LOGO; // destino tras PRELOAD
|
||||
inline Options options = Options::NONE; // 1P/2P/BOTH, timeouts, etc.
|
||||
inline AttractMode attract_mode = AttractMode::TITLE_TO_DEMO;
|
||||
```
|
||||
|
||||
`Name` enumera: `RESET, PRELOAD, LOGO, INTRO, TITLE, GAME, HI_SCORE_TABLE,
|
||||
GAME_DEMO, INSTRUCTIONS, CREDITS, QUIT`. Cualquier parte del código cambia el
|
||||
flujo simplemente asignando `Section::name = ...`.
|
||||
|
||||
### 3.2. La transición de secciones
|
||||
|
||||
`Director::handleSectionTransition()` (`director.cpp:390`) se ejecuta cada
|
||||
frame:
|
||||
|
||||
- Si `Section::name == RESET`: intenta `relaunch()`; si vuelve, hace el reset
|
||||
interno.
|
||||
- Si `Section::name == last_built_section_name_`: no hace nada (ya está viva).
|
||||
- Si cambió: `resetActiveSection()` (libera todos los `unique_ptr`) y un
|
||||
`switch` construye la nueva sección. Para `GAME` traduce `Section::options`
|
||||
a `Player::Id` (1P/2P/BOTH); para `GAME_DEMO` construye `Game(..., DEMO_ON)`
|
||||
con jugador aleatorio (`director.cpp:448`).
|
||||
|
||||
Cada sección (`source/game/scenes/`) es autónoma y expone `iterate()` +
|
||||
`handleEvent()`. `Director::iterate` despacha al puntero vivo con una cadena de
|
||||
`if/else if` (`director.cpp:517`).
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
RESET --> PRELOAD --> LOGO --> INTRO --> TITLE
|
||||
TITLE -->|jugar| GAME --> HI_SCORE_TABLE --> TITLE
|
||||
TITLE -->|timeout / attract| GAME_DEMO --> TITLE
|
||||
TITLE -->|attract alterno| LOGO
|
||||
TITLE --> INSTRUCTIONS & CREDITS
|
||||
```
|
||||
|
||||
### 3.3. Attract mode
|
||||
|
||||
El *attract mode* alterna entre mostrar la demo y volver al logo. En
|
||||
`title.cpp:51` el Title, al construirse, mira `Section::attract_mode` y fija su
|
||||
`next_section_` (a `GAME_DEMO` o `LOGO`), **invirtiendo el modo** para la
|
||||
próxima vez. Cuando el Title agota su *timeout* sin input, salta a esa sección
|
||||
(ver §8).
|
||||
|
||||
---
|
||||
|
||||
## 4. Renderizado: de la lógica al píxel
|
||||
|
||||
A diferencia de un blitter de software, aquí **los sprites son texturas GPU**
|
||||
compuestas por `SDL_Renderer`, y el post-procesado se hace en un backend SDL3
|
||||
GPU.
|
||||
|
||||
### 4.1. `Texture` y la jerarquía de sprites
|
||||
|
||||
- `source/core/rendering/texture.hpp` — `Texture` envuelve un `SDL_Texture*`,
|
||||
con `loadFromFile`, `createBlank`, `setAsRenderTarget` y un `render(x, y,
|
||||
clip, zoom, angle, …)`. Es la unidad de dibujo.
|
||||
- `source/core/rendering/sprite/` — jerarquía sobre `Sprite`
|
||||
(`sprite.hpp:13`), que guarda **varias texturas** (`textures_`), un
|
||||
`sprite_clip_` y posición/zoom:
|
||||
- `AnimatedSprite` — animación por fotogramas (clip que avanza).
|
||||
- `MovingSprite` — con velocidad.
|
||||
- `PathSprite` — sigue rutas precalculadas (`Path`).
|
||||
- `SmartSprite` — sprite con lógica propia (p.ej. el café que salta al ser
|
||||
golpeado).
|
||||
- `CardSprite` — variante para "cartas"/paneles.
|
||||
|
||||
El `Player`, por ejemplo, compone un `AnimatedSprite player_sprite_` y un
|
||||
`power_sprite_` para el aura (`player.hpp:250`).
|
||||
|
||||
### 4.2. Dos render-targets
|
||||
|
||||
Hay **dos texturas de destino** encadenadas:
|
||||
|
||||
1. **`Game::canvas_`** — textura de la *zona de juego*. `Game::fillCanvas()`
|
||||
(`game.cpp`) fija el render-target a `canvas_` y dibuja, **en este orden**:
|
||||
`background → smart_sprites → items → balloons → tabe → players → bullets →
|
||||
path_sprites`. Ese orden es el z-order del playfield.
|
||||
|
||||
2. **`Screen::game_canvas_`** — textura de *pantalla completa* (ARGB8888 a la
|
||||
resolución de juego; `screen.cpp:92`, `SDL_TEXTUREACCESS_TARGET`).
|
||||
`Game::render()` hace `screen_->start()` (que pone el target en
|
||||
`game_canvas_`), copia `canvas_` sobre él, y encima dibuja **scoreboard** y
|
||||
los **fades** de entrada/salida; finalmente `screen_->render()`.
|
||||
|
||||
### 4.3. Post-procesado y presentación
|
||||
|
||||
`Screen::renderPresent()` (`screen.cpp:156`) decide cómo llega a la ventana:
|
||||
|
||||
- **Con backend GPU acelerado**: lee los píxeles de `game_canvas_` con
|
||||
`SDL_RenderReadPixels` a un `pixel_buffer_` CPU, los sube al backend
|
||||
(`shader_backend_->uploadPixels(...)`) y este los renderiza con el shader
|
||||
activo (`shader_backend_->render()`).
|
||||
- **Sin backend** (fallback): vuelca `game_canvas_` directamente a la ventana
|
||||
con `SDL_RenderTexture`.
|
||||
|
||||
El backend vive en `source/core/rendering/sdl3gpu/` (SDL3 GPU API; shaders
|
||||
SPIR-V compilados offline desde GLSL en `data/shaders/`, embebidos en cabeceras
|
||||
`spv/`/`msl/`). Hay dos shaders seleccionables, **PostFX** y **CrtPi**, con
|
||||
presets en `postfx.yaml`/`crtpi.yaml` (`screen.cpp:382`). El build `NO_SHADERS`
|
||||
(Anbernic) desactiva todo el pipeline de shaders.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
OBJ["background, balloons, players, bullets…"] -->|SDL_RenderTexture| CANVAS["Game::canvas_ (zona de juego)"]
|
||||
CANVAS -->|copiar| GC["Screen::game_canvas_ (pantalla)"]
|
||||
SB2[scoreboard] --> GC
|
||||
FADE[fades de entrada/salida] --> GC
|
||||
GC -->|RenderReadPixels → uploadPixels| SHADER["ShaderBackend (PostFX/CrtPi)"]
|
||||
SHADER --> WIN[Ventana]
|
||||
GC -.fallback sin GPU.-> WIN
|
||||
```
|
||||
|
||||
### 4.4. Efectos de pantalla
|
||||
|
||||
`Screen` integra efectos globales sobre la presentación: **shake**
|
||||
(`ShakeEffect`, desplaza el rect), **flash** (`FlashEffect`, tinte temporal) y
|
||||
**attenuate** (atenuación) — todos definidos como structs internos en
|
||||
`screen.hpp:108`–`205`. Las transiciones entre estados usan
|
||||
`source/core/rendering/fade.*` (cuadrícula de cuadros).
|
||||
|
||||
---
|
||||
|
||||
## 5. Entrada
|
||||
|
||||
### 5.1. `Input` singleton
|
||||
|
||||
`source/core/input/input.hpp`. Centraliza teclado y mandos bajo un enum de
|
||||
**acciones** (`Input::Action`, alias de `InputAction`). El mapeo por defecto
|
||||
vive en structs:
|
||||
|
||||
- **`Keyboard`** (`input.hpp:54`): flechas = mover; **Q/W/E** = disparar
|
||||
izquierda/centro/derecha; Enter = START; F12 = SERVICE; P = PAUSE; ESC = EXIT;
|
||||
**F1–F11** = ventana/vídeo/audio/idioma/reset/info.
|
||||
- **`Gamepad`** (`input.hpp:99`): cruceta = mover; WEST/NORTH/EAST = disparar;
|
||||
START/ BACK = start/service. Cada mando es un `shared_ptr<Gamepad>` con su
|
||||
propio `bindings`, nombre y *path*.
|
||||
|
||||
La consulta principal es `checkAction(action, repeat, check_keyboard, gamepad)`
|
||||
(`input.hpp:168`). Hay además detección de ejes y triggers como botones, y
|
||||
gestión de configuraciones de mando persistidas (`gamepad_config_manager`,
|
||||
`controllers.json`).
|
||||
|
||||
### 5.2. Eventos globales y hotkeys
|
||||
|
||||
- `source/core/system/global_events.cpp` — `GlobalEvents::handle(event)` trata
|
||||
lo común a cualquier sección: `SDL_EVENT_QUIT`, *resize*, render target reset,
|
||||
hot-plug de mandos (con notificación tras `markStartupComplete`), el toggle
|
||||
del menú de servicio y el ratón.
|
||||
- `source/core/input/global_inputs.cpp` — hotkeys de sistema, despachadas con
|
||||
un mapa acción→lambda (`global_inputs.cpp:187`): fullscreen, zoom ±, audio,
|
||||
autofire, idioma (CHANGE_LANG → `Section::RESET`, reinicia), VSync, integer
|
||||
scale, info; más PostFX/shader/preset. **F10/RESET** pone `Section::name =
|
||||
RESET` (reinicio) y **ESC/EXIT** pone `QUIT`.
|
||||
|
||||
### 5.3. Cómo llega la entrada a un jugador
|
||||
|
||||
Dentro de `Game`, `checkInput()` → `handlePlayersInput()` →
|
||||
`handleNormalPlayerInput(player)` consulta `Input` y traduce a
|
||||
`player->setInput(Input::Action)` y disparos (`handleFireInput`). El `Player`
|
||||
sabe si usa teclado o un mando concreto (`uses_keyboard_`, `gamepad_`;
|
||||
`player.hpp:217`). En modo demo, esta misma vía se alimenta de datos grabados
|
||||
(§8).
|
||||
|
||||
---
|
||||
|
||||
## 6. Lógica del juego: la clase `Game`
|
||||
|
||||
`source/game/scenes/game.{hpp,cpp}` es la sección de gameplay y la clase más
|
||||
grande del proyecto. Coordina jugadores, globos, balas, ítems, fases,
|
||||
puntuación y efectos.
|
||||
|
||||
### 6.1. FSM de la partida
|
||||
|
||||
`Game::State` (`game.hpp:75`): `FADE_IN → ENTERING_PLAYER →
|
||||
SHOWING_GET_READY_MESSAGE → PLAYING → COMPLETED / GAME_OVER`. `checkState()`
|
||||
(`game.cpp`) detecta fin de juego (`stage_manager_->isGameCompleted()`) o game
|
||||
over (`allPlayersAreGameOver()`); `setState()` hace la transición.
|
||||
|
||||
### 6.2. El frame de `Game`
|
||||
|
||||
```cpp
|
||||
void Game::iterate() { // game.cpp
|
||||
const float DELTA_TIME = calculateDeltaTime();
|
||||
checkInput();
|
||||
update(DELTA_TIME);
|
||||
render();
|
||||
}
|
||||
void Game::update(float dt) {
|
||||
screen_->update(dt);
|
||||
Audio::update();
|
||||
updateDemo(dt); // no-op si no es demo
|
||||
updateGameStates(dt); // dispatch por State
|
||||
fillCanvas(); // dibuja la zona de juego en canvas_
|
||||
}
|
||||
```
|
||||
|
||||
`updateGameStates` despacha a un `updateGameState<Estado>` por cada valor de la
|
||||
FSM (`game.hpp:218`).
|
||||
|
||||
### 6.3. Sistemas que orquesta
|
||||
|
||||
`Game` posee (vía `unique_ptr`) los managers y objetos del nivel
|
||||
(`game.hpp:136`):
|
||||
|
||||
- **`StageManager`** — progresión por "poder": cada fase necesita
|
||||
`power_to_complete`; el juego se completa al acumular el poder total. También
|
||||
define umbrales de **amenaza** (`menace`) por fase. Implementa `IStageInfo`
|
||||
(`stage.hpp:51`), interfaz mínima que se inyecta a jugador y globos.
|
||||
- **`BalloonManager`** — despliega formaciones (`deployRandomFormation`),
|
||||
globos hijos al explotar uno, *power balls*, ajusta velocidad
|
||||
(`Balloon::GAME_TEMPO`) y calcula la amenaza en pantalla (§7).
|
||||
- **`BulletManager`** — pool de balas; las colisiones se resuelven con
|
||||
**callbacks** que registra `Game` (`setBalloonCollisionCallback`,
|
||||
`setTabeCollisionCallback`, `setOutOfBoundsCallback`; `bullet_manager.hpp:49`),
|
||||
manteniendo la lógica de juego dentro de `Game`.
|
||||
- **`Background`**, **`Fade` ×2**, **`Tabe`** (enemigo especial volador),
|
||||
**`Scoreboard`**, **`PauseManager`**.
|
||||
- Listas de `Item` (power-ups y puntos), `SmartSprite` y `PathSprite`.
|
||||
|
||||
### 6.4. Mecánicas destacadas
|
||||
|
||||
- **Ítems / power-ups** (`game.hpp:268`): café (toque extra),
|
||||
máquina de café (power-up), *power ball*, reloj (= **detener el tiempo**,
|
||||
`enableTimeStopItem`), e ítems de puntos. La probabilidad de soltar ítem la
|
||||
decide `dropItem()` con *odds* configurables (`Helper`, `game.hpp:100`).
|
||||
- **Sistema de amenaza** (`updateMenace`/`setMenace`): si la amenaza cae por
|
||||
debajo de un umbral, se generan más globos.
|
||||
- **Multijugador**: `players_` es un `vector<shared_ptr<Player>>` (1 o 2). Un
|
||||
`players_draw_list_` separado mantiene el z-order de dibujo
|
||||
(`buildPlayerDrawList`, `sendPlayerToBack`/`bringPlayerToFront`;
|
||||
`game.hpp:338`).
|
||||
- **Récords**: al perder con buena puntuación, el jugador entra en
|
||||
`ENTERING_NAME` (`enter_name.*`, `manage_hiscore_table.*`).
|
||||
|
||||
---
|
||||
|
||||
## 7. Entidades y managers de gameplay
|
||||
|
||||
`source/game/entities/`:
|
||||
|
||||
- **`Player`** (`player.hpp`) — la entidad más compleja. Hereda de
|
||||
`AnimatedSprite`. Tiene **tres ejes de estado** independientes:
|
||||
`walking_state_`, `firing_state_` y `playing_state_` (`player.hpp:266`). El
|
||||
disparo usa un **sistema de dos líneas**: una funcional (cooldown, `canFire`)
|
||||
y otra visual (animaciones `NORMAL→AIMING→RECOILING→THREAT_POSE`,
|
||||
`player.hpp:296`). Soporta power-up, invulnerabilidad con parpadeo, *continue*
|
||||
y entrada de nombre. Puede controlarse por teclado o por un `Gamepad`
|
||||
concreto.
|
||||
- **`Balloon`** (`balloon.hpp`) — el enemigo básico; al explotar puede crear
|
||||
globos hijos. `Balloon::GAME_TEMPO` define velocidades por progreso.
|
||||
- **`Bullet`** (`bullet.hpp`) — proyectil con tipo (UP/LEFT/RIGHT) y color
|
||||
(según power-up del jugador).
|
||||
- **`Item`** — power-ups y objetos de puntos que caen.
|
||||
- **`Explosions`** — efectos de explosión (lo posee `BalloonManager`).
|
||||
- **`Tabe`** — enemigo/objeto especial volador con su propia lógica de impacto.
|
||||
|
||||
`source/game/gameplay/` contiene los **managers y sistemas** (no entidades en
|
||||
sí): `BalloonManager`, `BulletManager`, `StageManager`, `Difficulty`,
|
||||
`Scoreboard`, `balloon_formations` (lee `formations.txt`), `cooldown`,
|
||||
`enter_name`, `manage_hiscore_table`, `game_logo`.
|
||||
|
||||
El patrón general aquí **no es un `EntityManager` polimórfico único** (como en
|
||||
el proyecto hermano), sino **managers especializados por tipo** + listas
|
||||
homogéneas en `Game`, con **callbacks** para cruzar responsabilidades sin
|
||||
acoplar (caso de `BulletManager`).
|
||||
|
||||
---
|
||||
|
||||
## 8. Modo demo y attract mode
|
||||
|
||||
> **A diferencia de muchos juegos, aquí el modo demo SÍ existe — pero NO es
|
||||
> IA.** Es **reproducción de input pregrabado**.
|
||||
|
||||
### 8.1. Formato de los datos
|
||||
|
||||
`source/core/system/demo.hpp`: cada fotograma de demo es un `DemoKeys` con seis
|
||||
banderas (`left`, `right`, `no_input`, `fire`, `fire_left`, `fire_right`). Una
|
||||
demo es un `vector<DemoKeys>` de `TOTAL_DEMO_DATA = 2000` fotogramas. Hay tres
|
||||
ficheros: `data/demo/demo{1,2,3}.bin`, leídos por `loadDemoDataFromFile`
|
||||
(`demo.cpp:11`).
|
||||
|
||||
### 8.2. Reproducción
|
||||
|
||||
Cuando `Game` se construye con `DEMO_ON` (sección `GAME_DEMO`), `initDemo()`
|
||||
(`game.cpp`) carga todas las demos del `Asset` y **asigna una a cada jugador de
|
||||
forma aleatoria** (`shuffle`), elige una fase de inicio al azar, da cafés
|
||||
aleatorios, pone los marcadores en modo `DEMO` y silencia los globos. Luego cada
|
||||
frame, `demoHandlePlayerInput()` lee el fotograma actual y lo inyecta por la
|
||||
**misma vía de input que un humano**:
|
||||
|
||||
```cpp
|
||||
// game.cpp — demoHandlePlayerInput
|
||||
const auto& demo_data = demo_data_vec.at(demo_.index % demo_data_vec.size());
|
||||
if (demo_data.left == 1) player->setInput(Input::Action::LEFT);
|
||||
else if (demo_data.right == 1) player->setInput(Input::Action::RIGHT);
|
||||
...
|
||||
if (demo_data.fire == 1) handleFireInput(player, Bullet::Type::UP);
|
||||
```
|
||||
|
||||
No hay toma de decisiones: el jugador "demo" repite exactamente las pulsaciones
|
||||
grabadas. `demoHandlePassInput()` permite **salir** con cualquier botón,
|
||||
volviendo a `TITLE` y dejando armado el siguiente attract (`TITLE_TO_DEMO`).
|
||||
|
||||
### 8.3. Attract mode
|
||||
|
||||
El Title alterna `TITLE_TO_DEMO` ↔ `TITLE_TO_LOGO` (`title.cpp:51`): tras su
|
||||
*timeout*, una vez lanza la demo y la siguiente vuelve al logo, en bucle de
|
||||
atracción típico de recreativa.
|
||||
|
||||
### 8.4. Grabación y autoplay
|
||||
|
||||
- **`#ifdef RECORDING`** (`demo.cpp:43`, `game.hpp:349`): build especial que
|
||||
arranca directamente en GAME y **graba** las pulsaciones a un `.bin` con
|
||||
`saveDemoFile`. Así se generan las demos.
|
||||
- **`autoplay`** (debug): en `_DEBUG`, `debug_config.autoplay` permite alimentar
|
||||
datos de demo a los jugadores en una partida normal (`game.hpp:354`,
|
||||
`initDemo`).
|
||||
|
||||
---
|
||||
|
||||
## 9. Recursos
|
||||
|
||||
### 9.1. `Resource` (singleton)
|
||||
|
||||
`source/core/resources/resource.hpp`. Cachea por nombre texturas, música,
|
||||
sonidos, ficheros de texto/fuentes, animaciones y **datos de demo**
|
||||
(`getDemoData`). Dos modos (`LoadingMode`):
|
||||
|
||||
- **`PRELOAD`** — carga incremental al arranque con `beginLoad()` +
|
||||
`loadStep(budget_ms)` + `renderProgress()` (la sección Preload pinta la
|
||||
barra). Es el modo por defecto.
|
||||
- **`LAZY_LOAD`** — carga bajo demanda (seleccionable vía `debug.yaml`).
|
||||
|
||||
### 9.2. `Asset` y el pack
|
||||
|
||||
`source/core/resources/asset.*` mantiene el **índice de ficheros** a partir del
|
||||
manifiesto `data/config/assets.txt` (`director.cpp:296`) y valida que no falte
|
||||
ninguno (`Asset::check()`). La lectura física pasa por
|
||||
`ResourceHelper::loadFile`, que sirve desde **`resources.pack`** y hace
|
||||
*fallback* al filesystem en builds de desarrollo (en Release nativo es estricto:
|
||||
solo el pack; `director.cpp:143`). `resource_pack.*` y `resource_loader.*`
|
||||
implementan el formato del pack y la carga.
|
||||
|
||||
Formatos: imágenes vía `stb_image` + `gif` propio; audio `.ogg` vía
|
||||
`stb_vorbis`; texto/JSON vía `nlohmann/json`; configs vía `fkyaml`.
|
||||
|
||||
---
|
||||
|
||||
## 10. Audio
|
||||
|
||||
`source/core/audio/audio.hpp` — `Audio` singleton sobre `jail_audio`
|
||||
(primer-party, no librería externa) y `audio_adapter`. API: `playMusic(name,
|
||||
loop, crossfade_ms)`, `playSound(name, Group)`, control de volumen por **grupos**
|
||||
(`Group::GAME`, `INTERFACE`, …), `pause/resume/stop`, *crossfade* y *fade out*
|
||||
(`audio.hpp:49`). `Audio::update()` se llama cada frame desde las secciones
|
||||
(p.ej. `Game::update`). El build `NO_AUDIO` compila sin audio.
|
||||
|
||||
---
|
||||
|
||||
## 11. Configuración, parámetros y constantes
|
||||
|
||||
El proyecto tiene **tres capas de configuración** que conviene no confundir:
|
||||
|
||||
1. **`Options`** (`source/game/options.hpp`) — opciones persistentes del
|
||||
usuario, agrupadas en structs (`Window`, `Video`, `Audio`, `Settings`,
|
||||
`Gamepad`, `Keyboard`) más presets de shaders (`PostFXPreset`,
|
||||
`CrtPiPreset`). Se guardan en `config.yaml` / `controllers.json` /
|
||||
`postfx.yaml` / `crtpi.yaml`. Incluye `params_preset` ∈ {`classic`,
|
||||
`arcade`, `red`} (`options.hpp:339`).
|
||||
2. **`param` / `Param`** (`source/utils/param.hpp`) — parámetros de *gameplay y
|
||||
layout* (dimensiones del juego, `play_area`, ajustes de globos, scoreboard,
|
||||
título, fades, jugador, Tabe…). Se cargan de ficheros de texto
|
||||
`param_320x240.txt` / `param_320x256.txt` / `classic.txt` con
|
||||
`loadParamsFromFile` (`director.cpp:275`). El objeto global `param` lo leen
|
||||
casi todos los subsistemas.
|
||||
3. **Otros datos de juego**: `stages.txt` (fases, lo lee `StageManager`),
|
||||
`formations.txt` (formaciones de globos), `assets.txt` (manifiesto).
|
||||
|
||||
Los **valores de fallback compilados** viven en
|
||||
`source/core/system/defaults.hpp` (y `source/utils/defines.hpp` para constantes
|
||||
base). En Debug, `debug.yaml` controla sección inicial, opciones, fase, modo de
|
||||
carga, `autoplay` e `invincibility` (`director.cpp:318`).
|
||||
|
||||
### Builds condicionales (multiplataforma)
|
||||
|
||||
El juego se adapta por *defines* (ver `CLAUDE.md`): `WINDOWS_BUILD` /
|
||||
`LINUX_BUILD` / `MACOS_BUILD`, `RELEASE_BUILD`, `MACOS_BUNDLE`, `ANBERNIC`
|
||||
(handheld), `__EMSCRIPTEN__` (web), `NO_SHADERS`, `NO_AUDIO`, `RECORDING`,
|
||||
`ARCADE`, `DEBUG`/`VERBOSE`. Aparecen sobre todo en `Director::init` y `Screen`.
|
||||
|
||||
---
|
||||
|
||||
## 12. Localización
|
||||
|
||||
`source/core/locale/lang.*` — `Lang` carga el idioma desde **JSON** en
|
||||
`data/lang/`. `Lang::setLanguage(Options::settings.language)` se llama en
|
||||
`init()` y en cada `reset()`. Cambiar idioma en caliente (F9 / `CHANGE_LANG`)
|
||||
fuerza un `Section::RESET` (reinicio del proceso) para recargarlo todo
|
||||
(`global_inputs.cpp`).
|
||||
|
||||
---
|
||||
|
||||
## 13. Convenciones y patrones recurrentes
|
||||
|
||||
- **Naming** (de `.clang-format`/`.clang-tidy`, resumido en `CLAUDE.md`):
|
||||
clases `CamelCase`, métodos `camelBack`, variables/params `snake_case`,
|
||||
miembros privados `snake_case_` (guion bajo final), constantes `UPPER_CASE`,
|
||||
namespaces `CamelCase`, valores de enum `UPPER_CASE`. clang-tidy trata los
|
||||
warnings como errores.
|
||||
- **Singletons con init/destroy manual**: `Screen`, `Resource`, `Audio`,
|
||||
`Input`, `Asset`, `ServiceMenu`, `Notifier`, `Lang`. Se crean/destruyen en
|
||||
orden explícito en `Director::init` / `shutdownSubsystems` (`director.cpp:227`)
|
||||
— no se confía en destructores estáticos.
|
||||
- **Estado de flujo por variable global** (`Section::name`): patrón central; no
|
||||
hay objetos de transición tipados.
|
||||
- **Time-based en todo**: cada sistema recibe `delta_time`; las constantes de
|
||||
tiempo se documentan como "N frames a 60fps → segundos" (p.ej.
|
||||
`game.hpp:85`).
|
||||
- **Callbacks para desacoplar**: `BulletManager` y `StageManager` reciben
|
||||
`std::function` desde `Game` para no conocer la lógica de colisión/puntuación.
|
||||
- **Interfaz mínima `IStageInfo`** (`stage_interface.hpp`): se inyecta a
|
||||
jugador y globos para que consulten amenaza/poder sin ver todo el
|
||||
`StageManager`.
|
||||
- **Managers por tipo + listas homogéneas** en `Game`, en vez de un contenedor
|
||||
de entidades polimórfico.
|
||||
- **Comentarios** en español/valenciano; muchos `#include` llevan comentario
|
||||
"// Para X" indicando qué símbolo justifican (estilo IWYU).
|
||||
- **Multiplataforma por `#ifdef`** (ver §11). Conviene leerlos al tocar
|
||||
arranque o render.
|
||||
|
||||
---
|
||||
|
||||
## 14. Guía de navegación: "si quieres tocar X, mira Y"
|
||||
|
||||
| Quiero… | Empieza por… |
|
||||
|---|---|
|
||||
| Entender el arranque | `source/core/system/director.cpp` (`init`, `iterate`) |
|
||||
| Cambiar el flujo de pantallas | `source/core/system/section.hpp` + `handleSectionTransition` |
|
||||
| Añadir/editar una pantalla | `source/game/scenes/` (crea `iterate()`+`handleEvent()`) y un `case` en `director.cpp` |
|
||||
| La barra de carga / arranque lento | `Resource::beginLoad/loadStep` + `scenes/preload.*` |
|
||||
| Cómo se dibuja todo | `Game::fillCanvas` + `Game::render` + `Screen::renderPresent` (`screen.cpp:156`) |
|
||||
| Sprites / animaciones | `source/core/rendering/sprite/` + `texture.hpp` |
|
||||
| Shaders / CRT / post-FX | `source/core/rendering/sdl3gpu/` + `data/shaders/` + `Options` presets |
|
||||
| Efectos (shake/flash/fade) | `screen.hpp` (structs) + `core/rendering/fade.*` |
|
||||
| Controles / mandos | `source/core/input/input.hpp` (`Keyboard`/`Gamepad`/`checkAction`) |
|
||||
| Hotkeys F1–F12 / reset | `source/core/input/global_inputs.cpp` |
|
||||
| Eventos globales / hot-plug | `source/core/system/global_events.cpp` |
|
||||
| Lógica de partida y estados | `source/game/scenes/game.cpp` (`updateGameState*`) |
|
||||
| Globos y formaciones | `gameplay/balloon_manager.*` + `entities/balloon.*` + `formations.txt` |
|
||||
| Balas y colisiones | `gameplay/bullet_manager.*` (callbacks) + `entities/bullet.*` |
|
||||
| Fases / progresión / dificultad | `gameplay/stage.*` (`StageManager`) + `stages.txt` + `gameplay/difficulty.*` |
|
||||
| El jugador (movimiento, disparo) | `entities/player.*` (sistema de disparo de dos líneas) |
|
||||
| Ítems y power-ups | `entities/item.*` + `Game::dropItem/createItem` |
|
||||
| Récords / entrada de nombre | `gameplay/manage_hiscore_table.*`, `gameplay/enter_name.*` |
|
||||
| **Modo demo / attract** | `core/system/demo.*`, `Game::initDemo/demoHandle*`, `scenes/title.cpp` |
|
||||
| Grabar nuevas demos | build con `RECORDING` + `Game::updateRecording` |
|
||||
| Cargar un recurso | `core/resources/resource.hpp` + `asset.*` + `assets.txt` |
|
||||
| Audio (música/SFX) | `core/audio/audio.hpp` |
|
||||
| Opciones del usuario | `game/options.hpp` (+ `config.yaml`) |
|
||||
| Parámetros de gameplay/layout | `utils/param.hpp` + `param_320x*.txt` |
|
||||
| Valores por defecto | `core/system/defaults.hpp`, `utils/defines.hpp` |
|
||||
| Idiomas | `core/locale/lang.*` + `data/lang/` |
|
||||
| Menú de servicio / notificaciones | `game/ui/service_menu.*`, `game/ui/notifier.*` |
|
||||
| Empaquetar datos | `tools/` (`pack_resources`) + `make resources.pack` |
|
||||
|
||||
---
|
||||
|
||||
*Documento generado a partir de la lectura directa del código en el commit
|
||||
actual de la rama `main`. Si algo aquí no cuadra con el código, el código
|
||||
manda: actualiza este documento.*
|
||||
@@ -8,8 +8,13 @@
|
||||
// Implementación de stb_vorbis (debe estar ANTES de incluir jail_audio.hpp).
|
||||
// clang-format off
|
||||
#undef STB_VORBIS_HEADER_ONLY
|
||||
// stb_vorbis.c (codi de tercers) dispara -Wtautological-compare; el silenciem
|
||||
// només per a aquesta inclusió sense afectar el nostre codi.
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wtautological-compare"
|
||||
// NOLINTNEXTLINE(bugprone-suspicious-include) — stb_vorbis és single-file: el TU principal inclou el .c per portar la implementació.
|
||||
#include "external/stb_vorbis.c"
|
||||
#pragma GCC diagnostic pop
|
||||
// stb_vorbis.c filtra les macros L, C i R (i PLAYBACK_*) al TU. Les netegem
|
||||
// perquè xocarien amb noms de paràmetres de plantilla en altres headers.
|
||||
#undef L
|
||||
|
||||
@@ -259,7 +259,7 @@ namespace Ja {
|
||||
sdl_audio_device = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &audio_spec);
|
||||
if (sdl_audio_device == 0) { std::cout << "Failed to initialize SDL audio!" << '\n'; }
|
||||
for (auto& ch : channels) { ch.state = ChannelState::FREE; }
|
||||
std::fill(std::begin(sound_volume), std::end(sound_volume), 0.5F);
|
||||
std::ranges::fill(sound_volume, 0.5F);
|
||||
}
|
||||
|
||||
inline void quit() {
|
||||
@@ -663,7 +663,7 @@ namespace Ja {
|
||||
const float V = SDL_clamp(volume, 0.0F, 1.0F);
|
||||
|
||||
if (group == -1) {
|
||||
std::fill(std::begin(sound_volume), std::end(sound_volume), V);
|
||||
std::ranges::fill(sound_volume, V);
|
||||
} else if (group >= 0 && group < MAX_GROUPS) {
|
||||
sound_volume[group] = V;
|
||||
} else {
|
||||
|
||||
@@ -64,7 +64,7 @@ class GamepadConfigManager {
|
||||
// Escribir al archivo
|
||||
std::ofstream file(filename);
|
||||
if (!file.is_open()) {
|
||||
return false; // NOLINT(readability-simplify-boolean-expr)
|
||||
return false;
|
||||
}
|
||||
|
||||
file << j.dump(4); // Formato con indentación de 4 espacios
|
||||
@@ -92,7 +92,7 @@ class GamepadConfigManager {
|
||||
configs.clear();
|
||||
|
||||
if (!j.contains("gamepads") || !j["gamepads"].is_array()) {
|
||||
return false; // NOLINT(readability-simplify-boolean-expr)
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto& gamepad_json : j["gamepads"]) {
|
||||
|
||||
@@ -6,15 +6,15 @@
|
||||
#include <utility> // Para pair
|
||||
#include <vector> // Para vector
|
||||
|
||||
#include "core/audio/audio.hpp" // Para Audio
|
||||
#include "core/input/input.hpp" // Para Input
|
||||
#include "core/locale/lang.hpp" // Para getText, getLangFile, getLangName, getNextLangCode, loadFromFile
|
||||
#include "core/rendering/screen.hpp" // Para Screen
|
||||
#include "core/system/section.hpp" // Para Name, name, Options, options, AttractMode, attract_mode
|
||||
#include "game/options.hpp" // Para Video, video, Settings, settings, Audio, audio, Window, window
|
||||
#include "game/ui/notifier.hpp" // Para Notifier
|
||||
#include "game/ui/service_menu.hpp" // Para ServiceMenu
|
||||
#include "utils/utils.hpp" // Para boolToOnOff
|
||||
#include "core/audio/audio.hpp" // Para Audio
|
||||
#include "core/input/input.hpp" // Para Input
|
||||
#include "core/locale/lang.hpp" // Para getText, getLangFile, getLangName, getNextLangCode, loadFromFile
|
||||
#include "core/rendering/screen.hpp" // Para Screen
|
||||
#include "core/system/section.hpp" // Para Name, name, Options, options, AttractMode, attract_mode
|
||||
#include "game/options.hpp" // Para Video, video, Settings, settings, Audio, audio, Window, window
|
||||
#include "game/ui/notifier.hpp" // Para Notifier
|
||||
#include "game/ui/service_menu.hpp" // Para ServiceMenu
|
||||
#include "utils/utils.hpp" // Para boolToOnOff
|
||||
|
||||
namespace GlobalInputs {
|
||||
// Termina
|
||||
@@ -91,12 +91,6 @@ namespace GlobalInputs {
|
||||
}
|
||||
}
|
||||
|
||||
// Activa o desactiva el supersampling
|
||||
void toggleSupersampling() {
|
||||
Screen::toggleSupersampling();
|
||||
Notifier::get()->show({"SS: " + std::string(Options::video.supersampling.enabled ? "ON" : "OFF")});
|
||||
}
|
||||
|
||||
// Cambia al siguiente idioma
|
||||
void setNextLang() {
|
||||
const std::string CODE = "LANG";
|
||||
@@ -227,10 +221,6 @@ namespace GlobalInputs {
|
||||
nextPreset();
|
||||
return true;
|
||||
}
|
||||
if (Input::get()->checkAction(Input::Action::TOGGLE_SUPERSAMPLING, Input::DO_NOT_ALLOW_REPEAT, Input::CHECK_KEYBOARD)) {
|
||||
toggleSupersampling();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -83,7 +83,6 @@ class Input {
|
||||
{Action::TOGGLE_VIDEO_POSTFX, KeyState(SDL_SCANCODE_F4)},
|
||||
{Action::NEXT_SHADER, KeyState(SDL_SCANCODE_8)},
|
||||
{Action::NEXT_POSTFX_PRESET, KeyState(SDL_SCANCODE_9)},
|
||||
{Action::TOGGLE_SUPERSAMPLING, KeyState(SDL_SCANCODE_0)},
|
||||
{Action::TOGGLE_VIDEO_INTEGER_SCALE, KeyState(SDL_SCANCODE_F5)},
|
||||
{Action::TOGGLE_VIDEO_VSYNC, KeyState(SDL_SCANCODE_F6)},
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ const std::unordered_map<InputAction, std::string> ACTION_TO_STRING = {
|
||||
{InputAction::TOGGLE_VIDEO_POSTFX, "TOGGLE_VIDEO_POSTFX"},
|
||||
{InputAction::NEXT_SHADER, "NEXT_SHADER"},
|
||||
{InputAction::NEXT_POSTFX_PRESET, "NEXT_POSTFX_PRESET"},
|
||||
{InputAction::TOGGLE_SUPERSAMPLING, "TOGGLE_SUPERSAMPLING"},
|
||||
{InputAction::TOGGLE_VIDEO_INTEGER_SCALE, "TOGGLE_VIDEO_INTEGER_SCALE"},
|
||||
{InputAction::TOGGLE_VIDEO_VSYNC, "TOGGLE_VIDEO_VSYNC"},
|
||||
{InputAction::RESET, "RESET"},
|
||||
@@ -57,7 +56,6 @@ const std::unordered_map<std::string, InputAction> STRING_TO_ACTION = {
|
||||
{"TOGGLE_VIDEO_POSTFX", InputAction::TOGGLE_VIDEO_POSTFX},
|
||||
{"NEXT_SHADER", InputAction::NEXT_SHADER},
|
||||
{"NEXT_POSTFX_PRESET", InputAction::NEXT_POSTFX_PRESET},
|
||||
{"TOGGLE_SUPERSAMPLING", InputAction::TOGGLE_SUPERSAMPLING},
|
||||
{"TOGGLE_VIDEO_INTEGER_SCALE", InputAction::TOGGLE_VIDEO_INTEGER_SCALE},
|
||||
{"TOGGLE_VIDEO_VSYNC", InputAction::TOGGLE_VIDEO_VSYNC},
|
||||
{"RESET", InputAction::RESET},
|
||||
|
||||
@@ -35,7 +35,6 @@ enum class InputAction : std::uint8_t { // Acciones de entrada posibles en el j
|
||||
TOGGLE_VIDEO_POSTFX,
|
||||
NEXT_SHADER,
|
||||
NEXT_POSTFX_PRESET,
|
||||
TOGGLE_SUPERSAMPLING,
|
||||
TOGGLE_VIDEO_INTEGER_SCALE,
|
||||
TOGGLE_VIDEO_VSYNC,
|
||||
RESET,
|
||||
|
||||
@@ -20,11 +20,11 @@ class PauseManager {
|
||||
|
||||
// --- Operadores friend ---
|
||||
friend auto operator|(Source a, Source b) -> Source {
|
||||
return static_cast<Source>(static_cast<uint8_t>(a) | static_cast<uint8_t>(b)); // NOLINT(readability-redundant-casting)
|
||||
return static_cast<Source>(static_cast<uint8_t>(a) | static_cast<uint8_t>(b));
|
||||
}
|
||||
|
||||
friend auto operator&(Source a, Source b) -> Source {
|
||||
return static_cast<Source>(static_cast<uint8_t>(a) & static_cast<uint8_t>(b)); // NOLINT(readability-redundant-casting)
|
||||
return static_cast<Source>(static_cast<uint8_t>(a) & static_cast<uint8_t>(b));
|
||||
}
|
||||
|
||||
friend auto operator~(Source a) -> uint8_t {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// NOLINTNEXTLINE(bugprone-reserved-identifier) -- requerido por <cmath> para exponer M_PI en MSVC
|
||||
#define _USE_MATH_DEFINES
|
||||
#include "core/rendering/background.hpp"
|
||||
|
||||
@@ -82,11 +83,11 @@ void Background::initializeSprites() {
|
||||
const float TOP_CLOUDS_Y = base_ - 165;
|
||||
const float BOTTOM_CLOUDS_Y = base_ - 101;
|
||||
|
||||
top_clouds_sprite_a_ = std::make_unique<MovingSprite>(top_clouds_texture_, (SDL_FRect){.x = 0, .y = TOP_CLOUDS_Y, .w = rect_.w, .h = static_cast<float>(top_clouds_texture_->getHeight())});
|
||||
top_clouds_sprite_b_ = std::make_unique<MovingSprite>(top_clouds_texture_, (SDL_FRect){.x = rect_.w, .y = TOP_CLOUDS_Y, .w = rect_.w, .h = static_cast<float>(top_clouds_texture_->getHeight())});
|
||||
top_clouds_sprite_a_ = std::make_unique<MovingSprite>(top_clouds_texture_, SDL_FRect{.x = 0, .y = TOP_CLOUDS_Y, .w = rect_.w, .h = static_cast<float>(top_clouds_texture_->getHeight())});
|
||||
top_clouds_sprite_b_ = std::make_unique<MovingSprite>(top_clouds_texture_, SDL_FRect{.x = rect_.w, .y = TOP_CLOUDS_Y, .w = rect_.w, .h = static_cast<float>(top_clouds_texture_->getHeight())});
|
||||
|
||||
bottom_clouds_sprite_a_ = std::make_unique<MovingSprite>(bottom_clouds_texture_, (SDL_FRect){.x = 0, .y = BOTTOM_CLOUDS_Y, .w = rect_.w, .h = static_cast<float>(bottom_clouds_texture_->getHeight())});
|
||||
bottom_clouds_sprite_b_ = std::make_unique<MovingSprite>(bottom_clouds_texture_, (SDL_FRect){.x = rect_.w, .y = BOTTOM_CLOUDS_Y, .w = rect_.w, .h = static_cast<float>(bottom_clouds_texture_->getHeight())});
|
||||
bottom_clouds_sprite_a_ = std::make_unique<MovingSprite>(bottom_clouds_texture_, SDL_FRect{.x = 0, .y = BOTTOM_CLOUDS_Y, .w = rect_.w, .h = static_cast<float>(bottom_clouds_texture_->getHeight())});
|
||||
bottom_clouds_sprite_b_ = std::make_unique<MovingSprite>(bottom_clouds_texture_, SDL_FRect{.x = rect_.w, .y = BOTTOM_CLOUDS_Y, .w = rect_.w, .h = static_cast<float>(bottom_clouds_texture_->getHeight())});
|
||||
|
||||
buildings_sprite_ = std::make_unique<Sprite>(buildings_texture_);
|
||||
gradient_sprite_ = std::make_unique<Sprite>(gradients_texture_, 0, 0, rect_.w, rect_.h);
|
||||
|
||||
@@ -68,7 +68,7 @@ void Fade::render() {
|
||||
}
|
||||
|
||||
// Actualiza las variables internas
|
||||
void Fade::update(float delta_time) {
|
||||
void Fade::update(float /*delta_time*/) {
|
||||
switch (state_) {
|
||||
case State::PRE:
|
||||
updatePreState();
|
||||
|
||||
@@ -141,7 +141,7 @@ namespace GIF {
|
||||
}
|
||||
|
||||
// Procesa el Image Descriptor y retorna el vector de datos sin comprimir.
|
||||
auto processImageDescriptor(const uint8_t *&buffer, const std::vector<RGB> &gct, int resolution_bits) -> std::vector<uint8_t> {
|
||||
auto processImageDescriptor(const uint8_t *&buffer, const std::vector<RGB> & /*gct*/, int /*resolution_bits*/) -> std::vector<uint8_t> {
|
||||
ImageDescriptor image_descriptor;
|
||||
readBytes(buffer, &image_descriptor, sizeof(ImageDescriptor));
|
||||
|
||||
|
||||
@@ -17,16 +17,17 @@
|
||||
#ifndef NO_SHADERS
|
||||
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp" // Para SDL3GPUShader
|
||||
#endif
|
||||
#include "core/rendering/text.hpp" // Para Text
|
||||
#include "core/rendering/texture.hpp" // Para Texture
|
||||
#include "core/resources/asset.hpp" // Para Asset
|
||||
#include "core/resources/resource.hpp" // Para Resource
|
||||
#include "core/system/director.hpp" // Para Director::debug_config
|
||||
#include "game/options.hpp" // Para Video, video, Window, window
|
||||
#include "game/ui/notifier.hpp" // Para Notifier
|
||||
#include "game/ui/service_menu.hpp" // Para ServiceMenu
|
||||
#include "utils/param.hpp" // Para Param, param, ParamGame, ParamDebug
|
||||
#include "utils/utils.hpp" // Para toLower
|
||||
#include "core/rendering/text.hpp" // Para Text
|
||||
#include "core/rendering/texture.hpp" // Para Texture
|
||||
#include "core/resources/asset.hpp" // Para Asset
|
||||
#ifdef _DEBUG
|
||||
#include "core/resources/resource.hpp" // Para Resource (només a debug, font 8bithud)
|
||||
#include "core/system/director.hpp" // Para Director::debug_config (només a debug)
|
||||
#endif
|
||||
#include "game/options.hpp" // Para Video, video, Window, window
|
||||
#include "game/ui/notifier.hpp" // Para Notifier
|
||||
#include "game/ui/service_menu.hpp" // Para ServiceMenu
|
||||
#include "utils/param.hpp" // Para Param, param, ParamGame, ParamDebug
|
||||
|
||||
// Singleton
|
||||
Screen* Screen::instance = nullptr;
|
||||
@@ -82,12 +83,7 @@ auto Screen::get() -> Screen* { return Screen::instance; }
|
||||
|
||||
// Constructor
|
||||
Screen::Screen()
|
||||
: window_(nullptr),
|
||||
renderer_(nullptr),
|
||||
game_canvas_(nullptr),
|
||||
service_menu_(nullptr),
|
||||
notifier_(nullptr),
|
||||
src_rect_(SDL_FRect{.x = 0, .y = 0, .w = param.game.width, .h = param.game.height}),
|
||||
: src_rect_(SDL_FRect{.x = 0, .y = 0, .w = param.game.width, .h = param.game.height}),
|
||||
dst_rect_(SDL_FRect{.x = 0, .y = 0, .w = param.game.width, .h = param.game.height}) {
|
||||
// Arranca SDL VIDEO, crea la ventana y el renderizador
|
||||
initSDLVideo();
|
||||
@@ -222,7 +218,7 @@ void Screen::handleCanvasResized() {
|
||||
|
||||
// Registra los callbacks nativos de Emscripten que restauran el canvas cuando
|
||||
// SDL3 no emite los events equivalentes. Fuera de Emscripten es un no-op.
|
||||
void Screen::registerEmscriptenEventCallbacks() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
void Screen::registerEmscriptenEventCallbacks() {
|
||||
#ifdef __EMSCRIPTEN__
|
||||
emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, EM_TRUE, onEmFullscreenChange);
|
||||
emscripten_set_orientationchange_callback(nullptr, EM_TRUE, onEmOrientationChange);
|
||||
@@ -341,7 +337,6 @@ auto Screen::buildDebugInfoText() const -> std::string {
|
||||
} else {
|
||||
const std::string PRESET_NAME = Options::postfx_presets.empty() ? "" : Options::postfx_presets.at(static_cast<size_t>(Options::video.shader.current_postfx_preset)).name;
|
||||
info_text += " - postfx " + toLower(PRESET_NAME);
|
||||
if (Options::video.supersampling.enabled) { info_text += " (ss)"; }
|
||||
}
|
||||
return info_text;
|
||||
}
|
||||
@@ -385,10 +380,6 @@ void Screen::initShaders() {
|
||||
std::cout << "Screen::initShaders: SDL3GPUShader::init() = " << (OK ? "OK" : "FAILED") << '\n';
|
||||
}
|
||||
if (self->shader_backend_ && self->shader_backend_->isHardwareAccelerated()) {
|
||||
self->shader_backend_->setLinearUpscale(Options::video.supersampling.linear_upscale);
|
||||
self->shader_backend_->setDownscaleAlgo(Options::video.supersampling.downscale_algo);
|
||||
self->shader_backend_->setOversample(Options::video.supersampling.enabled ? 3 : 1);
|
||||
|
||||
if (!Options::video.shader.enabled) {
|
||||
// Passthrough: POSTFX con parámetros a cero
|
||||
self->shader_backend_->setActiveShader(Rendering::ShaderType::POSTFX);
|
||||
@@ -593,36 +584,25 @@ void Screen::nextCrtPiPreset() {
|
||||
}
|
||||
}
|
||||
|
||||
// Alterna supersampling
|
||||
void Screen::toggleSupersampling() {
|
||||
Options::video.supersampling.enabled = !Options::video.supersampling.enabled;
|
||||
auto* self = Screen::get();
|
||||
if (self != nullptr && self->shader_backend_ && self->shader_backend_->isHardwareAccelerated()) {
|
||||
self->shader_backend_->setOversample(Options::video.supersampling.enabled ? 3 : 1);
|
||||
if (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) {
|
||||
self->applyCurrentCrtPiPreset();
|
||||
} else {
|
||||
self->applyCurrentPostFXPreset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Aplica el preset PostFX activo al backend
|
||||
void Screen::applyCurrentPostFXPreset() {
|
||||
if (!shader_backend_) { return; }
|
||||
shader_backend_->setOversample(Options::video.supersampling.enabled ? 3 : 1);
|
||||
Rendering::PostFXParams p{};
|
||||
if (Options::video.shader.enabled && !Options::postfx_presets.empty()) {
|
||||
const auto& preset = Options::postfx_presets.at(static_cast<size_t>(Options::video.shader.current_postfx_preset));
|
||||
p.vignette = preset.vignette;
|
||||
p.scanlines = preset.scanlines;
|
||||
p.chroma = preset.chroma;
|
||||
p.chroma_min = preset.chroma_min;
|
||||
p.chroma_max = preset.chroma_max;
|
||||
p.mask = preset.mask;
|
||||
p.gamma = preset.gamma;
|
||||
p.curvature = preset.curvature;
|
||||
p.bleeding = preset.bleeding;
|
||||
p.flicker = preset.flicker;
|
||||
std::cout << "Screen::applyCurrentPostFXPreset: preset='" << preset.name << "' scan=" << p.scanlines << " vign=" << p.vignette << " chroma=" << p.chroma << '\n';
|
||||
p.scan_dark_ratio = preset.scan_dark_ratio;
|
||||
p.scan_dark_floor = preset.scan_dark_floor;
|
||||
p.scan_edge_soft = preset.scan_edge_soft;
|
||||
std::cout << "Screen::applyCurrentPostFXPreset: preset='" << preset.name << "' scan=" << p.scanlines << " vign=" << p.vignette << " chroma=[" << p.chroma_min << ".." << p.chroma_max << "]\n";
|
||||
}
|
||||
shader_backend_->setPostFXParams(p);
|
||||
}
|
||||
@@ -662,13 +642,12 @@ void Screen::toggleIntegerScale() {
|
||||
}
|
||||
}
|
||||
|
||||
// Alterna entre activar y desactivar el V-Sync
|
||||
// Alterna entre activar y desactivar el V-Sync. ÚNICA via "user-facing" que
|
||||
// modifica la preferencia persistent (Options::video.vsync); la resta de
|
||||
// crides a setVSync apliquen l'estat al hardware sense escriure preferència.
|
||||
void Screen::toggleVSync() {
|
||||
Options::video.vsync = !Options::video.vsync;
|
||||
SDL_SetRenderVSync(renderer_, Options::video.vsync ? 1 : SDL_RENDERER_VSYNC_DISABLED);
|
||||
if (shader_backend_) {
|
||||
shader_backend_->setVSync(Options::video.vsync);
|
||||
}
|
||||
setVSync(Options::video.vsync);
|
||||
}
|
||||
|
||||
// Aplica Options::video.scale_mode a la textura del canvas de juego
|
||||
@@ -690,10 +669,16 @@ auto Screen::isHardwareAccelerated() -> bool {
|
||||
return self != nullptr && self->shader_backend_ && self->shader_backend_->isHardwareAccelerated();
|
||||
}
|
||||
|
||||
// Establece el estado del V-Sync
|
||||
// Aplica V-Sync al renderer SDL i al backend GPU. NO toca la preferència
|
||||
// persistent (Options::video.vsync) — és responsabilitat del caller (menú
|
||||
// servei, toggleVSync). Així Resource::beginLoad/finishBoot poden canviar
|
||||
// el vsync efectiu durant el preload sense clobberar la preferència de
|
||||
// l'usuari (bug històric: el seu vsync=true es perdia entre llançaments).
|
||||
void Screen::setVSync(bool enabled) {
|
||||
Options::video.vsync = enabled;
|
||||
SDL_SetRenderVSync(renderer_, enabled ? 1 : SDL_RENDERER_VSYNC_DISABLED);
|
||||
if (shader_backend_) {
|
||||
shader_backend_->setVSync(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
// Obtiene los punteros a los singletones
|
||||
@@ -718,7 +703,7 @@ void Screen::getSingletons() {
|
||||
// el viewport queda cacheado al tamaño de la ventana pequeña previa y el juego
|
||||
// se ve pequeño y centrado con barras negras alrededor.
|
||||
void Screen::applySettings() {
|
||||
SDL_SetRenderVSync(renderer_, Options::video.vsync ? 1 : SDL_RENDERER_VSYNC_DISABLED);
|
||||
setVSync(Options::video.vsync);
|
||||
setFullscreenMode();
|
||||
adjustWindowSize();
|
||||
SDL_SetRenderLogicalPresentation(renderer_, param.game.width, param.game.height, Options::video.integer_scale ? SDL_LOGICAL_PRESENTATION_INTEGER_SCALE : SDL_LOGICAL_PRESENTATION_LETTERBOX);
|
||||
|
||||
@@ -44,13 +44,12 @@ class Screen {
|
||||
// --- Efectos visuales ---
|
||||
void shake(int desp = 2, float delay_s = 0.05F, float duration_s = 0.133F) { shake_effect_.enable(src_rect_, dst_rect_, desp, delay_s, duration_s); }
|
||||
void flash(Color color, float duration_s = 0.167F, float delay_s = 0.0F) { flash_effect_ = FlashEffect(true, duration_s, delay_s, color); }
|
||||
static void toggleShaders(); // Alterna activar/desactivar shaders
|
||||
static void nextShader(); // Cambia entre PostFX y CrtPi
|
||||
static void nextPostFXPreset(); // Avanza al siguiente preset PostFX
|
||||
static void nextCrtPiPreset(); // Avanza al siguiente preset CrtPi
|
||||
static void toggleSupersampling(); // Alterna supersampling
|
||||
void toggleFilter(); // Alterna SDL_SCALEMODE_NEAREST ↔ SDL_SCALEMODE_LINEAR
|
||||
void applyFilter(); // Aplica Options::video.scale_mode a game_canvas_
|
||||
static void toggleShaders(); // Alterna activar/desactivar shaders
|
||||
static void nextShader(); // Cambia entre PostFX y CrtPi
|
||||
static void nextPostFXPreset(); // Avanza al siguiente preset PostFX
|
||||
static void nextCrtPiPreset(); // Avanza al siguiente preset CrtPi
|
||||
void toggleFilter(); // Alterna SDL_SCALEMODE_NEAREST ↔ SDL_SCALEMODE_LINEAR
|
||||
void applyFilter(); // Aplica Options::video.scale_mode a game_canvas_
|
||||
void toggleIntegerScale();
|
||||
void toggleVSync(); // Alterna entre activar y desactivar el V-Sync
|
||||
void setVSync(bool enabled); // Establece el estado del V-Sync
|
||||
@@ -214,11 +213,11 @@ class Screen {
|
||||
#endif
|
||||
|
||||
// --- Objetos y punteros ---
|
||||
SDL_Window* window_; // Ventana de la aplicación
|
||||
SDL_Renderer* renderer_; // El renderizador de la ventana
|
||||
SDL_Texture* game_canvas_; // Textura donde se dibuja todo antes de volcarse al renderizador
|
||||
ServiceMenu* service_menu_; // Objeto para mostrar el menú de servicio
|
||||
Notifier* notifier_; // Objeto para mostrar las notificaciones por pantalla
|
||||
SDL_Window* window_ = nullptr; // Ventana de la aplicación
|
||||
SDL_Renderer* renderer_ = nullptr; // El renderizador de la ventana
|
||||
SDL_Texture* game_canvas_ = nullptr; // Textura donde se dibuja todo antes de volcarse al renderizador
|
||||
ServiceMenu* service_menu_ = nullptr; // Objeto para mostrar el menú de servicio
|
||||
Notifier* notifier_ = nullptr; // Objeto para mostrar las notificaciones por pantalla
|
||||
std::shared_ptr<Text> text_; // Objeto para escribir texto en pantalla
|
||||
std::unique_ptr<Rendering::ShaderBackend> shader_backend_; // Backend de shaders (SDL3GPU)
|
||||
|
||||
@@ -236,20 +235,20 @@ class Screen {
|
||||
#endif
|
||||
|
||||
// --- Métodos internos ---
|
||||
auto initSDLVideo() -> bool; // Arranca SDL VIDEO y crea la ventana
|
||||
void registerEmscriptenEventCallbacks(); // Registra callbacks nativos para restaurar el canvas en wasm (no-op fuera de emscripten)
|
||||
void renderFlash(); // Dibuja el efecto de flash en la pantalla
|
||||
void renderShake(); // Aplica el efecto de agitar la pantalla
|
||||
void renderInfo() const; // Muestra información por pantalla
|
||||
auto initSDLVideo() -> bool; // Arranca SDL VIDEO y crea la ventana
|
||||
void registerEmscriptenEventCallbacks(); // Registra callbacks nativos para restaurar el canvas en wasm (no-op fuera de emscripten)
|
||||
void renderFlash(); // Dibuja el efecto de flash en la pantalla
|
||||
void renderShake(); // Aplica el efecto de agitar la pantalla
|
||||
void renderInfo() const; // Muestra información por pantalla
|
||||
[[nodiscard]] auto buildDebugInfoText() const -> std::string; // Compone fps + driver + shader/preset para renderInfo
|
||||
void renderPresent(); // Selecciona y ejecuta el método de renderizado adecuado
|
||||
void applyCurrentPostFXPreset(); // Aplica el preset PostFX activo al backend
|
||||
void applyCurrentCrtPiPreset(); // Aplica el preset CrtPi activo al backend
|
||||
void adjustWindowSize(); // Calcula el tamaño de la ventana
|
||||
void getDisplayInfo(); // Obtiene información sobre la pantalla
|
||||
void renderOverlays(); // Renderiza todos los overlays y efectos
|
||||
void renderAttenuate(); // Atenúa la pantalla
|
||||
void createText(); // Crea el objeto de texto
|
||||
void renderPresent(); // Selecciona y ejecuta el método de renderizado adecuado
|
||||
void applyCurrentPostFXPreset(); // Aplica el preset PostFX activo al backend
|
||||
void applyCurrentCrtPiPreset(); // Aplica el preset CrtPi activo al backend
|
||||
void adjustWindowSize(); // Calcula el tamaño de la ventana
|
||||
void getDisplayInfo(); // Obtiene información sobre la pantalla
|
||||
void renderOverlays(); // Renderiza todos los overlays y efectos
|
||||
void renderAttenuate(); // Atenúa la pantalla
|
||||
void createText(); // Crea el objeto de texto
|
||||
|
||||
// --- Constructores y destructor privados (singleton) ---
|
||||
Screen(); // Constructor privado
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef __APPLE__
|
||||
|
||||
// Fragment shader del shader "crtpi" (algoritme CRT-Pi): scanlines amb
|
||||
// pesos gaussians, multisample opcional, gamma i màscara de subpíxels.
|
||||
namespace Rendering::Msl {
|
||||
|
||||
inline constexpr const char* kCrtpiFrag = R"(
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
|
||||
struct PostVOut {
|
||||
float4 pos [[position]];
|
||||
float2 uv;
|
||||
};
|
||||
|
||||
struct CrtPiUniforms {
|
||||
float scanline_weight;
|
||||
float scanline_gap_brightness;
|
||||
float bloom_factor;
|
||||
float input_gamma;
|
||||
float output_gamma;
|
||||
float mask_brightness;
|
||||
float curvature_x;
|
||||
float curvature_y;
|
||||
int mask_type;
|
||||
int enable_scanlines;
|
||||
int enable_multisample;
|
||||
int enable_gamma;
|
||||
int enable_curvature;
|
||||
int enable_sharper;
|
||||
float texture_width;
|
||||
float texture_height;
|
||||
};
|
||||
|
||||
static float2 crtpi_distort(float2 coord, float2 screen_scale, float cx, float cy) {
|
||||
float2 curvature = float2(cx, cy);
|
||||
float2 barrel_scale = 1.0f - (0.23f * curvature);
|
||||
coord *= screen_scale;
|
||||
coord -= 0.5f;
|
||||
float rsq = coord.x * coord.x + coord.y * coord.y;
|
||||
coord += coord * (curvature * rsq);
|
||||
coord *= barrel_scale;
|
||||
if (abs(coord.x) >= 0.5f || abs(coord.y) >= 0.5f) { return float2(-1.0f); }
|
||||
coord += 0.5f;
|
||||
coord /= screen_scale;
|
||||
return coord;
|
||||
}
|
||||
|
||||
static float crtpi_scan_weight(float dist, float sw, float gap) {
|
||||
return max(1.0f - dist * dist * sw, gap);
|
||||
}
|
||||
|
||||
static float crtpi_scan_line(float dy, float filter_w, float sw, float gap, bool ms) {
|
||||
float w = crtpi_scan_weight(dy, sw, gap);
|
||||
if (ms) {
|
||||
w += crtpi_scan_weight(dy - filter_w, sw, gap);
|
||||
w += crtpi_scan_weight(dy + filter_w, sw, gap);
|
||||
w *= 0.3333333f;
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
fragment float4 crtpi_fs(PostVOut in [[stage_in]],
|
||||
texture2d<float> tex [[texture(0)]],
|
||||
sampler samp [[sampler(0)]],
|
||||
constant CrtPiUniforms& u [[buffer(0)]]) {
|
||||
float2 tex_size = float2(u.texture_width, u.texture_height);
|
||||
// Amplada del filtre de scanline analític. 768 = alçada de referència
|
||||
// CRT a la qual es va tarar l'algoritme original; 3 = divisió per
|
||||
// subpíxel (R/G/B) del multisample. El resultat escala amb la textura
|
||||
// d'entrada, de manera que més alçada → filtre més fi.
|
||||
const float CRT_REFERENCE_HEIGHT = 768.0f;
|
||||
const float SUBPIXEL_DIV = 3.0f;
|
||||
float filter_width = (CRT_REFERENCE_HEIGHT / u.texture_height) / SUBPIXEL_DIV;
|
||||
float2 texcoord = in.uv;
|
||||
|
||||
if (u.enable_curvature != 0) {
|
||||
texcoord = crtpi_distort(texcoord, float2(1.0f, 1.0f), u.curvature_x, u.curvature_y);
|
||||
if (texcoord.x < 0.0f) { return float4(0.0f, 0.0f, 0.0f, 1.0f); }
|
||||
}
|
||||
|
||||
float2 coord_in_pixels = texcoord * tex_size;
|
||||
float2 tc;
|
||||
float scan_weight;
|
||||
|
||||
if (u.enable_sharper != 0) {
|
||||
float2 temp = floor(coord_in_pixels) + 0.5f;
|
||||
tc = temp / tex_size;
|
||||
float2 deltas = coord_in_pixels - temp;
|
||||
scan_weight = crtpi_scan_line(deltas.y, filter_width, u.scanline_weight, u.scanline_gap_brightness, u.enable_multisample != 0);
|
||||
float2 signs = sign(deltas);
|
||||
deltas.x *= 2.0f;
|
||||
deltas = deltas * deltas;
|
||||
deltas.y = deltas.y * deltas.y;
|
||||
deltas.x *= 0.5f;
|
||||
deltas.y *= 8.0f;
|
||||
deltas /= tex_size;
|
||||
deltas *= signs;
|
||||
tc = tc + deltas;
|
||||
} else {
|
||||
float temp_y = floor(coord_in_pixels.y) + 0.5f;
|
||||
float y_coord = temp_y / tex_size.y;
|
||||
float dy = coord_in_pixels.y - temp_y;
|
||||
scan_weight = crtpi_scan_line(dy, filter_width, u.scanline_weight, u.scanline_gap_brightness, u.enable_multisample != 0);
|
||||
float sign_y = sign(dy);
|
||||
dy = dy * dy;
|
||||
dy = dy * dy;
|
||||
dy *= 8.0f;
|
||||
dy /= tex_size.y;
|
||||
dy *= sign_y;
|
||||
tc = float2(texcoord.x, y_coord + dy);
|
||||
}
|
||||
|
||||
float3 colour = tex.sample(samp, tc).rgb;
|
||||
|
||||
if (u.enable_scanlines != 0) {
|
||||
if (u.enable_gamma != 0) { colour = pow(colour, float3(u.input_gamma)); }
|
||||
colour *= scan_weight * u.bloom_factor;
|
||||
if (u.enable_gamma != 0) { colour = pow(colour, float3(1.0f / u.output_gamma)); }
|
||||
}
|
||||
|
||||
if (u.mask_type == 1) {
|
||||
float wm = fract(in.pos.x * 0.5f);
|
||||
float3 mask = (wm < 0.5f) ? float3(u.mask_brightness, 1.0f, u.mask_brightness)
|
||||
: float3(1.0f, u.mask_brightness, 1.0f);
|
||||
colour *= mask;
|
||||
} else if (u.mask_type == 2) {
|
||||
float wm = fract(in.pos.x * 0.3333333f);
|
||||
float3 mask = float3(u.mask_brightness);
|
||||
if (wm < 0.3333333f) mask.x = 1.0f;
|
||||
else if (wm < 0.6666666f) mask.y = 1.0f;
|
||||
else mask.z = 1.0f;
|
||||
colour *= mask;
|
||||
}
|
||||
|
||||
return float4(colour, 1.0f);
|
||||
}
|
||||
)";
|
||||
|
||||
} // namespace Rendering::Msl
|
||||
|
||||
#endif // __APPLE__
|
||||
@@ -0,0 +1,168 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef __APPLE__
|
||||
|
||||
// Fragment shader del shader "postfx": vignette, chroma, scanlines, mask,
|
||||
// gamma, curvature, bleeding i flicker. Els paràmetres venen via uniforms.
|
||||
//
|
||||
// IMPORTANT: mantenir sincronitzat a mà amb data/shaders/postfx.frag. SDL3 GPU
|
||||
// compila aquest string MSL en runtime; no hi ha generador automàtic. Qualsevol
|
||||
// canvi a la struct d'uniforms o a la lògica del GLSL cal replicar-lo ací al
|
||||
// mateix commit. Mida total = 64 bytes (4 × vec4).
|
||||
namespace Rendering::Msl {
|
||||
|
||||
inline constexpr const char* kPostfxFrag = R"(
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
|
||||
struct PostVOut {
|
||||
float4 pos [[position]];
|
||||
float2 uv;
|
||||
};
|
||||
|
||||
struct PostFXUniforms {
|
||||
float vignette_strength;
|
||||
float chroma_min;
|
||||
float scanline_strength;
|
||||
float screen_height;
|
||||
float mask_strength;
|
||||
float gamma_strength;
|
||||
float curvature;
|
||||
float bleeding;
|
||||
float pixel_scale;
|
||||
float time;
|
||||
float flicker;
|
||||
float chroma_max;
|
||||
// vec4 #3 — paràmetres de scanlines (exposats per preset YAML)
|
||||
float scan_dark_ratio;
|
||||
float scan_dark_floor;
|
||||
float scan_edge_soft;
|
||||
float pad3;
|
||||
};
|
||||
|
||||
// Mostreig bilinear horitzontal d'un canal RGB. Evita el "tic-tac" del sampler
|
||||
// NEAREST quan l'offset de chroma és subpíxel.
|
||||
static float sampleBilinearX(float2 uv_target, int channel, texture2d<float> scene, sampler samp) {
|
||||
float2 tex_size = float2(scene.get_width(), scene.get_height());
|
||||
float px = uv_target.x * tex_size.x - 0.5f;
|
||||
float p_floor = floor(px);
|
||||
float f = px - p_floor;
|
||||
float4 c0 = scene.sample(samp, float2((p_floor + 0.5f) / tex_size.x, uv_target.y));
|
||||
float4 c1 = scene.sample(samp, float2((p_floor + 1.5f) / tex_size.x, uv_target.y));
|
||||
return mix(c0[channel], c1[channel], f);
|
||||
}
|
||||
|
||||
static float3 rgb_to_ycc(float3 rgb) {
|
||||
return float3(
|
||||
0.299f*rgb.r + 0.587f*rgb.g + 0.114f*rgb.b,
|
||||
-0.169f*rgb.r - 0.331f*rgb.g + 0.500f*rgb.b + 0.5f,
|
||||
0.500f*rgb.r - 0.419f*rgb.g - 0.081f*rgb.b + 0.5f
|
||||
);
|
||||
}
|
||||
static float3 ycc_to_rgb(float3 ycc) {
|
||||
float y = ycc.x;
|
||||
float cb = ycc.y - 0.5f;
|
||||
float cr = ycc.z - 0.5f;
|
||||
return clamp(float3(
|
||||
y + 1.402f*cr,
|
||||
y - 0.344f*cb - 0.714f*cr,
|
||||
y + 1.772f*cb
|
||||
), 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
fragment float4 postfx_fs(PostVOut in [[stage_in]],
|
||||
texture2d<float> scene [[texture(0)]],
|
||||
sampler samp [[sampler(0)]],
|
||||
constant PostFXUniforms& u [[buffer(0)]]) {
|
||||
float2 uv = in.uv;
|
||||
|
||||
if (u.curvature > 0.0f) {
|
||||
float2 c = uv - 0.5f;
|
||||
float rsq = dot(c, c);
|
||||
float2 dist = float2(0.05f, 0.1f) * u.curvature;
|
||||
float2 barrelScale = 1.0f - 0.23f * dist;
|
||||
c += c * (dist * rsq);
|
||||
c *= barrelScale;
|
||||
if (abs(c.x) >= 0.5f || abs(c.y) >= 0.5f) {
|
||||
return float4(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
}
|
||||
uv = c + 0.5f;
|
||||
}
|
||||
|
||||
float3 base = scene.sample(samp, uv).rgb;
|
||||
|
||||
float3 colour;
|
||||
if (u.bleeding > 0.0f) {
|
||||
float tw = float(scene.get_width());
|
||||
float step = 1.0f / tw;
|
||||
float3 ycc = rgb_to_ycc(base);
|
||||
float3 ycc_l2 = rgb_to_ycc(scene.sample(samp, uv - float2(2.0f*step, 0.0f)).rgb);
|
||||
float3 ycc_l1 = rgb_to_ycc(scene.sample(samp, uv - float2(1.0f*step, 0.0f)).rgb);
|
||||
float3 ycc_r1 = rgb_to_ycc(scene.sample(samp, uv + float2(1.0f*step, 0.0f)).rgb);
|
||||
float3 ycc_r2 = rgb_to_ycc(scene.sample(samp, uv + float2(2.0f*step, 0.0f)).rgb);
|
||||
ycc.yz = (ycc_l2.yz + ycc_l1.yz*2.0f + ycc.yz*2.0f + ycc_r1.yz*2.0f + ycc_r2.yz) / 8.0f;
|
||||
colour = mix(base, ycc_to_rgb(ycc), u.bleeding);
|
||||
} else {
|
||||
colour = base;
|
||||
}
|
||||
|
||||
// Chroma — varia entre chroma_min i chroma_max via sinusoidal; si min == max
|
||||
// queda estàtic. Mostreig bilinear horitzontal per evitar el "tic-tac" del
|
||||
// NEAREST sampler amb offsets subpíxel.
|
||||
if (u.chroma_min > 0.0f || u.chroma_max > 0.0f) {
|
||||
float ca = mix(u.chroma_min, u.chroma_max, 0.5f + 0.5f * sin(u.time * 7.3f)) * 0.005f;
|
||||
colour.r = sampleBilinearX(uv + float2(ca, 0.0f), 0, scene, samp);
|
||||
colour.b = sampleBilinearX(uv - float2(ca, 0.0f), 2, scene, samp);
|
||||
}
|
||||
|
||||
if (u.gamma_strength > 0.0f) {
|
||||
float3 lin = pow(colour, float3(2.4f));
|
||||
colour = mix(colour, lin, u.gamma_strength);
|
||||
}
|
||||
|
||||
// Scanlines — 3 subpíxels per fila lògica (2 brillants + 1 fosca). Transició
|
||||
// suavitzada amb smoothstep d'ample ≈ 1 píxel físic (estil crtpi: filtratge
|
||||
// analític continu). scan_edge_soft = 0 recupera el step dur de l'original.
|
||||
if (u.scanline_strength > 0.0f) {
|
||||
float ps = max(u.pixel_scale, 1.0f);
|
||||
float sub = fract(uv.y * u.screen_height);
|
||||
float dark_center = 1.0f - u.scan_dark_ratio * 0.5f;
|
||||
float d = abs(sub - dark_center);
|
||||
d = min(d, 1.0f - d);
|
||||
float half_width = u.scan_dark_ratio * 0.5f;
|
||||
float softness = u.scan_edge_soft * 0.5f / ps;
|
||||
float band = 1.0f - smoothstep(half_width - softness, half_width + softness, d);
|
||||
float scan = mix(1.0f, u.scan_dark_floor, band);
|
||||
colour *= mix(1.0f, scan, u.scanline_strength);
|
||||
}
|
||||
|
||||
if (u.gamma_strength > 0.0f) {
|
||||
float3 enc = pow(colour, float3(1.0f/2.2f));
|
||||
colour = mix(colour, enc, u.gamma_strength);
|
||||
}
|
||||
|
||||
float2 d = uv - 0.5f;
|
||||
float vignette = 1.0f - dot(d, d) * u.vignette_strength;
|
||||
colour *= clamp(vignette, 0.0f, 1.0f);
|
||||
|
||||
if (u.mask_strength > 0.0f) {
|
||||
float whichMask = fract(in.pos.x * 0.3333333f);
|
||||
float3 mask = float3(0.80f);
|
||||
if (whichMask < 0.3333333f) mask.x = 1.0f;
|
||||
else if (whichMask < 0.6666667f) mask.y = 1.0f;
|
||||
else mask.z = 1.0f;
|
||||
colour = mix(colour, colour * mask, u.mask_strength);
|
||||
}
|
||||
|
||||
if (u.flicker > 0.0f) {
|
||||
float flicker_wave = sin(u.time * 100.0f) * 0.5f + 0.5f;
|
||||
colour *= 1.0f - u.flicker * 0.04f * flicker_wave;
|
||||
}
|
||||
|
||||
return float4(colour, 1.0f);
|
||||
}
|
||||
)";
|
||||
|
||||
} // namespace Rendering::Msl
|
||||
|
||||
#endif // __APPLE__
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef __APPLE__
|
||||
|
||||
// Vertex shader compartit per tots els pipelines de post-procés:
|
||||
// fullscreen-triangle que cobreix tota l'àrea del swapchain amb UVs a [0,1].
|
||||
namespace Rendering::Msl {
|
||||
|
||||
inline constexpr const char* kPostfxVert = 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;
|
||||
}
|
||||
)";
|
||||
|
||||
} // namespace Rendering::Msl
|
||||
|
||||
#endif // __APPLE__
|
||||
@@ -2,349 +2,22 @@
|
||||
|
||||
#include <SDL3/SDL_log.h>
|
||||
|
||||
#include <algorithm> // std::min, std::max, std::floor
|
||||
#include <cmath> // std::floor, std::ceil
|
||||
#include <algorithm> // std::min, std::max
|
||||
#include <cmath> // std::floor
|
||||
#include <cstring> // memcpy, strlen
|
||||
#include <iostream> // Para std::cout
|
||||
|
||||
#ifndef __APPLE__
|
||||
#include "core/rendering/sdl3gpu/spv/crtpi_frag_spv.h"
|
||||
#include "core/rendering/sdl3gpu/spv/downscale_frag_spv.h"
|
||||
#include "core/rendering/sdl3gpu/spv/postfx_frag_spv.h"
|
||||
#include "core/rendering/sdl3gpu/spv/postfx_vert_spv.h"
|
||||
#include "core/rendering/sdl3gpu/spv/upscale_frag_spv.h"
|
||||
#endif
|
||||
|
||||
#ifdef __APPLE__
|
||||
// ============================================================================
|
||||
// MSL shaders (Metal Shading Language) — macOS
|
||||
// ============================================================================
|
||||
|
||||
// NOLINTBEGIN(readability-identifier-naming)
|
||||
static const char* POSTFX_VERT_MSL = R"(
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
|
||||
struct PostVOut {
|
||||
float4 pos [[position]];
|
||||
float2 uv;
|
||||
};
|
||||
|
||||
vertex PostVOut postfx_vs(uint vid [[vertex_id]]) {
|
||||
const float2 positions[3] = { {-1.0, -1.0}, {3.0, -1.0}, {-1.0, 3.0} };
|
||||
const float2 uvs[3] = { { 0.0, 1.0}, {2.0, 1.0}, { 0.0,-1.0} };
|
||||
PostVOut out;
|
||||
out.pos = float4(positions[vid], 0.0, 1.0);
|
||||
out.uv = uvs[vid];
|
||||
return out;
|
||||
}
|
||||
)";
|
||||
|
||||
static const char* POSTFX_FRAG_MSL = R"(
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
|
||||
struct PostVOut {
|
||||
float4 pos [[position]];
|
||||
float2 uv;
|
||||
};
|
||||
|
||||
struct PostFXUniforms {
|
||||
float vignette_strength;
|
||||
float chroma_strength;
|
||||
float scanline_strength;
|
||||
float screen_height;
|
||||
float mask_strength;
|
||||
float gamma_strength;
|
||||
float curvature;
|
||||
float bleeding;
|
||||
float pixel_scale;
|
||||
float time;
|
||||
float oversample;
|
||||
float flicker;
|
||||
};
|
||||
|
||||
static float3 rgb_to_ycc(float3 rgb) {
|
||||
return float3(
|
||||
0.299f*rgb.r + 0.587f*rgb.g + 0.114f*rgb.b,
|
||||
-0.169f*rgb.r - 0.331f*rgb.g + 0.500f*rgb.b + 0.5f,
|
||||
0.500f*rgb.r - 0.419f*rgb.g - 0.081f*rgb.b + 0.5f
|
||||
);
|
||||
}
|
||||
static float3 ycc_to_rgb(float3 ycc) {
|
||||
float y = ycc.x;
|
||||
float cb = ycc.y - 0.5f;
|
||||
float cr = ycc.z - 0.5f;
|
||||
return clamp(float3(
|
||||
y + 1.402f*cr,
|
||||
y - 0.344f*cb - 0.714f*cr,
|
||||
y + 1.772f*cb
|
||||
), 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
fragment float4 postfx_fs(PostVOut in [[stage_in]],
|
||||
texture2d<float> scene [[texture(0)]],
|
||||
sampler samp [[sampler(0)]],
|
||||
constant PostFXUniforms& u [[buffer(0)]]) {
|
||||
float2 uv = in.uv;
|
||||
|
||||
if (u.curvature > 0.0f) {
|
||||
float2 c = uv - 0.5f;
|
||||
float rsq = dot(c, c);
|
||||
float2 dist = float2(0.05f, 0.1f) * u.curvature;
|
||||
float2 barrelScale = 1.0f - 0.23f * dist;
|
||||
c += c * (dist * rsq);
|
||||
c *= barrelScale;
|
||||
if (abs(c.x) >= 0.5f || abs(c.y) >= 0.5f) {
|
||||
return float4(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
}
|
||||
uv = c + 0.5f;
|
||||
}
|
||||
|
||||
float3 base = scene.sample(samp, uv).rgb;
|
||||
|
||||
float3 colour;
|
||||
if (u.bleeding > 0.0f) {
|
||||
float tw = float(scene.get_width());
|
||||
float step = u.oversample / tw;
|
||||
float3 ycc = rgb_to_ycc(base);
|
||||
float3 ycc_l2 = rgb_to_ycc(scene.sample(samp, uv - float2(2.0f*step, 0.0f)).rgb);
|
||||
float3 ycc_l1 = rgb_to_ycc(scene.sample(samp, uv - float2(1.0f*step, 0.0f)).rgb);
|
||||
float3 ycc_r1 = rgb_to_ycc(scene.sample(samp, uv + float2(1.0f*step, 0.0f)).rgb);
|
||||
float3 ycc_r2 = rgb_to_ycc(scene.sample(samp, uv + float2(2.0f*step, 0.0f)).rgb);
|
||||
ycc.yz = (ycc_l2.yz + ycc_l1.yz*2.0f + ycc.yz*2.0f + ycc_r1.yz*2.0f + ycc_r2.yz) / 8.0f;
|
||||
colour = mix(base, ycc_to_rgb(ycc), u.bleeding);
|
||||
} else {
|
||||
colour = base;
|
||||
}
|
||||
|
||||
float ca = u.chroma_strength * 0.005f * (1.0f + 0.15f * sin(u.time * 7.3f));
|
||||
colour.r = scene.sample(samp, uv + float2(ca, 0.0f)).r;
|
||||
colour.b = scene.sample(samp, uv - float2(ca, 0.0f)).b;
|
||||
|
||||
if (u.gamma_strength > 0.0f) {
|
||||
float3 lin = pow(colour, float3(2.4f));
|
||||
colour = mix(colour, lin, u.gamma_strength);
|
||||
}
|
||||
|
||||
const float SCAN_DARK_RATIO = 0.333f;
|
||||
const float SCAN_DARK_FLOOR = 0.42f;
|
||||
if (u.scanline_strength > 0.0f) {
|
||||
float ps = max(1.0f, round(u.pixel_scale));
|
||||
float frac_in_row = fract(uv.y * u.screen_height);
|
||||
float row_pos = floor(frac_in_row * ps);
|
||||
float bright_rows = (ps < 2.0f) ? ps : ((ps < 3.0f) ? 1.0f : floor(ps * (1.0f - SCAN_DARK_RATIO)));
|
||||
float is_dark = step(bright_rows, row_pos);
|
||||
float scan = mix(1.0f, SCAN_DARK_FLOOR, is_dark);
|
||||
colour *= mix(1.0f, scan, u.scanline_strength);
|
||||
}
|
||||
|
||||
if (u.gamma_strength > 0.0f) {
|
||||
float3 enc = pow(colour, float3(1.0f/2.2f));
|
||||
colour = mix(colour, enc, u.gamma_strength);
|
||||
}
|
||||
|
||||
float2 d = uv - 0.5f;
|
||||
float vignette = 1.0f - dot(d, d) * u.vignette_strength;
|
||||
colour *= clamp(vignette, 0.0f, 1.0f);
|
||||
|
||||
if (u.mask_strength > 0.0f) {
|
||||
float whichMask = fract(in.pos.x * 0.3333333f);
|
||||
float3 mask = float3(0.80f);
|
||||
if (whichMask < 0.3333333f) mask.x = 1.0f;
|
||||
else if (whichMask < 0.6666667f) mask.y = 1.0f;
|
||||
else mask.z = 1.0f;
|
||||
colour = mix(colour, colour * mask, u.mask_strength);
|
||||
}
|
||||
|
||||
if (u.flicker > 0.0f) {
|
||||
float flicker_wave = sin(u.time * 100.0f) * 0.5f + 0.5f;
|
||||
colour *= 1.0f - u.flicker * 0.04f * flicker_wave;
|
||||
}
|
||||
|
||||
return float4(colour, 1.0f);
|
||||
}
|
||||
)";
|
||||
static const char* UPSCALE_FRAG_MSL = R"(
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
struct VertOut { float4 pos [[position]]; float2 uv; };
|
||||
fragment float4 upscale_fs(VertOut in [[stage_in]],
|
||||
texture2d<float> scene [[texture(0)]],
|
||||
sampler smp [[sampler(0)]])
|
||||
{
|
||||
return scene.sample(smp, in.uv);
|
||||
}
|
||||
)";
|
||||
|
||||
static const char* DOWNSCALE_FRAG_MSL = R"(
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
struct VertOut { float4 pos [[position]]; float2 uv; };
|
||||
struct DownscaleUniforms { int algorithm; float pad0; float pad1; float pad2; };
|
||||
|
||||
static float lanczos_w(float t, float a) {
|
||||
t = abs(t);
|
||||
if (t < 0.0001f) { return 1.0f; }
|
||||
if (t >= a) { return 0.0f; }
|
||||
const float PI = 3.14159265358979f;
|
||||
float pt = PI * t;
|
||||
return (a * sin(pt) * sin(pt / a)) / (pt * pt);
|
||||
}
|
||||
|
||||
fragment float4 downscale_fs(VertOut in [[stage_in]],
|
||||
texture2d<float> source [[texture(0)]],
|
||||
sampler smp [[sampler(0)]],
|
||||
constant DownscaleUniforms& u [[buffer(0)]])
|
||||
{
|
||||
float2 src_size = float2(source.get_width(), source.get_height());
|
||||
float2 p = in.uv * src_size;
|
||||
float2 p_floor = floor(p);
|
||||
float a = (u.algorithm == 0) ? 2.0f : 3.0f;
|
||||
int win = int(a);
|
||||
float4 color = float4(0.0f);
|
||||
float weight_sum = 0.0f;
|
||||
for (int j = -win; j <= win; j++) {
|
||||
for (int i = -win; i <= win; i++) {
|
||||
float2 tap_center = p_floor + float2(float(i), float(j)) + 0.5f;
|
||||
float2 offset = tap_center - p;
|
||||
float w = lanczos_w(offset.x, a) * lanczos_w(offset.y, a);
|
||||
color += source.sample(smp, tap_center / src_size) * w;
|
||||
weight_sum += w;
|
||||
}
|
||||
}
|
||||
return (weight_sum > 0.0f) ? (color / weight_sum) : float4(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
}
|
||||
)";
|
||||
static const char* CRTPI_FRAG_MSL = R"(
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
|
||||
struct PostVOut {
|
||||
float4 pos [[position]];
|
||||
float2 uv;
|
||||
};
|
||||
|
||||
struct CrtPiUniforms {
|
||||
float scanline_weight;
|
||||
float scanline_gap_brightness;
|
||||
float bloom_factor;
|
||||
float input_gamma;
|
||||
float output_gamma;
|
||||
float mask_brightness;
|
||||
float curvature_x;
|
||||
float curvature_y;
|
||||
int mask_type;
|
||||
int enable_scanlines;
|
||||
int enable_multisample;
|
||||
int enable_gamma;
|
||||
int enable_curvature;
|
||||
int enable_sharper;
|
||||
float texture_width;
|
||||
float texture_height;
|
||||
};
|
||||
|
||||
static float2 crtpi_distort(float2 coord, float2 screen_scale, float cx, float cy) {
|
||||
float2 curvature = float2(cx, cy);
|
||||
float2 barrel_scale = 1.0f - (0.23f * curvature);
|
||||
coord *= screen_scale;
|
||||
coord -= 0.5f;
|
||||
float rsq = coord.x * coord.x + coord.y * coord.y;
|
||||
coord += coord * (curvature * rsq);
|
||||
coord *= barrel_scale;
|
||||
if (abs(coord.x) >= 0.5f || abs(coord.y) >= 0.5f) { return float2(-1.0f); }
|
||||
coord += 0.5f;
|
||||
coord /= screen_scale;
|
||||
return coord;
|
||||
}
|
||||
|
||||
static float crtpi_scan_weight(float dist, float sw, float gap) {
|
||||
return max(1.0f - dist * dist * sw, gap);
|
||||
}
|
||||
|
||||
static float crtpi_scan_line(float dy, float filter_w, float sw, float gap, bool ms) {
|
||||
float w = crtpi_scan_weight(dy, sw, gap);
|
||||
if (ms) {
|
||||
w += crtpi_scan_weight(dy - filter_w, sw, gap);
|
||||
w += crtpi_scan_weight(dy + filter_w, sw, gap);
|
||||
w *= 0.3333333f;
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
fragment float4 crtpi_fs(PostVOut in [[stage_in]],
|
||||
texture2d<float> tex [[texture(0)]],
|
||||
sampler samp [[sampler(0)]],
|
||||
constant CrtPiUniforms& u [[buffer(0)]]) {
|
||||
float2 tex_size = float2(u.texture_width, u.texture_height);
|
||||
float filter_width = (768.0f / u.texture_height) / 3.0f;
|
||||
float2 texcoord = in.uv;
|
||||
|
||||
if (u.enable_curvature != 0) {
|
||||
texcoord = crtpi_distort(texcoord, float2(1.0f, 1.0f), u.curvature_x, u.curvature_y);
|
||||
if (texcoord.x < 0.0f) { return float4(0.0f, 0.0f, 0.0f, 1.0f); }
|
||||
}
|
||||
|
||||
float2 coord_in_pixels = texcoord * tex_size;
|
||||
float2 tc;
|
||||
float scan_weight;
|
||||
|
||||
if (u.enable_sharper != 0) {
|
||||
float2 temp = floor(coord_in_pixels) + 0.5f;
|
||||
tc = temp / tex_size;
|
||||
float2 deltas = coord_in_pixels - temp;
|
||||
scan_weight = crtpi_scan_line(deltas.y, filter_width, u.scanline_weight, u.scanline_gap_brightness, u.enable_multisample != 0);
|
||||
float2 signs = sign(deltas);
|
||||
deltas.x *= 2.0f;
|
||||
deltas = deltas * deltas;
|
||||
deltas.y = deltas.y * deltas.y;
|
||||
deltas.x *= 0.5f;
|
||||
deltas.y *= 8.0f;
|
||||
deltas /= tex_size;
|
||||
deltas *= signs;
|
||||
tc = tc + deltas;
|
||||
} else {
|
||||
float temp_y = floor(coord_in_pixels.y) + 0.5f;
|
||||
float y_coord = temp_y / tex_size.y;
|
||||
float dy = coord_in_pixels.y - temp_y;
|
||||
scan_weight = crtpi_scan_line(dy, filter_width, u.scanline_weight, u.scanline_gap_brightness, u.enable_multisample != 0);
|
||||
float sign_y = sign(dy);
|
||||
dy = dy * dy;
|
||||
dy = dy * dy;
|
||||
dy *= 8.0f;
|
||||
dy /= tex_size.y;
|
||||
dy *= sign_y;
|
||||
tc = float2(texcoord.x, y_coord + dy);
|
||||
}
|
||||
|
||||
float3 colour = tex.sample(samp, tc).rgb;
|
||||
|
||||
if (u.enable_scanlines != 0) {
|
||||
if (u.enable_gamma != 0) { colour = pow(colour, float3(u.input_gamma)); }
|
||||
colour *= scan_weight * u.bloom_factor;
|
||||
if (u.enable_gamma != 0) { colour = pow(colour, float3(1.0f / u.output_gamma)); }
|
||||
}
|
||||
|
||||
if (u.mask_type == 1) {
|
||||
float wm = fract(in.pos.x * 0.5f);
|
||||
float3 mask = (wm < 0.5f) ? float3(u.mask_brightness, 1.0f, u.mask_brightness)
|
||||
: float3(1.0f, u.mask_brightness, 1.0f);
|
||||
colour *= mask;
|
||||
} else if (u.mask_type == 2) {
|
||||
float wm = fract(in.pos.x * 0.3333333f);
|
||||
float3 mask = float3(u.mask_brightness);
|
||||
if (wm < 0.3333333f) mask.x = 1.0f;
|
||||
else if (wm < 0.6666666f) mask.y = 1.0f;
|
||||
else mask.z = 1.0f;
|
||||
colour *= mask;
|
||||
}
|
||||
|
||||
return float4(colour, 1.0f);
|
||||
}
|
||||
)";
|
||||
// NOLINTEND(readability-identifier-naming)
|
||||
|
||||
#endif // __APPLE__
|
||||
#include "core/rendering/sdl3gpu/msl/crtpi_frag.msl.h"
|
||||
#include "core/rendering/sdl3gpu/msl/postfx_frag.msl.h"
|
||||
#include "core/rendering/sdl3gpu/msl/postfx_vert.msl.h"
|
||||
#endif
|
||||
|
||||
namespace Rendering {
|
||||
|
||||
@@ -374,7 +47,6 @@ namespace Rendering {
|
||||
game_width_ = static_cast<int>(fw);
|
||||
game_height_ = static_cast<int>(fh);
|
||||
uniforms_.screen_height = static_cast<float>(game_height_);
|
||||
uniforms_.oversample = static_cast<float>(oversample_);
|
||||
|
||||
// 1. GPU disabled by config
|
||||
if (preferred_driver_ == "none") {
|
||||
@@ -412,7 +84,7 @@ namespace Rendering {
|
||||
SDL_SetGPUSwapchainParameters(device_, window_, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, bestPresentMode(vsync_));
|
||||
}
|
||||
|
||||
// 3. Create scene texture (always game resolution)
|
||||
// 3. Create scene texture (game resolution)
|
||||
SDL_GPUTextureCreateInfo tex_info = {};
|
||||
tex_info.type = SDL_GPU_TEXTURETYPE_2D;
|
||||
tex_info.format = SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM;
|
||||
@@ -428,9 +100,7 @@ namespace Rendering {
|
||||
return false;
|
||||
}
|
||||
|
||||
ss_factor_ = 0;
|
||||
|
||||
// 4. Create upload transfer buffer (always game resolution)
|
||||
// 4. Create upload transfer buffer (game resolution)
|
||||
SDL_GPUTransferBufferCreateInfo tb_info = {};
|
||||
tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
|
||||
tb_info.size = static_cast<Uint32>(game_width_ * game_height_ * 4);
|
||||
@@ -441,7 +111,7 @@ namespace Rendering {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 5. Create samplers
|
||||
// 5. Create nearest sampler
|
||||
SDL_GPUSamplerCreateInfo samp_info = {};
|
||||
samp_info.min_filter = SDL_GPU_FILTER_NEAREST;
|
||||
samp_info.mag_filter = SDL_GPU_FILTER_NEAREST;
|
||||
@@ -456,20 +126,6 @@ namespace Rendering {
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_GPUSamplerCreateInfo lsamp_info = {};
|
||||
lsamp_info.min_filter = SDL_GPU_FILTER_LINEAR;
|
||||
lsamp_info.mag_filter = SDL_GPU_FILTER_LINEAR;
|
||||
lsamp_info.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST;
|
||||
lsamp_info.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
|
||||
lsamp_info.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
|
||||
lsamp_info.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
|
||||
linear_sampler_ = SDL_CreateGPUSampler(device_, &lsamp_info);
|
||||
if (linear_sampler_ == nullptr) {
|
||||
std::cout << "SDL3GPUShader: failed to create linear sampler: " << SDL_GetError() << '\n';
|
||||
cleanup();
|
||||
return false;
|
||||
}
|
||||
|
||||
// 6. Create pipelines
|
||||
if (!createPipeline()) {
|
||||
cleanup();
|
||||
@@ -486,15 +142,14 @@ namespace Rendering {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// createPipeline — PostFX + Upscale + PostFX offscreen + Downscale pipelines
|
||||
// createPipeline — PostFX → swapchain
|
||||
// ---------------------------------------------------------------------------
|
||||
auto SDL3GPUShader::createPipeline() -> bool { // NOLINT(readability-function-cognitive-complexity)
|
||||
auto SDL3GPUShader::createPipeline() -> bool {
|
||||
const SDL_GPUTextureFormat SWAPCHAIN_FMT = SDL_GetGPUSwapchainTextureFormat(device_, window_);
|
||||
|
||||
// ---- PostFX pipeline (→ swapchain) ----
|
||||
#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);
|
||||
SDL_GPUShader* vert = createShaderMSL(device_, Msl::kPostfxVert, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||
SDL_GPUShader* frag = createShaderMSL(device_, Msl::kPostfxFrag, "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);
|
||||
@@ -534,114 +189,6 @@ namespace Rendering {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- Upscale pipeline (scene → scaled_texture_) ----
|
||||
#ifdef __APPLE__
|
||||
SDL_GPUShader* uvert = createShaderMSL(device_, POSTFX_VERT_MSL, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||
SDL_GPUShader* ufrag = createShaderMSL(device_, UPSCALE_FRAG_MSL, "upscale_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0);
|
||||
#else
|
||||
SDL_GPUShader* uvert = createShaderSPIRV(device_, kpostfx_vert_spv, kpostfx_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||
SDL_GPUShader* ufrag = createShaderSPIRV(device_, kupscale_frag_spv, kupscale_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0);
|
||||
#endif
|
||||
|
||||
if ((uvert == nullptr) || (ufrag == nullptr)) {
|
||||
if (uvert != nullptr) { SDL_ReleaseGPUShader(device_, uvert); }
|
||||
if (ufrag != nullptr) { SDL_ReleaseGPUShader(device_, ufrag); }
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_GPUColorTargetDescription upscale_ct = {};
|
||||
upscale_ct.format = SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM;
|
||||
upscale_ct.blend_state = no_blend;
|
||||
|
||||
SDL_GPUGraphicsPipelineCreateInfo upscale_pi = {};
|
||||
upscale_pi.vertex_shader = uvert;
|
||||
upscale_pi.fragment_shader = ufrag;
|
||||
upscale_pi.vertex_input_state = no_input;
|
||||
upscale_pi.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
|
||||
upscale_pi.target_info.num_color_targets = 1;
|
||||
upscale_pi.target_info.color_target_descriptions = &upscale_ct;
|
||||
|
||||
upscale_pipeline_ = SDL_CreateGPUGraphicsPipeline(device_, &upscale_pi);
|
||||
SDL_ReleaseGPUShader(device_, uvert);
|
||||
SDL_ReleaseGPUShader(device_, ufrag);
|
||||
|
||||
if (upscale_pipeline_ == nullptr) {
|
||||
std::cout << "SDL3GPUShader: upscale pipeline creation failed: " << SDL_GetError() << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- PostFX offscreen pipeline (→ B8G8R8A8 texture, for Lanczos path) ----
|
||||
#ifdef __APPLE__
|
||||
SDL_GPUShader* ofvert = createShaderMSL(device_, POSTFX_VERT_MSL, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||
SDL_GPUShader* offrag = createShaderMSL(device_, POSTFX_FRAG_MSL, "postfx_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||
#else
|
||||
SDL_GPUShader* ofvert = createShaderSPIRV(device_, kpostfx_vert_spv, kpostfx_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||
SDL_GPUShader* offrag = createShaderSPIRV(device_, kpostfx_frag_spv, kpostfx_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||
#endif
|
||||
|
||||
if ((ofvert == nullptr) || (offrag == nullptr)) {
|
||||
if (ofvert != nullptr) { SDL_ReleaseGPUShader(device_, ofvert); }
|
||||
if (offrag != nullptr) { SDL_ReleaseGPUShader(device_, offrag); }
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_GPUColorTargetDescription offscreen_ct = {};
|
||||
offscreen_ct.format = SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM;
|
||||
offscreen_ct.blend_state = no_blend;
|
||||
|
||||
SDL_GPUGraphicsPipelineCreateInfo offscreen_pi = {};
|
||||
offscreen_pi.vertex_shader = ofvert;
|
||||
offscreen_pi.fragment_shader = offrag;
|
||||
offscreen_pi.vertex_input_state = no_input;
|
||||
offscreen_pi.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
|
||||
offscreen_pi.target_info.num_color_targets = 1;
|
||||
offscreen_pi.target_info.color_target_descriptions = &offscreen_ct;
|
||||
|
||||
postfx_offscreen_pipeline_ = SDL_CreateGPUGraphicsPipeline(device_, &offscreen_pi);
|
||||
SDL_ReleaseGPUShader(device_, ofvert);
|
||||
SDL_ReleaseGPUShader(device_, offrag);
|
||||
|
||||
if (postfx_offscreen_pipeline_ == nullptr) {
|
||||
std::cout << "SDL3GPUShader: PostFX offscreen pipeline creation failed: " << SDL_GetError() << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- Downscale pipeline (Lanczos → swapchain) ----
|
||||
#ifdef __APPLE__
|
||||
SDL_GPUShader* dvert = createShaderMSL(device_, POSTFX_VERT_MSL, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||
SDL_GPUShader* dfrag = createShaderMSL(device_, DOWNSCALE_FRAG_MSL, "downscale_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||
#else
|
||||
SDL_GPUShader* dvert = createShaderSPIRV(device_, kpostfx_vert_spv, kpostfx_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||
SDL_GPUShader* dfrag = createShaderSPIRV(device_, kdownscale_frag_spv, kdownscale_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||
#endif
|
||||
|
||||
if ((dvert == nullptr) || (dfrag == nullptr)) {
|
||||
if (dvert != nullptr) { SDL_ReleaseGPUShader(device_, dvert); }
|
||||
if (dfrag != nullptr) { SDL_ReleaseGPUShader(device_, dfrag); }
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_GPUColorTargetDescription downscale_ct = {};
|
||||
downscale_ct.format = SWAPCHAIN_FMT;
|
||||
downscale_ct.blend_state = no_blend;
|
||||
|
||||
SDL_GPUGraphicsPipelineCreateInfo downscale_pi = {};
|
||||
downscale_pi.vertex_shader = dvert;
|
||||
downscale_pi.fragment_shader = dfrag;
|
||||
downscale_pi.vertex_input_state = no_input;
|
||||
downscale_pi.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
|
||||
downscale_pi.target_info.num_color_targets = 1;
|
||||
downscale_pi.target_info.color_target_descriptions = &downscale_ct;
|
||||
|
||||
downscale_pipeline_ = SDL_CreateGPUGraphicsPipeline(device_, &downscale_pi);
|
||||
SDL_ReleaseGPUShader(device_, dvert);
|
||||
SDL_ReleaseGPUShader(device_, dfrag);
|
||||
|
||||
if (downscale_pipeline_ == nullptr) {
|
||||
std::cout << "SDL3GPUShader: downscale pipeline creation failed: " << SDL_GetError() << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -652,8 +199,8 @@ namespace Rendering {
|
||||
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_, CRTPI_FRAG_MSL, "crtpi_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||
SDL_GPUShader* vert = createShaderMSL(device_, Msl::kPostfxVert, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||
SDL_GPUShader* frag = createShaderMSL(device_, Msl::kCrtpiFrag, "crtpi_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_, kcrtpi_frag_spv, kcrtpi_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||
@@ -691,7 +238,7 @@ namespace Rendering {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// uploadPixels — direct memcpy, GPU handles upscale
|
||||
// uploadPixels — direct memcpy
|
||||
// ---------------------------------------------------------------------------
|
||||
void SDL3GPUShader::uploadPixels(const Uint32* pixels, int width, int height) {
|
||||
if (!is_initialized_ || (upload_buffer_ == nullptr)) { return; }
|
||||
@@ -707,24 +254,11 @@ namespace Rendering {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// render — 3-path: CrtPi direct, PostFX+Lanczos, PostFX direct
|
||||
// render — CrtPi direct OR PostFX direct → swapchain
|
||||
// ---------------------------------------------------------------------------
|
||||
void SDL3GPUShader::render() { // NOLINT(readability-function-cognitive-complexity)
|
||||
void SDL3GPUShader::render() {
|
||||
if (!is_initialized_) { return; }
|
||||
|
||||
// SS factor calculation
|
||||
if (oversample_ > 1 && game_height_ > 0) {
|
||||
int win_w = 0;
|
||||
int win_h = 0;
|
||||
SDL_GetWindowSizeInPixels(window_, &win_w, &win_h);
|
||||
const float ZOOM = static_cast<float>(win_h) / static_cast<float>(game_height_);
|
||||
const int NEED_FACTOR = calcSsFactor(ZOOM);
|
||||
if (NEED_FACTOR != ss_factor_) {
|
||||
SDL_WaitForGPUIdle(device_);
|
||||
recreateScaledTexture(NEED_FACTOR);
|
||||
}
|
||||
}
|
||||
|
||||
SDL_GPUCommandBuffer* cmd = SDL_AcquireGPUCommandBuffer(device_);
|
||||
if (cmd == nullptr) {
|
||||
std::cout << "SDL3GPUShader: SDL_AcquireGPUCommandBuffer failed: " << SDL_GetError() << '\n';
|
||||
@@ -750,25 +284,6 @@ namespace Rendering {
|
||||
SDL_EndGPUCopyPass(copy);
|
||||
}
|
||||
|
||||
// ---- Upscale pass: scene → scaled_texture_ ----
|
||||
if (oversample_ > 1 && scaled_texture_ != nullptr && upscale_pipeline_ != nullptr) {
|
||||
SDL_GPUColorTargetInfo upscale_target = {};
|
||||
upscale_target.texture = scaled_texture_;
|
||||
upscale_target.load_op = SDL_GPU_LOADOP_DONT_CARE;
|
||||
upscale_target.store_op = SDL_GPU_STOREOP_STORE;
|
||||
|
||||
SDL_GPURenderPass* upass = SDL_BeginGPURenderPass(cmd, &upscale_target, 1, nullptr);
|
||||
if (upass != nullptr) {
|
||||
SDL_BindGPUGraphicsPipeline(upass, upscale_pipeline_);
|
||||
SDL_GPUTextureSamplerBinding ubinding = {};
|
||||
ubinding.texture = scene_texture_;
|
||||
ubinding.sampler = (linear_upscale_ && linear_sampler_ != nullptr) ? linear_sampler_ : sampler_;
|
||||
SDL_BindGPUFragmentSamplers(upass, 0, &ubinding, 1);
|
||||
SDL_DrawGPUPrimitives(upass, 3, 1, 0, 0);
|
||||
SDL_EndGPURenderPass(upass);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Acquire swapchain texture ----
|
||||
SDL_GPUTexture* swapchain = nullptr;
|
||||
Uint32 sw = 0;
|
||||
@@ -802,117 +317,37 @@ namespace Rendering {
|
||||
vx = std::floor((static_cast<float>(sw) - vw) * 0.5F);
|
||||
vy = std::floor((static_cast<float>(sh) - vh) * 0.5F);
|
||||
|
||||
if (oversample_ > 1 && ss_factor_ > 0) {
|
||||
uniforms_.pixel_scale = static_cast<float>(ss_factor_);
|
||||
} else {
|
||||
uniforms_.pixel_scale = (game_height_ > 0) ? (vh / static_cast<float>(game_height_)) : 1.0F;
|
||||
}
|
||||
uniforms_.pixel_scale = (game_height_ > 0) ? (vh / static_cast<float>(game_height_)) : 1.0F;
|
||||
uniforms_.time = static_cast<float>(SDL_GetTicks()) / 1000.0F;
|
||||
uniforms_.oversample = (oversample_ > 1 && ss_factor_ > 0)
|
||||
? static_cast<float>(ss_factor_)
|
||||
: 1.0F;
|
||||
|
||||
// ---- Path A: CrtPi direct ----
|
||||
if (active_shader_ == ShaderType::CRTPI && crtpi_pipeline_ != nullptr) {
|
||||
SDL_GPUColorTargetInfo ct = {};
|
||||
ct.texture = swapchain;
|
||||
ct.load_op = SDL_GPU_LOADOP_CLEAR;
|
||||
ct.store_op = SDL_GPU_STOREOP_STORE;
|
||||
ct.clear_color = {.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F};
|
||||
SDL_GPUColorTargetInfo ct = {};
|
||||
ct.texture = swapchain;
|
||||
ct.load_op = SDL_GPU_LOADOP_CLEAR;
|
||||
ct.store_op = SDL_GPU_STOREOP_STORE;
|
||||
ct.clear_color = {.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F};
|
||||
|
||||
SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd, &ct, 1, nullptr);
|
||||
if (pass != nullptr) {
|
||||
SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd, &ct, 1, nullptr);
|
||||
if (pass != nullptr) {
|
||||
SDL_GPUViewport vp = {.x = vx, .y = vy, .w = vw, .h = vh, .min_depth = 0.0F, .max_depth = 1.0F};
|
||||
SDL_SetGPUViewport(pass, &vp);
|
||||
|
||||
SDL_GPUTextureSamplerBinding binding = {};
|
||||
binding.texture = scene_texture_;
|
||||
binding.sampler = sampler_;
|
||||
SDL_BindGPUFragmentSamplers(pass, 0, &binding, 1);
|
||||
|
||||
if (active_shader_ == ShaderType::CRTPI && crtpi_pipeline_ != nullptr) {
|
||||
SDL_BindGPUGraphicsPipeline(pass, crtpi_pipeline_);
|
||||
SDL_GPUViewport vp = {.x = vx, .y = vy, .w = vw, .h = vh, .min_depth = 0.0F, .max_depth = 1.0F};
|
||||
SDL_SetGPUViewport(pass, &vp);
|
||||
|
||||
SDL_GPUTextureSamplerBinding binding = {};
|
||||
binding.texture = scene_texture_;
|
||||
binding.sampler = sampler_;
|
||||
SDL_BindGPUFragmentSamplers(pass, 0, &binding, 1);
|
||||
|
||||
crtpi_uniforms_.texture_width = static_cast<float>(game_width_);
|
||||
crtpi_uniforms_.texture_height = static_cast<float>(game_height_);
|
||||
SDL_PushGPUFragmentUniformData(cmd, 0, &crtpi_uniforms_, sizeof(CrtPiUniforms));
|
||||
|
||||
SDL_DrawGPUPrimitives(pass, 3, 1, 0, 0);
|
||||
SDL_EndGPURenderPass(pass);
|
||||
}
|
||||
|
||||
SDL_SubmitGPUCommandBuffer(cmd);
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- Path B: PostFX + Lanczos (SS active + algo selected) ----
|
||||
const bool USE_LANCZOS = (oversample_ > 1 && downscale_algo_ > 0 && scaled_texture_ != nullptr && postfx_texture_ != nullptr && postfx_offscreen_pipeline_ != nullptr && downscale_pipeline_ != nullptr);
|
||||
|
||||
if (USE_LANCZOS) {
|
||||
// Pass B1: PostFX → postfx_texture_
|
||||
SDL_GPUColorTargetInfo postfx_ct = {};
|
||||
postfx_ct.texture = postfx_texture_;
|
||||
postfx_ct.load_op = SDL_GPU_LOADOP_CLEAR;
|
||||
postfx_ct.store_op = SDL_GPU_STOREOP_STORE;
|
||||
postfx_ct.clear_color = {.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F};
|
||||
|
||||
SDL_GPURenderPass* ppass = SDL_BeginGPURenderPass(cmd, &postfx_ct, 1, nullptr);
|
||||
if (ppass != nullptr) {
|
||||
SDL_BindGPUGraphicsPipeline(ppass, postfx_offscreen_pipeline_);
|
||||
SDL_GPUTextureSamplerBinding pbinding = {};
|
||||
pbinding.texture = scaled_texture_;
|
||||
pbinding.sampler = sampler_;
|
||||
SDL_BindGPUFragmentSamplers(ppass, 0, &pbinding, 1);
|
||||
SDL_PushGPUFragmentUniformData(cmd, 0, &uniforms_, sizeof(PostFXUniforms));
|
||||
SDL_DrawGPUPrimitives(ppass, 3, 1, 0, 0);
|
||||
SDL_EndGPURenderPass(ppass);
|
||||
}
|
||||
|
||||
// Pass B2: Lanczos downscale → swapchain
|
||||
SDL_GPUColorTargetInfo ds_ct = {};
|
||||
ds_ct.texture = swapchain;
|
||||
ds_ct.load_op = SDL_GPU_LOADOP_CLEAR;
|
||||
ds_ct.store_op = SDL_GPU_STOREOP_STORE;
|
||||
ds_ct.clear_color = {.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F};
|
||||
|
||||
SDL_GPURenderPass* dpass = SDL_BeginGPURenderPass(cmd, &ds_ct, 1, nullptr);
|
||||
if (dpass != nullptr) {
|
||||
SDL_BindGPUGraphicsPipeline(dpass, downscale_pipeline_);
|
||||
SDL_GPUViewport vp = {.x = vx, .y = vy, .w = vw, .h = vh, .min_depth = 0.0F, .max_depth = 1.0F};
|
||||
SDL_SetGPUViewport(dpass, &vp);
|
||||
SDL_GPUTextureSamplerBinding dbinding = {};
|
||||
dbinding.texture = postfx_texture_;
|
||||
dbinding.sampler = sampler_;
|
||||
SDL_BindGPUFragmentSamplers(dpass, 0, &dbinding, 1);
|
||||
DownscaleUniforms du = {.algorithm = downscale_algo_ - 1, .pad0 = 0.0F, .pad1 = 0.0F, .pad2 = 0.0F};
|
||||
SDL_PushGPUFragmentUniformData(cmd, 0, &du, sizeof(DownscaleUniforms));
|
||||
SDL_DrawGPUPrimitives(dpass, 3, 1, 0, 0);
|
||||
SDL_EndGPURenderPass(dpass);
|
||||
}
|
||||
} else {
|
||||
// ---- Path C: PostFX direct → swapchain ----
|
||||
SDL_GPUColorTargetInfo ct = {};
|
||||
ct.texture = swapchain;
|
||||
ct.load_op = SDL_GPU_LOADOP_CLEAR;
|
||||
ct.store_op = SDL_GPU_STOREOP_STORE;
|
||||
ct.clear_color = {.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F};
|
||||
|
||||
SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd, &ct, 1, nullptr);
|
||||
if (pass != nullptr) {
|
||||
} else {
|
||||
SDL_BindGPUGraphicsPipeline(pass, pipeline_);
|
||||
SDL_GPUViewport vp = {.x = vx, .y = vy, .w = vw, .h = vh, .min_depth = 0.0F, .max_depth = 1.0F};
|
||||
SDL_SetGPUViewport(pass, &vp);
|
||||
|
||||
SDL_GPUTexture* input_texture = (oversample_ > 1 && scaled_texture_ != nullptr) ? scaled_texture_ : scene_texture_;
|
||||
SDL_GPUSampler* active_sampler = (oversample_ > 1 && linear_sampler_ != nullptr) ? linear_sampler_ : sampler_;
|
||||
|
||||
SDL_GPUTextureSamplerBinding binding = {};
|
||||
binding.texture = input_texture;
|
||||
binding.sampler = active_sampler;
|
||||
SDL_BindGPUFragmentSamplers(pass, 0, &binding, 1);
|
||||
|
||||
SDL_PushGPUFragmentUniformData(cmd, 0, &uniforms_, sizeof(PostFXUniforms));
|
||||
SDL_DrawGPUPrimitives(pass, 3, 1, 0, 0);
|
||||
SDL_EndGPURenderPass(pass);
|
||||
}
|
||||
|
||||
SDL_DrawGPUPrimitives(pass, 3, 1, 0, 0);
|
||||
SDL_EndGPURenderPass(pass);
|
||||
}
|
||||
|
||||
SDL_SubmitGPUCommandBuffer(cmd);
|
||||
@@ -942,13 +377,7 @@ namespace Rendering {
|
||||
|
||||
release_pipeline(pipeline_);
|
||||
release_pipeline(crtpi_pipeline_);
|
||||
release_pipeline(postfx_offscreen_pipeline_);
|
||||
release_pipeline(upscale_pipeline_);
|
||||
release_pipeline(downscale_pipeline_);
|
||||
release_texture(scene_texture_);
|
||||
release_texture(scaled_texture_);
|
||||
release_texture(postfx_texture_);
|
||||
ss_factor_ = 0;
|
||||
if (upload_buffer_ != nullptr) {
|
||||
SDL_ReleaseGPUTransferBuffer(device_, upload_buffer_);
|
||||
upload_buffer_ = nullptr;
|
||||
@@ -957,10 +386,6 @@ namespace Rendering {
|
||||
SDL_ReleaseGPUSampler(device_, sampler_);
|
||||
sampler_ = nullptr;
|
||||
}
|
||||
if (linear_sampler_ != nullptr) {
|
||||
SDL_ReleaseGPUSampler(device_, linear_sampler_);
|
||||
linear_sampler_ = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1004,7 +429,7 @@ namespace Rendering {
|
||||
return shader;
|
||||
}
|
||||
|
||||
auto SDL3GPUShader::createShaderSPIRV(SDL_GPUDevice* device, // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto SDL3GPUShader::createShaderSPIRV(SDL_GPUDevice* device,
|
||||
const uint8_t* spv_code,
|
||||
size_t spv_size,
|
||||
const char* entrypoint,
|
||||
@@ -1028,13 +453,17 @@ namespace Rendering {
|
||||
|
||||
void SDL3GPUShader::setPostFXParams(const PostFXParams& p) {
|
||||
uniforms_.vignette_strength = p.vignette;
|
||||
uniforms_.chroma_strength = p.chroma;
|
||||
uniforms_.chroma_min = p.chroma_min;
|
||||
uniforms_.chroma_max = p.chroma_max;
|
||||
uniforms_.scanline_strength = p.scanlines;
|
||||
uniforms_.mask_strength = p.mask;
|
||||
uniforms_.gamma_strength = p.gamma;
|
||||
uniforms_.curvature = p.curvature;
|
||||
uniforms_.bleeding = p.bleeding;
|
||||
uniforms_.flicker = p.flicker;
|
||||
uniforms_.scan_dark_ratio = p.scan_dark_ratio;
|
||||
uniforms_.scan_dark_floor = p.scan_dark_floor;
|
||||
uniforms_.scan_edge_soft = p.scan_edge_soft;
|
||||
}
|
||||
|
||||
void SDL3GPUShader::setCrtPiParams(const CrtPiParams& p) {
|
||||
@@ -1080,28 +509,6 @@ namespace Rendering {
|
||||
integer_scale_ = integer_scale;
|
||||
}
|
||||
|
||||
void SDL3GPUShader::setOversample(int factor) {
|
||||
const int NEW_FACTOR = std::max(1, factor);
|
||||
if (NEW_FACTOR == oversample_) { return; }
|
||||
oversample_ = NEW_FACTOR;
|
||||
if (is_initialized_) {
|
||||
reinitTexturesAndBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
void SDL3GPUShader::setLinearUpscale(bool linear) {
|
||||
linear_upscale_ = linear;
|
||||
}
|
||||
|
||||
void SDL3GPUShader::setDownscaleAlgo(int algo) {
|
||||
downscale_algo_ = std::max(0, std::min(algo, 2));
|
||||
}
|
||||
|
||||
auto SDL3GPUShader::getSsTextureSize() const -> std::pair<int, int> {
|
||||
if (ss_factor_ <= 1) { return {0, 0}; }
|
||||
return {game_width_ * ss_factor_, game_height_ * ss_factor_};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// reinitTexturesAndBuffer
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1113,18 +520,12 @@ namespace Rendering {
|
||||
SDL_ReleaseGPUTexture(device_, scene_texture_);
|
||||
scene_texture_ = nullptr;
|
||||
}
|
||||
if (scaled_texture_ != nullptr) {
|
||||
SDL_ReleaseGPUTexture(device_, scaled_texture_);
|
||||
scaled_texture_ = nullptr;
|
||||
}
|
||||
ss_factor_ = 0;
|
||||
if (upload_buffer_ != nullptr) {
|
||||
SDL_ReleaseGPUTransferBuffer(device_, upload_buffer_);
|
||||
upload_buffer_ = nullptr;
|
||||
}
|
||||
|
||||
uniforms_.screen_height = static_cast<float>(game_height_);
|
||||
uniforms_.oversample = static_cast<float>(oversample_);
|
||||
|
||||
SDL_GPUTextureCreateInfo tex_info = {};
|
||||
tex_info.type = SDL_GPU_TEXTURETYPE_2D;
|
||||
@@ -1151,55 +552,7 @@ namespace Rendering {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::cout << "SDL3GPUShader: reinit — scene " << game_width_ << "x" << game_height_ << ", SS " << (oversample_ > 1 ? "on" : "off") << '\n';
|
||||
return true;
|
||||
}
|
||||
|
||||
auto SDL3GPUShader::calcSsFactor(float zoom) -> int {
|
||||
const int MULTIPLE = 3;
|
||||
const int N = static_cast<int>(std::ceil(zoom / static_cast<float>(MULTIPLE)));
|
||||
return std::max(1, N) * MULTIPLE;
|
||||
}
|
||||
|
||||
auto SDL3GPUShader::recreateScaledTexture(int factor) -> bool {
|
||||
if (scaled_texture_ != nullptr) {
|
||||
SDL_ReleaseGPUTexture(device_, scaled_texture_);
|
||||
scaled_texture_ = nullptr;
|
||||
}
|
||||
if (postfx_texture_ != nullptr) {
|
||||
SDL_ReleaseGPUTexture(device_, postfx_texture_);
|
||||
postfx_texture_ = nullptr;
|
||||
}
|
||||
ss_factor_ = 0;
|
||||
|
||||
const int W = game_width_ * factor;
|
||||
const int H = game_height_ * factor;
|
||||
|
||||
SDL_GPUTextureCreateInfo info = {};
|
||||
info.type = SDL_GPU_TEXTURETYPE_2D;
|
||||
info.format = SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM;
|
||||
info.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER | SDL_GPU_TEXTUREUSAGE_COLOR_TARGET;
|
||||
info.width = static_cast<Uint32>(W);
|
||||
info.height = static_cast<Uint32>(H);
|
||||
info.layer_count_or_depth = 1;
|
||||
info.num_levels = 1;
|
||||
|
||||
scaled_texture_ = SDL_CreateGPUTexture(device_, &info);
|
||||
if (scaled_texture_ == nullptr) {
|
||||
std::cout << "SDL3GPUShader: failed to create scaled texture " << W << "x" << H << ": " << SDL_GetError() << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
postfx_texture_ = SDL_CreateGPUTexture(device_, &info);
|
||||
if (postfx_texture_ == nullptr) {
|
||||
std::cout << "SDL3GPUShader: failed to create postfx texture " << W << "x" << H << ": " << SDL_GetError() << '\n';
|
||||
SDL_ReleaseGPUTexture(device_, scaled_texture_);
|
||||
scaled_texture_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
ss_factor_ = factor;
|
||||
std::cout << "SDL3GPUShader: scaled+postfx textures " << W << "x" << H << " (factor " << factor << "x)" << '\n';
|
||||
std::cout << "SDL3GPUShader: reinit — scene " << game_width_ << "x" << game_height_ << '\n';
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,25 +4,32 @@
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "core/rendering/shader_backend.hpp"
|
||||
|
||||
// PostFX uniforms pushed to fragment stage each frame.
|
||||
// 12 floats = 48 bytes — meets Metal/Vulkan 16-byte alignment requirement.
|
||||
// 16 floats = 64 bytes (4 × vec4) — std140/scalar layout for Vulkan + Metal.
|
||||
struct PostFXUniforms {
|
||||
// vec4 #0
|
||||
float vignette_strength;
|
||||
float chroma_strength;
|
||||
float chroma_min;
|
||||
float scanline_strength;
|
||||
float screen_height;
|
||||
// vec4 #1
|
||||
float mask_strength;
|
||||
float gamma_strength;
|
||||
float curvature;
|
||||
float bleeding;
|
||||
// vec4 #2
|
||||
float pixel_scale;
|
||||
float time;
|
||||
float oversample;
|
||||
float flicker;
|
||||
float chroma_max;
|
||||
// vec4 #3 — paràmetres de scanlines exposats al preset YAML
|
||||
float scan_dark_ratio;
|
||||
float scan_dark_floor;
|
||||
float scan_edge_soft;
|
||||
float pad3;
|
||||
};
|
||||
|
||||
// CrtPi uniforms pushed to fragment stage each frame.
|
||||
@@ -46,22 +53,13 @@ struct CrtPiUniforms {
|
||||
float texture_height;
|
||||
};
|
||||
|
||||
// Downscale uniforms for Lanczos downscale fragment stage.
|
||||
// 1 int + 3 floats = 16 bytes.
|
||||
struct DownscaleUniforms {
|
||||
int algorithm;
|
||||
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)
|
||||
*
|
||||
* Pipeline: Surface pixels (CPU) → SDL_GPUTransferBuffer → SDL_GPUTexture (scene)
|
||||
* → [Upscale →] PostFX/CrtPi render pass → [Lanczos downscale →] swapchain → present
|
||||
* → PostFX/CrtPi render pass → swapchain → present
|
||||
*/
|
||||
class SDL3GPUShader : public ShaderBackend {
|
||||
public:
|
||||
@@ -74,7 +72,7 @@ namespace Rendering {
|
||||
const std::string& fragment_source) -> bool override;
|
||||
|
||||
void render() override;
|
||||
void setTextureSize(float width, float height) override {}
|
||||
void setTextureSize(float /*width*/, float /*height*/) override {}
|
||||
void cleanup() final;
|
||||
void destroy();
|
||||
[[nodiscard]] auto isHardwareAccelerated() const -> bool override { return is_initialized_; }
|
||||
@@ -85,13 +83,6 @@ namespace Rendering {
|
||||
void setPostFXParams(const PostFXParams& p) override;
|
||||
void setVSync(bool vsync) override;
|
||||
void setScaleMode(bool integer_scale) override;
|
||||
void setOversample(int factor) override;
|
||||
|
||||
void setLinearUpscale(bool linear) override;
|
||||
[[nodiscard]] auto isLinearUpscale() const -> bool override { return linear_upscale_; }
|
||||
void setDownscaleAlgo(int algo) override;
|
||||
[[nodiscard]] auto getDownscaleAlgo() const -> int override { return downscale_algo_; }
|
||||
[[nodiscard]] auto getSsTextureSize() const -> std::pair<int, int> override;
|
||||
|
||||
void setActiveShader(ShaderType type) override;
|
||||
void setCrtPiParams(const CrtPiParams& p) override;
|
||||
@@ -116,39 +107,59 @@ namespace Rendering {
|
||||
auto createPipeline() -> bool;
|
||||
auto createCrtPiPipeline() -> bool;
|
||||
auto reinitTexturesAndBuffer() -> bool;
|
||||
auto recreateScaledTexture(int factor) -> bool;
|
||||
static auto calcSsFactor(float zoom) -> int;
|
||||
[[nodiscard]] auto bestPresentMode(bool vsync) const -> SDL_GPUPresentMode;
|
||||
|
||||
SDL_Window* window_ = nullptr;
|
||||
SDL_GPUDevice* device_ = nullptr;
|
||||
SDL_GPUGraphicsPipeline* pipeline_ = nullptr;
|
||||
SDL_GPUGraphicsPipeline* crtpi_pipeline_ = nullptr;
|
||||
SDL_GPUGraphicsPipeline* postfx_offscreen_pipeline_ = nullptr;
|
||||
SDL_GPUGraphicsPipeline* upscale_pipeline_ = nullptr;
|
||||
SDL_GPUGraphicsPipeline* downscale_pipeline_ = nullptr;
|
||||
SDL_GPUTexture* scene_texture_ = nullptr;
|
||||
SDL_GPUTexture* scaled_texture_ = nullptr;
|
||||
SDL_GPUTexture* postfx_texture_ = nullptr;
|
||||
SDL_GPUTransferBuffer* upload_buffer_ = nullptr;
|
||||
SDL_GPUSampler* sampler_ = nullptr;
|
||||
SDL_GPUSampler* linear_sampler_ = nullptr;
|
||||
|
||||
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};
|
||||
PostFXUniforms uniforms_{
|
||||
.vignette_strength = 0.6F,
|
||||
.chroma_min = 0.15F,
|
||||
.scanline_strength = 0.7F,
|
||||
.screen_height = 192.0F,
|
||||
.mask_strength = 0.0F,
|
||||
.gamma_strength = 0.0F,
|
||||
.curvature = 0.0F,
|
||||
.bleeding = 0.0F,
|
||||
.pixel_scale = 1.0F,
|
||||
.time = 0.0F,
|
||||
.flicker = 0.0F,
|
||||
.chroma_max = 0.15F,
|
||||
.scan_dark_ratio = 0.333F,
|
||||
.scan_dark_floor = 0.42F,
|
||||
.scan_edge_soft = 1.0F,
|
||||
.pad3 = 0.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,
|
||||
.enable_curvature = 0,
|
||||
.enable_sharper = 0,
|
||||
.texture_width = 0.0F,
|
||||
.texture_height = 0.0F};
|
||||
ShaderType active_shader_ = ShaderType::POSTFX;
|
||||
|
||||
int game_width_ = 0;
|
||||
int game_height_ = 0;
|
||||
int ss_factor_ = 0;
|
||||
int oversample_ = 1;
|
||||
int downscale_algo_ = 1;
|
||||
std::string driver_name_;
|
||||
std::string preferred_driver_;
|
||||
bool is_initialized_ = false;
|
||||
bool vsync_ = true;
|
||||
bool integer_scale_ = false;
|
||||
bool linear_upscale_ = false;
|
||||
};
|
||||
|
||||
} // namespace Rendering
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,634 +0,0 @@
|
||||
#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;
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace Rendering {
|
||||
|
||||
@@ -18,12 +17,19 @@ namespace Rendering {
|
||||
struct PostFXParams {
|
||||
float vignette = 0.0F;
|
||||
float scanlines = 0.0F;
|
||||
float chroma = 0.0F;
|
||||
// Aberració cromàtica — varia entre min i max via sinusoidal; si coincideixen
|
||||
// queda estàtica. Permet un "floor" perquè la imatge mai sigui lliure de chroma.
|
||||
float chroma_min = 0.0F;
|
||||
float chroma_max = 0.0F;
|
||||
float mask = 0.0F;
|
||||
float gamma = 0.0F;
|
||||
float curvature = 0.0F;
|
||||
float bleeding = 0.0F;
|
||||
float flicker = 0.0F;
|
||||
// Forma de les scanlines — 3 subpíxels per fila lògica per defecte.
|
||||
float scan_dark_ratio{0.333F};
|
||||
float scan_dark_floor{0.42F};
|
||||
float scan_edge_soft{1.0F};
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -66,15 +72,6 @@ namespace Rendering {
|
||||
virtual void setPostFXParams(const PostFXParams& /*p*/) {}
|
||||
virtual void setVSync(bool /*vsync*/) {}
|
||||
virtual void setScaleMode(bool /*integer_scale*/) {}
|
||||
virtual void setOversample(int /*factor*/) {}
|
||||
|
||||
virtual void setLinearUpscale(bool /*linear*/) {}
|
||||
[[nodiscard]] virtual auto isLinearUpscale() const -> bool { return false; }
|
||||
|
||||
virtual void setDownscaleAlgo(int /*algo*/) {}
|
||||
[[nodiscard]] virtual auto getDownscaleAlgo() const -> int { return 0; }
|
||||
|
||||
[[nodiscard]] virtual auto getSsTextureSize() const -> std::pair<int, int> { return {0, 0}; }
|
||||
|
||||
[[nodiscard]] virtual auto isHardwareAccelerated() const -> bool = 0;
|
||||
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
// Constructor
|
||||
Sprite::Sprite(std::shared_ptr<Texture> texture, float pos_x, float pos_y, float width, float height)
|
||||
: textures_{std::move(texture)},
|
||||
pos_((SDL_FRect){.x = pos_x, .y = pos_y, .w = width, .h = height}),
|
||||
sprite_clip_((SDL_FRect){.x = 0, .y = 0, .w = pos_.w, .h = pos_.h}) {}
|
||||
pos_(SDL_FRect{.x = pos_x, .y = pos_y, .w = width, .h = height}),
|
||||
sprite_clip_(SDL_FRect{.x = 0, .y = 0, .w = pos_.w, .h = pos_.h}) {}
|
||||
|
||||
Sprite::Sprite(std::shared_ptr<Texture> texture, SDL_FRect rect)
|
||||
: textures_{std::move(texture)},
|
||||
pos_(rect),
|
||||
sprite_clip_((SDL_FRect){.x = 0, .y = 0, .w = pos_.w, .h = pos_.h}) {}
|
||||
sprite_clip_(SDL_FRect{.x = 0, .y = 0, .w = pos_.w, .h = pos_.h}) {}
|
||||
|
||||
Sprite::Sprite(std::shared_ptr<Texture> texture)
|
||||
: textures_{std::move(texture)},
|
||||
|
||||
@@ -32,7 +32,7 @@ Text::Text(const std::shared_ptr<Texture>& texture, const std::string& text_file
|
||||
}
|
||||
|
||||
// Crea los objetos
|
||||
sprite_ = std::make_unique<Sprite>(texture, (SDL_FRect){.x = 0, .y = 0, .w = static_cast<float>(box_width_), .h = static_cast<float>(box_height_)});
|
||||
sprite_ = std::make_unique<Sprite>(texture, SDL_FRect{.x = 0, .y = 0, .w = static_cast<float>(box_width_), .h = static_cast<float>(box_height_)});
|
||||
|
||||
// Inicializa variables
|
||||
fixed_width_ = false;
|
||||
@@ -50,7 +50,7 @@ Text::Text(const std::shared_ptr<Texture>& texture, const std::shared_ptr<Text::
|
||||
}
|
||||
|
||||
// Crea los objetos
|
||||
sprite_ = std::make_unique<Sprite>(texture, (SDL_FRect){.x = 0, .y = 0, .w = static_cast<float>(box_width_), .h = static_cast<float>(box_height_)});
|
||||
sprite_ = std::make_unique<Sprite>(texture, SDL_FRect{.x = 0, .y = 0, .w = static_cast<float>(box_width_), .h = static_cast<float>(box_height_)});
|
||||
|
||||
// Inicializa variables
|
||||
fixed_width_ = false;
|
||||
@@ -71,8 +71,8 @@ Text::Text(const std::shared_ptr<Texture>& texture, const std::shared_ptr<Textur
|
||||
}
|
||||
|
||||
// Crea los objetos
|
||||
sprite_ = std::make_unique<Sprite>(texture, (SDL_FRect){.x = 0, .y = 0, .w = static_cast<float>(box_width_), .h = static_cast<float>(box_height_)});
|
||||
white_sprite_ = std::make_unique<Sprite>(white_texture, (SDL_FRect){.x = 0, .y = 0, .w = static_cast<float>(box_width_), .h = static_cast<float>(box_height_)});
|
||||
sprite_ = std::make_unique<Sprite>(texture, SDL_FRect{.x = 0, .y = 0, .w = static_cast<float>(box_width_), .h = static_cast<float>(box_height_)});
|
||||
white_sprite_ = std::make_unique<Sprite>(white_texture, SDL_FRect{.x = 0, .y = 0, .w = static_cast<float>(box_width_), .h = static_cast<float>(box_height_)});
|
||||
|
||||
// Inicializa variables
|
||||
fixed_width_ = false;
|
||||
@@ -90,8 +90,8 @@ Text::Text(const std::shared_ptr<Texture>& texture, const std::shared_ptr<Textur
|
||||
}
|
||||
|
||||
// Crea los objetos
|
||||
sprite_ = std::make_unique<Sprite>(texture, (SDL_FRect){.x = 0, .y = 0, .w = static_cast<float>(box_width_), .h = static_cast<float>(box_height_)});
|
||||
white_sprite_ = std::make_unique<Sprite>(white_texture, (SDL_FRect){.x = 0, .y = 0, .w = static_cast<float>(box_width_), .h = static_cast<float>(box_height_)});
|
||||
sprite_ = std::make_unique<Sprite>(texture, SDL_FRect{.x = 0, .y = 0, .w = static_cast<float>(box_width_), .h = static_cast<float>(box_height_)});
|
||||
white_sprite_ = std::make_unique<Sprite>(white_texture, SDL_FRect{.x = 0, .y = 0, .w = static_cast<float>(box_width_), .h = static_cast<float>(box_height_)});
|
||||
|
||||
// Inicializa variables
|
||||
fixed_width_ = false;
|
||||
@@ -137,7 +137,7 @@ void Text::write2X(int x, int y, const std::string& text, int kerning, int lengt
|
||||
}
|
||||
|
||||
// Escribe el texto en una textura
|
||||
auto Text::writeToTexture(const std::string& text, int zoom, int kerning, int length) -> std::shared_ptr<Texture> {
|
||||
auto Text::writeToTexture(const std::string& text, int zoom, int kerning, int /*length*/) -> std::shared_ptr<Texture> {
|
||||
auto* renderer = Screen::get()->getRenderer();
|
||||
auto texture = std::make_shared<Texture>(renderer);
|
||||
auto width = Text::length(text, kerning) * zoom;
|
||||
@@ -342,7 +342,7 @@ void Text::writeDX(Uint8 flags, int x, int y, const std::string& text, int kerni
|
||||
}
|
||||
|
||||
// Escribe texto a partir de un TextStyle
|
||||
void Text::writeStyle(int x, int y, const std::string& text, const Style& style, int length) {
|
||||
void Text::writeStyle(int x, int y, const std::string& text, const Style& style, int /*length*/) {
|
||||
writeDX(style.flags, x, y, text, style.kerning, style.text_color, style.shadow_distance, style.shadow_color);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include <SDL3/SDL.h> // Para SDL_LogError, SDL_LogCategory, Uint8, SDL_...
|
||||
|
||||
#include <cstdint> // Para uint32_t
|
||||
#include <cstring> // Para memcpy
|
||||
#include <fstream> // Para basic_ifstream, basic_istream, basic_ios
|
||||
#include <iostream> // Para std::cout
|
||||
#include <sstream> // Para basic_istringstream
|
||||
@@ -259,12 +258,7 @@ auto Texture::loadSurface(const std::string& file_path) -> std::shared_ptr<Surfa
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Si el constructor de Surface espera un std::shared_ptr<Uint8[]>:
|
||||
size_t pixel_count = raw_pixels.size();
|
||||
auto pixels = std::shared_ptr<Uint8[]>(new Uint8[pixel_count], std::default_delete<Uint8[]>()); // NOLINT(modernize-avoid-c-arrays)
|
||||
std::memcpy(pixels.get(), raw_pixels.data(), pixel_count);
|
||||
|
||||
auto surface = std::make_shared<Surface>(w, h, pixels);
|
||||
auto surface = std::make_shared<Surface>(w, h, std::move(raw_pixels));
|
||||
|
||||
// Actualizar las dimensiones
|
||||
width_ = w;
|
||||
|
||||
@@ -16,11 +16,11 @@ using Palette = std::array<Uint32, 256>;
|
||||
|
||||
// Definición de Surface para imágenes con paleta
|
||||
struct Surface {
|
||||
std::shared_ptr<Uint8[]> data; // NOLINT(modernize-avoid-c-arrays)
|
||||
std::vector<Uint8> data;
|
||||
Uint16 w, h;
|
||||
|
||||
// Constructor
|
||||
Surface(Uint16 width, Uint16 height, std::shared_ptr<Uint8[]> pixels) // NOLINT(modernize-avoid-c-arrays)
|
||||
Surface(Uint16 width, Uint16 height, std::vector<Uint8> pixels)
|
||||
: data(std::move(pixels)),
|
||||
w(width),
|
||||
h(height) {}
|
||||
|
||||
@@ -50,7 +50,7 @@ TiledBG::~TiledBG() {
|
||||
// Rellena la textura con el contenido
|
||||
void TiledBG::fillTexture() {
|
||||
// Crea los objetos para pintar en la textura de fondo
|
||||
auto tile = std::make_unique<Sprite>(Resource::get()->getTexture("title_bg_tile.png"), (SDL_FRect){.x = 0, .y = 0, .w = TILE_WIDTH, .h = TILE_HEIGHT});
|
||||
auto tile = std::make_unique<Sprite>(Resource::get()->getTexture("title_bg_tile.png"), SDL_FRect{.x = 0, .y = 0, .w = TILE_WIDTH, .h = TILE_HEIGHT});
|
||||
|
||||
// Prepara para dibujar sobre la textura
|
||||
auto* temp = SDL_GetRenderTarget(renderer_);
|
||||
|
||||
@@ -47,27 +47,39 @@ void Asset::add(const std::string& file_path, Type type, bool required, bool abs
|
||||
addToMap(file_path, type, required, absolute);
|
||||
}
|
||||
|
||||
// Carga recursos desde un archivo de configuración con soporte para variables
|
||||
// Carga recursos desde un archivo de configuración en el filesystem
|
||||
void Asset::loadFromFile(const std::string& config_file_path, const std::string& prefix, const std::string& system_folder) {
|
||||
std::ifstream file(config_file_path);
|
||||
if (!file.is_open()) {
|
||||
std::cout << "Error: Cannot open config file: " << config_file_path << '\n';
|
||||
return;
|
||||
}
|
||||
parseConfigStream(file, prefix, system_folder);
|
||||
}
|
||||
|
||||
// Carga recursos desde un buffer en memoria (p.ex. obtenido del pack)
|
||||
void Asset::loadFromBuffer(const std::vector<uint8_t>& buffer, const std::string& prefix, const std::string& system_folder) {
|
||||
if (buffer.empty()) {
|
||||
std::cout << "Error: Asset index buffer is empty" << '\n';
|
||||
return;
|
||||
}
|
||||
std::string content(buffer.begin(), buffer.end());
|
||||
std::istringstream stream(content);
|
||||
parseConfigStream(stream, prefix, system_folder);
|
||||
}
|
||||
|
||||
// Parsea el contenido del índice (formato común a loadFromFile y loadFromBuffer)
|
||||
void Asset::parseConfigStream(std::istream& stream, const std::string& prefix, const std::string& system_folder) {
|
||||
std::string line;
|
||||
int line_number = 0;
|
||||
|
||||
while (std::getline(file, line)) {
|
||||
while (std::getline(stream, line)) {
|
||||
++line_number;
|
||||
|
||||
// Limpiar espacios en blanco al principio y final
|
||||
line.erase(0, line.find_first_not_of(" \t\r"));
|
||||
line.erase(line.find_last_not_of(" \t\r") + 1);
|
||||
|
||||
// DEBUG: mostrar línea leída (opcional, puedes comentar esta línea)
|
||||
// SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Line %d: '%s'", line_number, line.c_str());
|
||||
|
||||
// Ignorar líneas vacías y comentarios
|
||||
if (line.empty() || line[0] == '#' || line[0] == ';') {
|
||||
continue;
|
||||
@@ -114,8 +126,6 @@ void Asset::loadFromFile(const std::string& config_file_path, const std::string&
|
||||
std::cout << "Error parsing line " << line_number << " in config file: " << e.what() << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
file.close();
|
||||
}
|
||||
|
||||
// Devuelve la ruta completa a un fichero (búsqueda O(1))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint> // Para uint8_t
|
||||
#include <iosfwd> // Para istream (forward decl)
|
||||
#include <string> // Para string
|
||||
#include <unordered_map> // Para unordered_map
|
||||
#include <utility> // Para move
|
||||
@@ -32,7 +33,8 @@ class Asset {
|
||||
|
||||
// --- Métodos para la gestión de recursos ---
|
||||
void add(const std::string& file_path, Type type, bool required = true, bool absolute = false);
|
||||
void loadFromFile(const std::string& config_file_path, const std::string& prefix = "", const std::string& system_folder = ""); // Con soporte para variables
|
||||
void loadFromFile(const std::string& config_file_path, const std::string& prefix = "", const std::string& system_folder = ""); // Carga el índice desde el filesystem
|
||||
void loadFromBuffer(const std::vector<uint8_t>& buffer, const std::string& prefix = "", const std::string& system_folder = ""); // Carga el índice desde memoria (p.ex. desde el pack)
|
||||
[[nodiscard]] auto getPath(const std::string& filename) const -> std::string;
|
||||
[[nodiscard]] auto loadData(const std::string& filename) const -> std::vector<uint8_t>; // Carga datos del archivo
|
||||
[[nodiscard]] auto check() const -> bool;
|
||||
@@ -63,6 +65,7 @@ class Asset {
|
||||
void addToMap(const std::string& file_path, Type type, bool required, bool absolute); // Añade archivo al mapa
|
||||
[[nodiscard]] static auto replaceVariables(const std::string& path, const std::string& prefix, const std::string& system_folder) -> std::string; // Reemplaza variables en la ruta
|
||||
static auto parseOptions(const std::string& options, bool& required, bool& absolute) -> void; // Parsea opciones
|
||||
void parseConfigStream(std::istream& stream, const std::string& prefix, const std::string& system_folder); // Lógica común de parseo del índice
|
||||
|
||||
// --- Constructores y destructor privados (singleton) ---
|
||||
explicit Asset(std::string executable_path) // Constructor privado
|
||||
|
||||
@@ -88,20 +88,11 @@ auto AssetIntegrated::getSystemPath(const std::string& filename) -> std::string
|
||||
}
|
||||
|
||||
auto AssetIntegrated::shouldUseResourcePack(const std::string& filepath) -> bool {
|
||||
// Los archivos que NO van al pack:
|
||||
// - Archivos de config/ (ahora están fuera de data/)
|
||||
// - Archivos con absolute=true en assets.txt
|
||||
// - Archivos de sistema (${SYSTEM_FOLDER})
|
||||
// Els fitxers que NO van al pack:
|
||||
// - Fitxers amb absolute=true a assets.txt
|
||||
// - Fitxers de sistema (${SYSTEM_FOLDER})
|
||||
// - Fitxers al costat del binari (com gamecontrollerdb.txt)
|
||||
|
||||
if (filepath.find("/config/") != std::string::npos ||
|
||||
filepath.starts_with("config/")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filepath.find("/data/") != std::string::npos ||
|
||||
filepath.starts_with("data/")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return filepath.find("/data/") != std::string::npos ||
|
||||
filepath.starts_with("data/");
|
||||
}
|
||||
@@ -59,20 +59,10 @@ namespace ResourceHelper {
|
||||
}
|
||||
|
||||
auto shouldUseResourcePack(const std::string& filepath) -> bool {
|
||||
// Archivos que NO van al pack:
|
||||
// - config/ (ahora está fuera de data/)
|
||||
// - archivos absolutos del sistema
|
||||
|
||||
if (filepath.find("config/") != std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Si contiene "data/" es candidato para el pack
|
||||
if (filepath.find("data/") != std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
// Si la ruta conté "data/" és candidat al pack. La resta (paths absoluts del
|
||||
// SYSTEM_FOLDER o relatives al binari com gamecontrollerdb.txt) van sempre al
|
||||
// filesystem.
|
||||
return filepath.find("data/") != std::string::npos;
|
||||
}
|
||||
|
||||
auto getPackPath(const std::string& asset_path) -> std::string {
|
||||
|
||||
@@ -9,9 +9,7 @@
|
||||
|
||||
std::unique_ptr<ResourceLoader> ResourceLoader::instance = nullptr;
|
||||
|
||||
ResourceLoader::ResourceLoader()
|
||||
: resource_pack_(nullptr),
|
||||
fallback_to_files_(true) {}
|
||||
ResourceLoader::ResourceLoader() = default;
|
||||
|
||||
auto ResourceLoader::getInstance() -> ResourceLoader& {
|
||||
if (!instance) {
|
||||
|
||||
@@ -11,9 +11,9 @@ class ResourcePack;
|
||||
class ResourceLoader {
|
||||
private:
|
||||
static std::unique_ptr<ResourceLoader> instance;
|
||||
ResourcePack* resource_pack_;
|
||||
ResourcePack* resource_pack_ = nullptr;
|
||||
std::string pack_path_;
|
||||
bool fallback_to_files_;
|
||||
bool fallback_to_files_ = true;
|
||||
|
||||
ResourceLoader();
|
||||
|
||||
|
||||
@@ -10,8 +10,7 @@
|
||||
|
||||
const std::string ResourcePack::DEFAULT_ENCRYPT_KEY = "CCAE_RESOURCES__2024";
|
||||
|
||||
ResourcePack::ResourcePack()
|
||||
: loaded_(false) {}
|
||||
ResourcePack::ResourcePack() = default;
|
||||
|
||||
ResourcePack::~ResourcePack() {
|
||||
clear();
|
||||
@@ -156,7 +155,7 @@ auto ResourcePack::addFile(const std::string& filename, const std::string& filep
|
||||
auto ResourcePack::addDirectory(const std::string& directory) -> bool {
|
||||
if (!std::filesystem::exists(directory)) {
|
||||
std::cerr << "Error: Directory does not exist: " << directory << '\n';
|
||||
return false; // NOLINT(readability-simplify-boolean-expr)
|
||||
return false;
|
||||
}
|
||||
|
||||
return std::ranges::all_of(std::filesystem::recursive_directory_iterator(directory),
|
||||
|
||||
@@ -17,7 +17,7 @@ class ResourcePack {
|
||||
private:
|
||||
std::unordered_map<std::string, ResourceEntry> resources_;
|
||||
std::vector<uint8_t> data_;
|
||||
bool loaded_;
|
||||
bool loaded_ = false;
|
||||
|
||||
static auto calculateChecksum(const std::vector<uint8_t>& data) -> uint32_t;
|
||||
static void encryptData(std::vector<uint8_t>& data, const std::string& key);
|
||||
|
||||
@@ -177,8 +177,8 @@ namespace Defaults::Player::OutlineColor {
|
||||
|
||||
namespace Defaults::Window {
|
||||
constexpr const char* CAPTION = "© 2025 Coffee Crisis Arcade Edition — JailDesigner";
|
||||
constexpr int ZOOM = 2;
|
||||
constexpr int MAX_ZOOM = 2;
|
||||
constexpr int ZOOM = 3;
|
||||
constexpr int MAX_ZOOM = 3;
|
||||
} // namespace Defaults::Window
|
||||
|
||||
namespace Defaults::Video {
|
||||
@@ -188,9 +188,6 @@ namespace Defaults::Video {
|
||||
constexpr bool INTEGER_SCALE = true;
|
||||
constexpr bool GPU_ACCELERATION = true;
|
||||
constexpr bool SHADER_ENABLED = false;
|
||||
constexpr bool SUPERSAMPLING = false;
|
||||
constexpr bool LINEAR_UPSCALE = false;
|
||||
constexpr int DOWNSCALE_ALGO = 1;
|
||||
} // namespace Defaults::Video
|
||||
|
||||
namespace Defaults::Music {
|
||||
@@ -211,7 +208,7 @@ namespace Defaults::Audio {
|
||||
namespace Defaults::Settings {
|
||||
constexpr bool AUTOFIRE = true;
|
||||
constexpr bool SHUTDOWN_ENABLED = false;
|
||||
constexpr const char* PARAMS_FILE = "param_320x256.txt";
|
||||
constexpr const char* PARAMS_PRESET = "arcade"; // Identificador intern del preset (correspon a data/config/<preset>.txt)
|
||||
} // namespace Defaults::Settings
|
||||
|
||||
namespace Defaults::Loading {
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
#include <SDL3/SDL.h> // Para SDL_SetLogPriority, SDL_LogCategory, SDL_LogPriority, SDL_Quit
|
||||
|
||||
#include <cerrno> // Para errno
|
||||
#include <cstdlib> // Para srand, exit, rand, EXIT_FAILURE
|
||||
#include <cstring> // Para std::strerror
|
||||
#include <ctime> // Para time
|
||||
#include <fstream> // Para ifstream, ofstream
|
||||
#include <iostream> // Para basic_ostream, operator<<, cerr
|
||||
@@ -11,6 +13,12 @@
|
||||
#include <stdexcept> // Para runtime_error
|
||||
#include <string> // Para allocator, basic_string, char_traits, operator+, string, operator==
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <process.h> // Per _execv
|
||||
#else
|
||||
#include <unistd.h> // Per execv
|
||||
#endif
|
||||
|
||||
#include "core/audio/audio.hpp" // Para Audio
|
||||
#include "core/input/input.hpp" // Para Input
|
||||
#include "core/locale/lang.hpp" // Para setLanguage
|
||||
@@ -71,7 +79,8 @@ namespace {
|
||||
} // namespace
|
||||
|
||||
// Constructor
|
||||
Director::Director() {
|
||||
Director::Director(int /*argc*/, char** argv)
|
||||
: argv_(argv) {
|
||||
Section::attract_mode = Section::AttractMode::TITLE_TO_DEMO;
|
||||
|
||||
// Establece el nivel de prioridad de la categoría de registro
|
||||
@@ -141,7 +150,9 @@ void Director::init() {
|
||||
|
||||
loadAssets(); // Crea el índice de archivos
|
||||
|
||||
Input::init(Asset::get()->getPath("gamecontrollerdb.txt"), Asset::get()->getPath("controllers.json")); // Carga configuración de controles
|
||||
// gamecontrollerdb.txt no pot anar al pack (SDL_AddGamepadMappingsFromFile només llegeix del FS).
|
||||
// Sempre viu al costat del binari, fora del índex d'assets.
|
||||
Input::init(executable_path_ + "gamecontrollerdb.txt", Asset::get()->getPath("controllers.json")); // Carga configuración de controles
|
||||
|
||||
Options::setConfigFile(Asset::get()->getPath("config.yaml")); // Establece el fichero de configuración
|
||||
Options::setControllersFile(Asset::get()->getPath("controllers.json")); // Establece el fichero de configuración de mandos
|
||||
@@ -212,8 +223,8 @@ void Director::finishBoot() {
|
||||
}
|
||||
}
|
||||
|
||||
// Cierra todo y libera recursos del sistema y de los singletons
|
||||
void Director::close() {
|
||||
// Allibera tots els singletons i SDL (compartit entre close() i relaunch())
|
||||
void Director::shutdownSubsystems() {
|
||||
// Guarda las opciones actuales en el archivo de configuración
|
||||
Options::saveToFile();
|
||||
|
||||
@@ -226,22 +237,47 @@ void Director::close() {
|
||||
Screen::destroy(); // Libera el sistema de pantalla y renderizado
|
||||
Asset::destroy(); // Libera el gestor de archivos
|
||||
|
||||
std::cout << "\nBye!\n";
|
||||
|
||||
// Libera todos los recursos de SDL
|
||||
SDL_Quit();
|
||||
}
|
||||
|
||||
// Cierra todo y libera recursos del sistema y de los singletons
|
||||
void Director::close() {
|
||||
shutdownSubsystems();
|
||||
std::cout << "\nBye!\n";
|
||||
// Apaga el sistema
|
||||
shutdownSystem(Section::options == Section::Options::SHUTDOWN);
|
||||
}
|
||||
|
||||
// Reemplaça el procés actual per ell mateix (reinici en calent). En cas d'èxit no torna.
|
||||
// Si no es pot reiniciar (Emscripten, argv invàlid), retorna i el caller fa el reset clàssic.
|
||||
void Director::relaunch() const {
|
||||
#ifdef __EMSCRIPTEN__
|
||||
// Al navegador el reinici real seria location.reload(); aquí caiem al reset intern.
|
||||
return;
|
||||
#else
|
||||
if (argv_ == nullptr || argv_[0] == nullptr) { return; }
|
||||
std::cout << "Relaunching " << argv_[0] << "...\n";
|
||||
shutdownSubsystems();
|
||||
#ifdef _WIN32
|
||||
_execv(argv_[0], argv_);
|
||||
#else
|
||||
execv(argv_[0], argv_);
|
||||
#endif
|
||||
// Si arribem aquí, execv ha fallat. Tots els subsistemes ja estan destruïts: no
|
||||
// podem reprendre el bucle. Sortim amb error.
|
||||
std::cerr << "Relaunch failed: " << std::strerror(errno) << "\n";
|
||||
std::exit(EXIT_FAILURE);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Carga los parametros
|
||||
void Director::loadParams() {
|
||||
// Carga los parametros para configurar el juego
|
||||
#ifdef ANBERNIC
|
||||
const std::string PARAM_FILE_PATH = Asset::get()->getPath("param_320x240.txt");
|
||||
const std::string PARAM_FILE_PATH = Asset::get()->getPath("classic.txt");
|
||||
#else
|
||||
const std::string PARAM_FILE_PATH = Asset::get()->getPath(Options::settings.params_file);
|
||||
const std::string PARAM_FILE_PATH = Asset::get()->getPath(Options::settings.params_preset + ".txt");
|
||||
#endif
|
||||
loadParamsFromFile(PARAM_FILE_PATH);
|
||||
}
|
||||
@@ -256,7 +292,7 @@ void Director::loadScoreFile() {
|
||||
#endif
|
||||
}
|
||||
|
||||
// Carga el indice de ficheros desde un fichero
|
||||
// Carga el indice de ficheros desde el pack (o filesystem como fallback)
|
||||
void Director::loadAssets() {
|
||||
#ifdef MACOS_BUNDLE
|
||||
const std::string PREFIX = "/../Resources";
|
||||
@@ -264,9 +300,13 @@ void Director::loadAssets() {
|
||||
const std::string PREFIX;
|
||||
#endif
|
||||
|
||||
// Cargar la configuración de assets (también aplicar el prefijo al archivo de configuración)
|
||||
std::string config_path = executable_path_ + PREFIX + "/config/assets.txt";
|
||||
Asset::get()->loadFromFile(config_path, PREFIX, system_folder_);
|
||||
// El índice ahora vive dins el pack a "config/assets.txt" (ResourceHelper li trau el "data/" prefix).
|
||||
// ResourceHelper::loadFile fa fallback automàtic al filesystem si el pack no està o no conté el fitxer.
|
||||
auto buffer = ResourceHelper::loadFile("/data/config/assets.txt");
|
||||
if (buffer.empty()) {
|
||||
throw std::runtime_error("No s'ha pogut carregar l'índex d'assets (data/config/assets.txt)");
|
||||
}
|
||||
Asset::get()->loadFromBuffer(buffer, PREFIX, system_folder_);
|
||||
|
||||
// Si falta algun fichero, sale del programa
|
||||
if (!Asset::get()->check()) {
|
||||
@@ -348,9 +388,11 @@ void Director::resetActiveSection() {
|
||||
|
||||
// Destruye la sección anterior y construye la nueva cuando Section::name cambia
|
||||
void Director::handleSectionTransition() {
|
||||
// RESET: recarga recursos y vuelve a LOGO (el propio reset() cambia Section::name)
|
||||
// RESET: intenta reinici real via execv; si no es pot (Emscripten o argv invàlid),
|
||||
// cau al reset intern (recarrega recursos i torna a LOGO, sense recrear Screen/Params).
|
||||
if (Section::name == Section::Name::RESET) {
|
||||
resetActiveSection(); // libera recursos actuales antes del reload
|
||||
relaunch(); // En èxit no torna; el binari es reemplaça
|
||||
resetActiveSection(); // Fallback: libera recursos actuales antes del reload
|
||||
reset();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
|
||||
#include <SDL3/SDL.h> // Para SDL_AppResult, SDL_Event
|
||||
|
||||
#include <memory> // Para unique_ptr
|
||||
#include <string> // Para string
|
||||
#include <cstdint> // Para std::uint8_t
|
||||
#include <memory> // Para unique_ptr
|
||||
#include <string> // Para string
|
||||
|
||||
#include "core/system/section.hpp" // Para Section::Name
|
||||
|
||||
#include <cstdint> // Para std::uint8_t
|
||||
|
||||
namespace Lang {
|
||||
enum class Code : std::uint8_t;
|
||||
}
|
||||
@@ -27,7 +26,7 @@ class Credits;
|
||||
class Director {
|
||||
public:
|
||||
// --- Constructor y destructor ---
|
||||
Director();
|
||||
Director(int argc, char** argv);
|
||||
~Director();
|
||||
|
||||
// --- Callbacks para SDL_MAIN_USE_CALLBACKS ---
|
||||
@@ -55,6 +54,7 @@ class Director {
|
||||
// --- Variables internas ---
|
||||
std::string executable_path_; // Ruta del ejecutable
|
||||
std::string system_folder_; // Carpeta del sistema para almacenar datos
|
||||
char** argv_ = nullptr; // argv original; usat per relaunch() (execv)
|
||||
|
||||
// --- Sección activa (una y sólo una viva en cada momento) ---
|
||||
std::unique_ptr<Preload> preload_;
|
||||
@@ -71,9 +71,11 @@ class Director {
|
||||
bool boot_loading_ = true; // True mientras Resource::loadStep está cargando incremental
|
||||
|
||||
// --- Inicialización y cierre del sistema ---
|
||||
void init(); // Inicializa la aplicación (pre-boot)
|
||||
static void finishBoot(); // Post-boot: inicializa lo que depende de recursos cargados
|
||||
static void close(); // Cierra y libera recursos
|
||||
void init(); // Inicializa la aplicación (pre-boot)
|
||||
static void finishBoot(); // Post-boot: inicializa lo que depende de recursos cargados
|
||||
static void shutdownSubsystems(); // Allibera singletons i SDL (sense apagar el sistema)
|
||||
static void close(); // Cierra y libera recursos
|
||||
void relaunch() const; // Reemplaça el procés via execv (fallback silenciós si no es pot)
|
||||
|
||||
// --- Configuración inicial ---
|
||||
static void loadParams(); // Carga los parámetros del programa
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace SystemShutdown {
|
||||
return shutdownSystem(config);
|
||||
}
|
||||
|
||||
auto shutdownSystem(const ShutdownConfig& config) -> ShutdownResult {
|
||||
auto shutdownSystem([[maybe_unused]] const ShutdownConfig& config) -> ShutdownResult {
|
||||
#ifdef _WIN32
|
||||
// Windows: Usar CreateProcess
|
||||
STARTUPINFOA si = {0};
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace SystemUtils {
|
||||
|
||||
// Función auxiliar para crear una carpeta individual
|
||||
auto createSingleFolder(const std::string& path, int permissions) -> Result {
|
||||
struct stat st = {.st_dev = 0};
|
||||
struct stat st{};
|
||||
|
||||
// Verificar si ya existe
|
||||
if (stat(path.c_str(), &st) == 0) {
|
||||
@@ -131,7 +131,7 @@ namespace SystemUtils {
|
||||
}
|
||||
|
||||
auto folderExists(const std::string& path) -> bool {
|
||||
struct stat st = {.st_dev = 0};
|
||||
struct stat st{};
|
||||
return (stat(path.c_str(), &st) == 0 && S_ISDIR(st.st_mode));
|
||||
}
|
||||
|
||||
|
||||
@@ -890,7 +890,7 @@ void Player::shiftColliders() {
|
||||
}
|
||||
|
||||
// Pone las texturas del jugador
|
||||
void Player::setPlayerTextures(const std::vector<std::shared_ptr<Texture>>& texture) { // NOLINT(readability-named-parameter)
|
||||
void Player::setPlayerTextures(const std::vector<std::shared_ptr<Texture>>& texture) {
|
||||
player_sprite_->setTexture(texture[0]);
|
||||
power_sprite_->setTexture(texture[1]);
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ class Player {
|
||||
void setAnimation(float delta_time); // Establece la animación según el estado (time-based)
|
||||
|
||||
// --- Texturas y animaciones ---
|
||||
void setPlayerTextures(const std::vector<std::shared_ptr<Texture>>& texture); // NOLINT(readability-avoid-const-params-in-decls) Cambia las texturas del jugador
|
||||
void setPlayerTextures(const std::vector<std::shared_ptr<Texture>>& texture);
|
||||
|
||||
// --- Gameplay: Puntuación y power-ups ---
|
||||
void addScore(int score, int lowest_hi_score_entry); // Añade puntos
|
||||
|
||||
@@ -5,17 +5,17 @@
|
||||
#include <cctype> // Para isdigit
|
||||
#include <cstddef> // Para size_t
|
||||
#include <exception> // Para exception
|
||||
#include <fstream> // Para basic_istream, basic_ifstream, ifstream, istringstream
|
||||
#include <iterator> // Para reverse_iterator
|
||||
#include <map> // Para map, operator==, _Rb_tree_iterator
|
||||
#include <sstream> // Para basic_istringstream
|
||||
#include <string> // Para string, char_traits, allocator, operator==, stoi, getline, operator<=>, basic_string
|
||||
#include <utility> // Para std::cmp_less
|
||||
|
||||
#include "core/resources/asset.hpp" // Para Asset
|
||||
#include "game/entities/balloon.hpp" // Para Balloon
|
||||
#include "utils/param.hpp" // Para Param, ParamGame, param
|
||||
#include "utils/utils.hpp" // Para Zone, BLOCK
|
||||
#include "core/resources/asset.hpp" // Para Asset
|
||||
#include "core/resources/resource_helper.hpp" // Para ResourceHelper::loadFile
|
||||
#include "game/entities/balloon.hpp" // Para Balloon
|
||||
#include "utils/param.hpp" // Para Param, ParamGame, param
|
||||
#include "utils/utils.hpp" // Para Zone, BLOCK
|
||||
|
||||
void BalloonFormations::initFormations() {
|
||||
// Calcular posiciones base
|
||||
@@ -60,16 +60,19 @@ void BalloonFormations::initFormations() {
|
||||
}
|
||||
|
||||
auto BalloonFormations::loadFormationsFromFile(const std::string& filename, const std::map<std::string, float>& variables) -> bool {
|
||||
std::ifstream file(filename);
|
||||
if (!file.is_open()) {
|
||||
auto buffer = ResourceHelper::loadFile(filename);
|
||||
if (buffer.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string content(buffer.begin(), buffer.end());
|
||||
std::istringstream stream(content);
|
||||
|
||||
std::string line;
|
||||
int current_formation = -1;
|
||||
std::vector<SpawnParams> current_params;
|
||||
|
||||
while (std::getline(file, line)) {
|
||||
while (std::getline(stream, line)) {
|
||||
// Eliminar espacios en blanco al inicio y final
|
||||
line = trim(line);
|
||||
|
||||
@@ -113,7 +116,6 @@ auto BalloonFormations::loadFormationsFromFile(const std::string& filename, cons
|
||||
addTestFormation();
|
||||
#endif
|
||||
|
||||
file.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -272,18 +274,21 @@ void BalloonFormations::initFormationPools() {
|
||||
}
|
||||
|
||||
auto BalloonFormations::loadPoolsFromFile(const std::string& filename) -> bool {
|
||||
std::ifstream file(filename);
|
||||
if (!file.is_open()) {
|
||||
auto buffer = ResourceHelper::loadFile(filename);
|
||||
if (buffer.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string content(buffer.begin(), buffer.end());
|
||||
std::istringstream stream(content);
|
||||
|
||||
std::string line;
|
||||
pools_.clear(); // Limpiar pools existentes
|
||||
|
||||
// Map temporal para ordenar los pools por ID
|
||||
std::map<int, std::vector<int>> temp_pools;
|
||||
|
||||
while (std::getline(file, line)) {
|
||||
while (std::getline(stream, line)) {
|
||||
// Eliminar espacios en blanco al inicio y final
|
||||
line = trim(line);
|
||||
|
||||
@@ -299,8 +304,6 @@ auto BalloonFormations::loadPoolsFromFile(const std::string& filename) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
file.close();
|
||||
|
||||
// Convertir el map ordenado a vector
|
||||
// Redimensionar el vector para el pool con ID más alto
|
||||
if (!temp_pools.empty()) {
|
||||
|
||||
@@ -114,7 +114,9 @@ void BalloonManager::deployRandomFormation(int stage) {
|
||||
.size = balloon.size,
|
||||
.vel_x = balloon.vel_x,
|
||||
.game_tempo = balloon_speed_,
|
||||
.creation_counter = creation_time_enabled_ ? balloon.creation_counter : 0.0F};
|
||||
.creation_counter = creation_time_enabled_ ? balloon.creation_counter : 0.0F,
|
||||
.animation = {},
|
||||
.sound = {}};
|
||||
createBalloon(config);
|
||||
}
|
||||
|
||||
@@ -135,7 +137,9 @@ void BalloonManager::deployFormation(int formation_id) {
|
||||
.size = balloon.size,
|
||||
.vel_x = balloon.vel_x,
|
||||
.game_tempo = balloon_speed_,
|
||||
.creation_counter = balloon.creation_counter};
|
||||
.creation_counter = balloon.creation_counter,
|
||||
.animation = {},
|
||||
.sound = {}};
|
||||
createBalloon(config);
|
||||
}
|
||||
}
|
||||
@@ -151,7 +155,9 @@ void BalloonManager::deployFormation(int formation_id, float y) {
|
||||
.size = balloon.size,
|
||||
.vel_x = balloon.vel_x,
|
||||
.game_tempo = balloon_speed_,
|
||||
.creation_counter = balloon.creation_counter};
|
||||
.creation_counter = balloon.creation_counter,
|
||||
.animation = {},
|
||||
.sound = {}};
|
||||
createBalloon(config);
|
||||
}
|
||||
}
|
||||
@@ -213,7 +219,9 @@ void BalloonManager::createChildBalloon(const std::shared_ptr<Balloon>& parent_b
|
||||
.size = static_cast<Balloon::Size>(static_cast<int>(parent_balloon->getSize()) - 1),
|
||||
.vel_x = direction == "LEFT" ? Balloon::VELX_NEGATIVE : Balloon::VELX_POSITIVE,
|
||||
.game_tempo = balloon_speed_,
|
||||
.creation_counter = 0};
|
||||
.creation_counter = 0,
|
||||
.animation = {},
|
||||
.sound = {}};
|
||||
|
||||
// Crea el globo hijo
|
||||
auto child_balloon = createBalloon(config);
|
||||
@@ -267,6 +275,8 @@ void BalloonManager::createPowerBall() {
|
||||
.texture = balloon_textures_.at(4),
|
||||
.animation = balloon_animations_.at(4),
|
||||
.sound = {
|
||||
.bouncing_file = {},
|
||||
.popping_file = {},
|
||||
.bouncing_enabled = bouncing_sound_enabled_,
|
||||
.poping_enabled = poping_sound_enabled_,
|
||||
.enabled = sound_enabled_}};
|
||||
|
||||
@@ -2,26 +2,21 @@
|
||||
|
||||
#include <algorithm> // Para max, min
|
||||
#include <exception> // Para exception
|
||||
#include <fstream> // Para basic_istream, basic_ifstream, ifstream, stringstream
|
||||
#include <numeric> // Para accumulate
|
||||
#include <sstream> // Para basic_stringstream
|
||||
#include <sstream> // Para basic_stringstream, istringstream
|
||||
#include <utility> // Para move
|
||||
|
||||
#include "core/resources/resource_helper.hpp" // Para ResourceHelper::loadFile
|
||||
|
||||
// Implementación de StageData
|
||||
StageData::StageData(int power_to_complete, int min_menace, int max_menace, std::string name)
|
||||
: status_(StageStatus::LOCKED),
|
||||
name_(std::move(name)),
|
||||
: name_(std::move(name)),
|
||||
power_to_complete_(power_to_complete),
|
||||
min_menace_(min_menace),
|
||||
max_menace_(max_menace) {}
|
||||
|
||||
// Implementación de StageManager
|
||||
StageManager::StageManager()
|
||||
: power_change_callback_(nullptr),
|
||||
power_collection_state_(PowerCollectionState::ENABLED),
|
||||
current_stage_index_(0),
|
||||
current_power_(0),
|
||||
total_power_(0) { initialize(); }
|
||||
StageManager::StageManager() { initialize(); }
|
||||
|
||||
void StageManager::initialize() {
|
||||
stages_.clear();
|
||||
@@ -63,14 +58,17 @@ void StageManager::createDefaultStages() {
|
||||
}
|
||||
|
||||
auto StageManager::loadStagesFromFile(const std::string& filename) -> bool {
|
||||
std::ifstream file(filename);
|
||||
if (!file.is_open()) {
|
||||
return false; // No se pudo abrir el archivo
|
||||
auto buffer = ResourceHelper::loadFile(filename);
|
||||
if (buffer.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string content(buffer.begin(), buffer.end());
|
||||
std::istringstream stream(content);
|
||||
|
||||
std::string line;
|
||||
|
||||
while (std::getline(file, line)) {
|
||||
while (std::getline(stream, line)) {
|
||||
// Ignorar líneas vacías y comentarios (líneas que empiezan con #)
|
||||
if (line.empty() || line[0] == '#') {
|
||||
continue;
|
||||
@@ -116,8 +114,6 @@ auto StageManager::loadStagesFromFile(const std::string& filename) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
file.close();
|
||||
|
||||
// Verificar que se cargó al menos una fase
|
||||
return !stages_.empty();
|
||||
}
|
||||
|
||||
@@ -40,11 +40,11 @@ class StageData {
|
||||
|
||||
private:
|
||||
// --- Variables de estado ---
|
||||
StageStatus status_; // Estado actual de la fase
|
||||
std::string name_; // Nombre de la fase
|
||||
int power_to_complete_; // Poder necesario para completar la fase
|
||||
int min_menace_; // Nivel mínimo de amenaza
|
||||
int max_menace_; // Nivel máximo de amenaza
|
||||
StageStatus status_ = StageStatus::LOCKED; // Estado actual de la fase
|
||||
std::string name_; // Nombre de la fase
|
||||
int power_to_complete_; // Poder necesario para completar la fase
|
||||
int min_menace_; // Nivel mínimo de amenaza
|
||||
int max_menace_; // Nivel máximo de amenaza
|
||||
};
|
||||
|
||||
// --- Clase StageManager: gestor principal del sistema de fases del juego ---
|
||||
@@ -100,12 +100,12 @@ class StageManager : public IStageInfo {
|
||||
|
||||
private:
|
||||
// --- Variables de estado ---
|
||||
std::vector<StageData> stages_; // Lista de todas las fases
|
||||
PowerChangeCallback power_change_callback_; // Callback para notificar cambios de poder
|
||||
PowerCollectionState power_collection_state_; // Estado de recolección de poder
|
||||
size_t current_stage_index_; // Índice de la fase actual
|
||||
int current_power_; // Poder actual en la fase activa
|
||||
int total_power_; // Poder total acumulado en todo el juego
|
||||
std::vector<StageData> stages_; // Lista de todas las fases
|
||||
PowerChangeCallback power_change_callback_; // Callback para notificar cambios de poder
|
||||
PowerCollectionState power_collection_state_ = PowerCollectionState::ENABLED; // Estado de recolección de poder
|
||||
size_t current_stage_index_ = 0; // Índice de la fase actual
|
||||
int current_power_ = 0; // Poder actual en la fase activa
|
||||
int total_power_ = 0; // Poder total acumulado en todo el juego
|
||||
|
||||
// --- Métodos internos ---
|
||||
void createDefaultStages(); // Crea las fases predeterminadas del juego
|
||||
|
||||
+75
-93
@@ -25,14 +25,20 @@ namespace Options {
|
||||
GamepadManager gamepad_manager; // Opciones de mando para cada jugador
|
||||
Keyboard keyboard; // Opciones para el teclado
|
||||
PendingChanges pending_changes; // Opciones que se aplican al cerrar
|
||||
std::vector<PostFXPreset> postfx_presets = {
|
||||
{.name = "CRT", .vignette = 0.15F, .scanlines = 0.7F, .chroma = 0.2F, .mask = 0.5F, .gamma = 0.1F, .curvature = 0.0F, .bleeding = 0.0F, .flicker = 0.0F},
|
||||
{.name = "NTSC", .vignette = 0.4F, .scanlines = 0.5F, .chroma = 0.2F, .mask = 0.3F, .gamma = 0.3F, .curvature = 0.0F, .bleeding = 0.6F, .flicker = 0.0F},
|
||||
{.name = "Curved", .vignette = 0.5F, .scanlines = 0.6F, .chroma = 0.1F, .mask = 0.4F, .gamma = 0.4F, .curvature = 0.8F, .bleeding = 0.0F, .flicker = 0.0F},
|
||||
{.name = "Scanlines", .vignette = 0.0F, .scanlines = 0.8F, .chroma = 0.0F, .mask = 0.0F, .gamma = 0.0F, .curvature = 0.0F, .bleeding = 0.0F, .flicker = 0.0F},
|
||||
{.name = "Subtle", .vignette = 0.3F, .scanlines = 0.4F, .chroma = 0.05F, .mask = 0.0F, .gamma = 0.2F, .curvature = 0.0F, .bleeding = 0.0F, .flicker = 0.0F},
|
||||
{.name = "CRT Live", .vignette = 0.15F, .scanlines = 0.6F, .chroma = 0.3F, .mask = 0.3F, .gamma = 0.1F, .curvature = 0.0F, .bleeding = 0.4F, .flicker = 0.8F},
|
||||
};
|
||||
namespace {
|
||||
auto defaultPostFXPresets() -> std::vector<PostFXPreset> {
|
||||
return {
|
||||
{.name = "CRT", .vignette = 0.6F, .scanlines = 0.7F, .chroma_min = 0.15F, .chroma_max = 0.15F, .mask = 0.6F, .gamma = 0.8F, .curvature = 0.0F, .bleeding = 0.0F, .flicker = 0.0F},
|
||||
{.name = "NTSC", .vignette = 0.4F, .scanlines = 0.5F, .chroma_min = 0.17F, .chroma_max = 0.23F, .mask = 0.4F, .gamma = 0.5F, .curvature = 0.0F, .bleeding = 0.6F, .flicker = 0.0F},
|
||||
{.name = "Curved", .vignette = 0.5F, .scanlines = 0.6F, .chroma_min = 0.10F, .chroma_max = 0.10F, .mask = 0.5F, .gamma = 0.7F, .curvature = 0.8F, .bleeding = 0.0F, .flicker = 0.0F},
|
||||
{.name = "Scanlines", .vignette = 0.0F, .scanlines = 0.8F, .chroma_min = 0.0F, .chroma_max = 0.0F, .mask = 0.0F, .gamma = 0.0F, .curvature = 0.0F, .bleeding = 0.0F, .flicker = 0.0F},
|
||||
{.name = "Subtle", .vignette = 0.3F, .scanlines = 0.4F, .chroma_min = 0.05F, .chroma_max = 0.05F, .mask = 0.0F, .gamma = 0.3F, .curvature = 0.0F, .bleeding = 0.0F, .flicker = 0.0F},
|
||||
{.name = "CRT Live", .vignette = 0.5F, .scanlines = 0.6F, .chroma_min = 0.255F, .chroma_max = 0.365F, .mask = 0.3F, .gamma = 0.4F, .curvature = 0.3F, .bleeding = 0.4F, .flicker = 0.8F},
|
||||
};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::vector<PostFXPreset> postfx_presets = defaultPostFXPresets();
|
||||
std::string postfx_file_path;
|
||||
std::vector<CrtPiPreset> crtpi_presets;
|
||||
std::string crtpi_file_path;
|
||||
@@ -88,12 +94,16 @@ namespace Options {
|
||||
}
|
||||
parseField(p, "vignette", preset.vignette);
|
||||
parseField(p, "scanlines", preset.scanlines);
|
||||
parseField(p, "chroma", preset.chroma);
|
||||
parseField(p, "chroma_min", preset.chroma_min);
|
||||
parseField(p, "chroma_max", preset.chroma_max);
|
||||
parseField(p, "mask", preset.mask);
|
||||
parseField(p, "gamma", preset.gamma);
|
||||
parseField(p, "curvature", preset.curvature);
|
||||
parseField(p, "bleeding", preset.bleeding);
|
||||
parseField(p, "flicker", preset.flicker);
|
||||
parseField(p, "scan_dark_ratio", preset.scan_dark_ratio);
|
||||
parseField(p, "scan_dark_floor", preset.scan_dark_floor);
|
||||
parseField(p, "scan_edge_soft", preset.scan_edge_soft);
|
||||
postfx_presets.push_back(preset);
|
||||
}
|
||||
}
|
||||
@@ -130,6 +140,10 @@ namespace Options {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (postfx_presets.empty()) {
|
||||
postfx_presets = defaultPostFXPresets();
|
||||
}
|
||||
|
||||
std::ofstream file(postfx_file_path);
|
||||
if (!file.is_open()) {
|
||||
std::cout << "Error: " << postfx_file_path << " can't be opened for writing" << '\n';
|
||||
@@ -140,79 +154,35 @@ namespace Options {
|
||||
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 << "# chroma_min/chroma_max: chromatic aberration (RGB color fringing) — intensitat\n";
|
||||
file << "# varia entre min i max amb una sinusoidal; si coincideixen, queda estàtica\n";
|
||||
file << "# mask: phosphor dot mask (RGB subpixel pattern)\n";
|
||||
file << "# gamma: gamma correction input 2.4 / output 2.2\n";
|
||||
file << "# curvature: CRT barrel distortion\n";
|
||||
file << "# bleeding: NTSC horizontal colour bleeding\n";
|
||||
file << "# flicker: phosphor CRT flicker ~50 Hz (0.0 = off, 1.0 = max)\n";
|
||||
file << "# scan_dark_ratio: fracció obscura dins de cada fila lògica (1/3 ≈ 0.333)\n";
|
||||
file << "# scan_dark_floor: brillantor mínima de la subfila fosca\n";
|
||||
file << "# scan_edge_soft: 0 = step dur, 1 = transició d'1 píxel físic (estil crtpi)\n";
|
||||
file << "\n";
|
||||
file << "presets:\n";
|
||||
file << " - name: \"CRT\"\n";
|
||||
file << " vignette: 0.15\n";
|
||||
file << " scanlines: 0.7\n";
|
||||
file << " chroma: 0.2\n";
|
||||
file << " mask: 0.5\n";
|
||||
file << " gamma: 0.1\n";
|
||||
file << " curvature: 0.0\n";
|
||||
file << " bleeding: 0.0\n";
|
||||
file << " flicker: 0.0\n";
|
||||
file << " - name: \"NTSC\"\n";
|
||||
file << " vignette: 0.4\n";
|
||||
file << " scanlines: 0.5\n";
|
||||
file << " chroma: 0.2\n";
|
||||
file << " mask: 0.3\n";
|
||||
file << " gamma: 0.3\n";
|
||||
file << " curvature: 0.0\n";
|
||||
file << " bleeding: 0.6\n";
|
||||
file << " flicker: 0.0\n";
|
||||
file << " - name: \"Curved\"\n";
|
||||
file << " vignette: 0.5\n";
|
||||
file << " scanlines: 0.6\n";
|
||||
file << " chroma: 0.1\n";
|
||||
file << " mask: 0.4\n";
|
||||
file << " gamma: 0.4\n";
|
||||
file << " curvature: 0.8\n";
|
||||
file << " bleeding: 0.0\n";
|
||||
file << " flicker: 0.0\n";
|
||||
file << " - name: \"Scanlines\"\n";
|
||||
file << " vignette: 0.0\n";
|
||||
file << " scanlines: 0.8\n";
|
||||
file << " chroma: 0.0\n";
|
||||
file << " mask: 0.0\n";
|
||||
file << " gamma: 0.0\n";
|
||||
file << " curvature: 0.0\n";
|
||||
file << " bleeding: 0.0\n";
|
||||
file << " flicker: 0.0\n";
|
||||
file << " - name: \"Subtle\"\n";
|
||||
file << " vignette: 0.3\n";
|
||||
file << " scanlines: 0.4\n";
|
||||
file << " chroma: 0.05\n";
|
||||
file << " mask: 0.0\n";
|
||||
file << " gamma: 0.2\n";
|
||||
file << " curvature: 0.0\n";
|
||||
file << " bleeding: 0.0\n";
|
||||
file << " flicker: 0.0\n";
|
||||
file << " - name: \"CRT Live\"\n";
|
||||
file << " vignette: 0.15\n";
|
||||
file << " scanlines: 0.6\n";
|
||||
file << " chroma: 0.3\n";
|
||||
file << " mask: 0.3\n";
|
||||
file << " gamma: 0.1\n";
|
||||
file << " curvature: 0.0\n";
|
||||
file << " bleeding: 0.4\n";
|
||||
file << " flicker: 0.8\n";
|
||||
for (const auto& preset : postfx_presets) {
|
||||
file << " - name: \"" << preset.name << "\"\n";
|
||||
file << " vignette: " << preset.vignette << "\n";
|
||||
file << " scanlines: " << preset.scanlines << "\n";
|
||||
file << " chroma_min: " << preset.chroma_min << "\n";
|
||||
file << " chroma_max: " << preset.chroma_max << "\n";
|
||||
file << " mask: " << preset.mask << "\n";
|
||||
file << " gamma: " << preset.gamma << "\n";
|
||||
file << " curvature: " << preset.curvature << "\n";
|
||||
file << " bleeding: " << preset.bleeding << "\n";
|
||||
file << " flicker: " << preset.flicker << "\n";
|
||||
file << " scan_dark_ratio: " << preset.scan_dark_ratio << "\n";
|
||||
file << " scan_dark_floor: " << preset.scan_dark_floor << "\n";
|
||||
file << " scan_edge_soft: " << preset.scan_edge_soft << "\n";
|
||||
}
|
||||
|
||||
file.close();
|
||||
|
||||
// Cargar los presets recién escritos
|
||||
postfx_presets.clear();
|
||||
postfx_presets.push_back({"CRT", 0.15F, 0.7F, 0.2F, 0.5F, 0.1F, 0.0F, 0.0F, 0.0F});
|
||||
postfx_presets.push_back({"NTSC", 0.4F, 0.5F, 0.2F, 0.3F, 0.3F, 0.0F, 0.6F, 0.0F});
|
||||
postfx_presets.push_back({"Curved", 0.5F, 0.6F, 0.1F, 0.4F, 0.4F, 0.8F, 0.0F, 0.0F});
|
||||
postfx_presets.push_back({"Scanlines", 0.0F, 0.8F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F});
|
||||
postfx_presets.push_back({"Subtle", 0.3F, 0.4F, 0.05F, 0.0F, 0.2F, 0.0F, 0.0F, 0.0F});
|
||||
postfx_presets.push_back({"CRT Live", 0.15F, 0.6F, 0.3F, 0.3F, 0.1F, 0.0F, 0.4F, 0.8F});
|
||||
video.shader.current_postfx_preset = 0;
|
||||
|
||||
return true;
|
||||
@@ -333,6 +303,7 @@ namespace Options {
|
||||
// Opciones de cambios pendientes
|
||||
pending_changes.new_language = settings.language;
|
||||
pending_changes.new_difficulty = settings.difficulty;
|
||||
pending_changes.new_params_preset = settings.params_preset;
|
||||
pending_changes.has_pending_changes = false;
|
||||
}
|
||||
|
||||
@@ -375,13 +346,6 @@ namespace Options {
|
||||
video.shader.current_shader = (shader_name == "crtpi") ? Rendering::ShaderType::CRTPI : Rendering::ShaderType::POSTFX;
|
||||
}
|
||||
}
|
||||
|
||||
if (vid.contains("supersampling")) {
|
||||
const auto& ss_node = vid["supersampling"];
|
||||
parseField(ss_node, "enabled", video.supersampling.enabled);
|
||||
parseField(ss_node, "linear_upscale", video.supersampling.linear_upscale);
|
||||
parseField(ss_node, "downscale_algo", video.supersampling.downscale_algo);
|
||||
}
|
||||
}
|
||||
|
||||
void loadAudioFromYaml(const fkyaml::node& yaml) {
|
||||
@@ -423,8 +387,8 @@ namespace Options {
|
||||
parseField(game, "language", lang_int);
|
||||
const auto LANG = static_cast<Lang::Code>(lang_int);
|
||||
settings.language = (LANG == Lang::Code::ENGLISH || LANG == Lang::Code::VALENCIAN || LANG == Lang::Code::SPANISH)
|
||||
? LANG
|
||||
: Lang::Code::ENGLISH;
|
||||
? LANG
|
||||
: Lang::Code::ENGLISH;
|
||||
pending_changes.new_language = settings.language;
|
||||
}
|
||||
if (game.contains("difficulty")) {
|
||||
@@ -435,7 +399,12 @@ namespace Options {
|
||||
}
|
||||
parseField(game, "autofire", settings.autofire);
|
||||
parseField(game, "shutdown_enabled", settings.shutdown_enabled);
|
||||
parseField(game, "params_file", settings.params_file);
|
||||
parseField(game, "params_preset", settings.params_preset);
|
||||
// Validar que el preset llegit existeix; si no, caure al default
|
||||
if (std::ranges::find(PARAMS_PRESETS, settings.params_preset) == PARAMS_PRESETS.end()) {
|
||||
settings.params_preset = Defaults::Settings::PARAMS_PRESET;
|
||||
}
|
||||
pending_changes.new_params_preset = settings.params_preset;
|
||||
}
|
||||
|
||||
void loadControllersFromYaml(const fkyaml::node& yaml) {
|
||||
@@ -558,10 +527,6 @@ namespace Options {
|
||||
file << " postfx_preset: \"" << postfx_name << "\"\n";
|
||||
file << " crtpi_preset: \"" << crtpi_name << "\"\n";
|
||||
}
|
||||
file << " supersampling:\n";
|
||||
file << " enabled: " << boolToString(video.supersampling.enabled) << "\n";
|
||||
file << " linear_upscale: " << boolToString(video.supersampling.linear_upscale) << "\n";
|
||||
file << " downscale_algo: " << video.supersampling.downscale_algo << "\n";
|
||||
file << "\n";
|
||||
|
||||
// AUDIO
|
||||
@@ -592,7 +557,7 @@ namespace Options {
|
||||
file << " difficulty: " << static_cast<int>(settings.difficulty) << " # " << static_cast<int>(Difficulty::Code::EASY) << ": easy, " << static_cast<int>(Difficulty::Code::NORMAL) << ": normal, " << static_cast<int>(Difficulty::Code::HARD) << ": hard\n";
|
||||
file << " autofire: " << boolToString(settings.autofire) << "\n";
|
||||
file << " shutdown_enabled: " << boolToString(settings.shutdown_enabled) << "\n";
|
||||
file << " params_file: " << settings.params_file << "\n";
|
||||
file << " params_preset: " << settings.params_preset << "\n";
|
||||
file << "\n";
|
||||
|
||||
// CONTROLLERS
|
||||
@@ -635,13 +600,30 @@ namespace Options {
|
||||
if (pending_changes.has_pending_changes) {
|
||||
settings.language = pending_changes.new_language;
|
||||
settings.difficulty = pending_changes.new_difficulty;
|
||||
settings.params_preset = pending_changes.new_params_preset;
|
||||
pending_changes.has_pending_changes = false;
|
||||
}
|
||||
}
|
||||
|
||||
void checkPendingChanges() {
|
||||
pending_changes.has_pending_changes = settings.language != pending_changes.new_language ||
|
||||
settings.difficulty != pending_changes.new_difficulty;
|
||||
settings.difficulty != pending_changes.new_difficulty ||
|
||||
settings.params_preset != pending_changes.new_params_preset;
|
||||
}
|
||||
|
||||
// --- Presets de paràmetres ---
|
||||
auto getParamsPresetDisplayName(const std::string& preset_id) -> std::string {
|
||||
if (preset_id == "classic") { return Lang::getText("[SERVICE_MENU] PRESET_CLASSIC"); }
|
||||
if (preset_id == "arcade") { return Lang::getText("[SERVICE_MENU] PRESET_ARCADE"); }
|
||||
if (preset_id == "red") { return Lang::getText("[SERVICE_MENU] PRESET_RED"); }
|
||||
return preset_id;
|
||||
}
|
||||
|
||||
auto getParamsPresetIdFromDisplay(const std::string& display_name) -> std::string {
|
||||
const auto IT = std::ranges::find_if(PARAMS_PRESETS, [&](const std::string& id) {
|
||||
return getParamsPresetDisplayName(id) == display_name;
|
||||
});
|
||||
return IT != PARAMS_PRESETS.end() ? *IT : Defaults::Settings::PARAMS_PRESET;
|
||||
}
|
||||
|
||||
// Buscar y asignar un mando disponible por nombre
|
||||
@@ -697,7 +679,7 @@ namespace Options {
|
||||
// --- PRIMERA PASADA: Intenta asignar mandos basándose en la ruta guardada ---
|
||||
void GamepadManager::assignGamepadsByPath(
|
||||
const std::array<std::string, MAX_PLAYERS>& desired_paths,
|
||||
const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads, // NOLINT(readability-named-parameter)
|
||||
const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads,
|
||||
std::vector<std::shared_ptr<Input::Gamepad>>& assigned_instances) {
|
||||
for (size_t i = 0; i < MAX_PLAYERS; ++i) {
|
||||
const std::string& desired_path = desired_paths[i];
|
||||
@@ -723,7 +705,7 @@ namespace Options {
|
||||
// refrescamos el path guardado al del dispositivo físico actual.
|
||||
void GamepadManager::assignGamepadsByName(
|
||||
const std::array<std::string, MAX_PLAYERS>& desired_names,
|
||||
const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads, // NOLINT(readability-named-parameter)
|
||||
const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads,
|
||||
std::vector<std::shared_ptr<Input::Gamepad>>& assigned_instances) {
|
||||
for (size_t i = 0; i < MAX_PLAYERS; ++i) {
|
||||
if (gamepads_[i].instance != nullptr) {
|
||||
@@ -749,7 +731,7 @@ namespace Options {
|
||||
|
||||
// --- TERCERA PASADA: Asigna los mandos físicos restantes a los jugadores libres ---
|
||||
void GamepadManager::assignRemainingGamepads(
|
||||
const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads, // NOLINT(readability-named-parameter)
|
||||
const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads,
|
||||
std::vector<std::shared_ptr<Input::Gamepad>>& assigned_instances) {
|
||||
for (size_t i = 0; i < MAX_PLAYERS; ++i) {
|
||||
if (gamepads_[i].instance != nullptr) {
|
||||
@@ -781,7 +763,7 @@ namespace Options {
|
||||
|
||||
auto GamepadManager::isGamepadAssigned(
|
||||
const std::shared_ptr<Input::Gamepad>& physical_gamepad,
|
||||
const std::vector<std::shared_ptr<Input::Gamepad>>& assigned_instances) -> bool { // NOLINT(readability-named-parameter)
|
||||
const std::vector<std::shared_ptr<Input::Gamepad>>& assigned_instances) -> bool {
|
||||
return std::ranges::any_of(assigned_instances,
|
||||
[&physical_gamepad](const auto& assigned) -> auto {
|
||||
return assigned == physical_gamepad;
|
||||
|
||||
+28
-19
@@ -29,12 +29,18 @@ namespace Options {
|
||||
std::string name;
|
||||
float vignette{0.6F};
|
||||
float scanlines{0.7F};
|
||||
float chroma{0.15F};
|
||||
float mask{0.0F};
|
||||
float gamma{0.0F};
|
||||
// Aberració cromàtica entre min i max (si coincideixen, estàtica).
|
||||
float chroma_min{0.15F};
|
||||
float chroma_max{0.15F};
|
||||
float mask{0.6F};
|
||||
float gamma{0.8F};
|
||||
float curvature{0.0F};
|
||||
float bleeding{0.0F};
|
||||
float flicker{0.0F};
|
||||
// Forma de les scanlines (3 subpíxels per fila lògica per defecte).
|
||||
float scan_dark_ratio{0.333F};
|
||||
float scan_dark_floor{0.42F};
|
||||
float scan_edge_soft{1.0F};
|
||||
};
|
||||
|
||||
struct CrtPiPreset {
|
||||
@@ -66,12 +72,6 @@ namespace Options {
|
||||
std::string preferred_driver;
|
||||
};
|
||||
|
||||
struct Supersampling {
|
||||
bool enabled{Defaults::Video::SUPERSAMPLING};
|
||||
bool linear_upscale{Defaults::Video::LINEAR_UPSCALE};
|
||||
int downscale_algo{Defaults::Video::DOWNSCALE_ALGO};
|
||||
};
|
||||
|
||||
struct ShaderConfig {
|
||||
bool enabled{Defaults::Video::SHADER_ENABLED};
|
||||
Rendering::ShaderType current_shader{Rendering::ShaderType::POSTFX};
|
||||
@@ -88,7 +88,6 @@ namespace Options {
|
||||
bool integer_scale = Defaults::Video::INTEGER_SCALE;
|
||||
std::string info;
|
||||
GPU gpu{};
|
||||
Supersampling supersampling{};
|
||||
ShaderConfig shader{};
|
||||
};
|
||||
|
||||
@@ -126,7 +125,7 @@ namespace Options {
|
||||
std::vector<int> glowing_entries = {ManageHiScoreTable::NO_ENTRY, ManageHiScoreTable::NO_ENTRY}; // Últimas posiciones de entrada en la tabla
|
||||
std::string config_file; // Ruta al fichero donde guardar la configuración y las opciones del juego
|
||||
std::string controllers_file; // Ruta al fichero con las configuraciones de los mandos
|
||||
std::string params_file = Defaults::Settings::PARAMS_FILE; // Ruta al fichero de parámetros del juego
|
||||
std::string params_preset = Defaults::Settings::PARAMS_PRESET; // Identificador del preset de parámetros (classic/arcade/red)
|
||||
|
||||
// Reinicia las últimas entradas de puntuación
|
||||
void clearLastHiScoreEntries() {
|
||||
@@ -263,7 +262,7 @@ namespace Options {
|
||||
void clearPlayers() { players_.clear(); } // Limpia la lista de jugadores
|
||||
|
||||
// Asigna el mando a un jugador
|
||||
void assignTo(const Input::Gamepad& gamepad, Player::Id player_id) {
|
||||
void assignTo(const Input::Gamepad& /*gamepad*/, Player::Id /*player_id*/) {
|
||||
}
|
||||
|
||||
// Asigna los mandos físicos basándose en la configuración actual de nombres.
|
||||
@@ -297,19 +296,19 @@ namespace Options {
|
||||
|
||||
void assignGamepadsByPath(
|
||||
const std::array<std::string, MAX_PLAYERS>& desired_paths,
|
||||
const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads, // NOLINT(readability-avoid-const-params-in-decls)
|
||||
const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads,
|
||||
std::vector<std::shared_ptr<Input::Gamepad>>& assigned_instances);
|
||||
void assignGamepadsByName(
|
||||
const std::array<std::string, MAX_PLAYERS>& desired_names,
|
||||
const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads, // NOLINT(readability-avoid-const-params-in-decls)
|
||||
const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads,
|
||||
std::vector<std::shared_ptr<Input::Gamepad>>& assigned_instances);
|
||||
void assignRemainingGamepads(
|
||||
const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads, // NOLINT(readability-avoid-const-params-in-decls)
|
||||
const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads,
|
||||
std::vector<std::shared_ptr<Input::Gamepad>>& assigned_instances);
|
||||
void clearUnassignedGamepadSlots();
|
||||
[[nodiscard]] static auto isGamepadAssigned(
|
||||
const std::shared_ptr<Input::Gamepad>& physical_gamepad,
|
||||
const std::vector<std::shared_ptr<Input::Gamepad>>& assigned_instances) -> bool; // NOLINT(readability-avoid-const-params-in-decls)
|
||||
const std::vector<std::shared_ptr<Input::Gamepad>>& assigned_instances) -> bool;
|
||||
};
|
||||
|
||||
struct Keyboard {
|
||||
@@ -329,11 +328,21 @@ namespace Options {
|
||||
};
|
||||
|
||||
struct PendingChanges {
|
||||
Lang::Code new_language = Lang::Code::VALENCIAN; // Idioma en espera de aplicar
|
||||
Difficulty::Code new_difficulty = Difficulty::Code::NORMAL; // Dificultad en espera de aplicar
|
||||
bool has_pending_changes = false; // Indica si hay cambios pendientes
|
||||
Lang::Code new_language = Lang::Code::VALENCIAN; // Idioma en espera de aplicar
|
||||
Difficulty::Code new_difficulty = Difficulty::Code::NORMAL; // Dificultad en espera de aplicar
|
||||
std::string new_params_preset = Defaults::Settings::PARAMS_PRESET; // Preset de parámetros en espera de aplicar (requiere reinicio)
|
||||
bool has_pending_changes = false; // Indica si hay cambios pendientes
|
||||
};
|
||||
|
||||
// Lista d'identificadors de presets de paràmetres disponibles. Cada nom es correspon
|
||||
// amb el .txt al pack (data/config/<id>.txt). L'ordre marca el cicle al service menu.
|
||||
inline const std::vector<std::string> PARAMS_PRESETS = {"classic", "arcade", "red"};
|
||||
|
||||
// Tradueix l'identificador intern a la cadena visible (localitzada).
|
||||
auto getParamsPresetDisplayName(const std::string& preset_id) -> std::string;
|
||||
// Tradueix la cadena visible al seu identificador intern.
|
||||
auto getParamsPresetIdFromDisplay(const std::string& display_name) -> std::string;
|
||||
|
||||
// --- Variables ---
|
||||
extern Window window; // Opciones de la ventana
|
||||
extern Settings settings; // Opciones del juego
|
||||
|
||||
@@ -800,7 +800,7 @@ void Game::renderPathSprites() {
|
||||
}
|
||||
|
||||
// Acciones a realizar cuando el jugador colisiona con un globo
|
||||
void Game::handlePlayerCollision(std::shared_ptr<Player>& player, const std::shared_ptr<Balloon>& balloon) {
|
||||
void Game::handlePlayerCollision(std::shared_ptr<Player>& player, const std::shared_ptr<Balloon>& /*balloon*/) {
|
||||
if (!player->isPlaying() || player->isInvulnerable()) {
|
||||
return; // Si no está jugando o tiene inmunidad, no hace nada
|
||||
}
|
||||
@@ -953,8 +953,8 @@ void Game::fillCanvas() {
|
||||
renderItems();
|
||||
balloon_manager_->render();
|
||||
tabe_->render();
|
||||
bullet_manager_->render();
|
||||
renderPlayers();
|
||||
bullet_manager_->render();
|
||||
|
||||
renderPathSprites();
|
||||
|
||||
@@ -1315,7 +1315,13 @@ void Game::demoHandleInput() {
|
||||
|
||||
// Procesa las entradas para un jugador específico durante el modo demo.
|
||||
void Game::demoHandlePlayerInput(const std::shared_ptr<Player>& player, int index) {
|
||||
const auto& demo_data = demo_.data.at(index).at(demo_.index);
|
||||
// Guarda: si el fade no s'arriba a disparar (frame skip al == TOTAL_DEMO_DATA-200)
|
||||
// i demo_.index passa de 2000, fem loop al primer frame en comptes de petar.
|
||||
const auto& demo_data_vec = demo_.data.at(index);
|
||||
if (demo_data_vec.empty()) {
|
||||
return;
|
||||
}
|
||||
const auto& demo_data = demo_data_vec.at(static_cast<size_t>(demo_.index) % demo_data_vec.size());
|
||||
|
||||
if (demo_data.left == 1) {
|
||||
player->setInput(Input::Action::LEFT);
|
||||
@@ -1341,8 +1347,8 @@ void Game::handleFireInput(const std::shared_ptr<Player>& player, Bullet::Type t
|
||||
switch (type) {
|
||||
case Bullet::Type::UP:
|
||||
player->setInput(Input::Action::FIRE_CENTER);
|
||||
bullet.x = 2 + player->getPosX() + ((Player::WIDTH - Bullet::WIDTH) / 2);
|
||||
bullet.y = player->getPosY() - (Bullet::HEIGHT / 2);
|
||||
bullet.x = 1 + player->getPosX() + ((Player::WIDTH - Bullet::WIDTH) / 2);
|
||||
bullet.y = player->getPosY() - (Bullet::HEIGHT / 2) - 2;
|
||||
break;
|
||||
case Bullet::Type::LEFT:
|
||||
player->setInput(Input::Action::FIRE_LEFT);
|
||||
@@ -1756,8 +1762,13 @@ void Game::updateDemo(float delta_time) {
|
||||
demo_.elapsed_s += delta_time;
|
||||
demo_.index = static_cast<int>(demo_.elapsed_s * 60.0F);
|
||||
|
||||
// Activa el fundido antes de acabar con los datos de la demo
|
||||
if (demo_.index == TOTAL_DEMO_DATA - 200) {
|
||||
// Activa el fundido abans d'acabar amb les dades de la demo.
|
||||
// Cal >= (no ==) perquè un frame lent (p.ex. canviar preset des del
|
||||
// service menu) podria saltar exactament el frame 1800 i deixar la
|
||||
// demo en bucle infinit fins a petar al .at(2000).
|
||||
// fade_out_->activate() ja és idempotent (Fade::activate retorna si
|
||||
// l'estat ja no és NOT_ENABLED).
|
||||
if (demo_.index >= TOTAL_DEMO_DATA - 200) {
|
||||
fade_out_->setType(Fade::Type::RANDOM_SQUARE2);
|
||||
fade_out_->setPostDuration(param.fade.post_duration_ms);
|
||||
fade_out_->activate();
|
||||
|
||||
@@ -36,7 +36,6 @@ HiScoreTable::HiScoreTable()
|
||||
background_(std::make_unique<Background>()),
|
||||
|
||||
view_area_(SDL_FRect{.x = 0, .y = 0, .w = param.game.width, .h = param.game.height}),
|
||||
fade_mode_(Fade::Mode::IN),
|
||||
background_fade_color_(Color(0, 0, 0)) {
|
||||
// Inicializa el resto
|
||||
Section::name = Section::Name::HI_SCORE_TABLE;
|
||||
|
||||
@@ -55,12 +55,12 @@ class HiScoreTable {
|
||||
std::vector<Path> paths_; // Vector con los recorridos precalculados
|
||||
|
||||
// --- Variables ---
|
||||
float elapsed_time_ = 0.0F; // Tiempo transcurrido (segundos)
|
||||
Uint64 last_time_ = 0; // Último timestamp para calcular delta-time
|
||||
SDL_FRect view_area_; // Parte de la textura que se muestra en pantalla
|
||||
Fade::Mode fade_mode_; // Modo de fade a utilizar
|
||||
Color background_fade_color_; // Color de atenuación del fondo
|
||||
std::vector<Color> entry_colors_; // Colores para destacar las entradas en la tabla
|
||||
float elapsed_time_ = 0.0F; // Tiempo transcurrido (segundos)
|
||||
Uint64 last_time_ = 0; // Último timestamp para calcular delta-time
|
||||
SDL_FRect view_area_; // Parte de la textura que se muestra en pantalla
|
||||
Fade::Mode fade_mode_ = Fade::Mode::IN; // Modo de fade a utilizar
|
||||
Color background_fade_color_; // Color de atenuación del fondo
|
||||
std::vector<Color> entry_colors_; // Colores para destacar las entradas en la tabla
|
||||
|
||||
// --- Flags para eventos basados en tiempo ---
|
||||
struct HiScoreFlags {
|
||||
|
||||
@@ -84,7 +84,7 @@ void Instructions::iniSprites() {
|
||||
// Inicializa los sprites
|
||||
for (int i = 0; std::cmp_less(i, item_textures_.size()); ++i) {
|
||||
auto sprite = std::make_unique<Sprite>(item_textures_[i], 0, 0, Item::WIDTH, Item::HEIGHT);
|
||||
sprite->setPosition((SDL_FPoint){.x = sprite_pos_.x, .y = sprite_pos_.y + ((Item::HEIGHT + item_space_) * i)});
|
||||
sprite->setPosition(SDL_FPoint{.x = sprite_pos_.x, .y = sprite_pos_.y + ((Item::HEIGHT + item_space_) * i)});
|
||||
sprites_.push_back(std::move(sprite));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ Title::Title()
|
||||
tiled_bg_(std::make_unique<TiledBG>(param.game.game_area.rect, TiledBGMode::RANDOM)),
|
||||
game_logo_(std::make_unique<GameLogo>(param.game.game_area.center_x, param.title.title_c_c_position)),
|
||||
mini_logo_sprite_(std::make_unique<Sprite>(Resource::get()->getTexture("logo_jailgames_mini.png"))),
|
||||
state_(State::LOGO_ANIMATING),
|
||||
num_controllers_(Input::get()->getNumGamepads()) {
|
||||
// Configura objetos
|
||||
tiled_bg_->setColor(param.title.bg_color);
|
||||
|
||||
@@ -94,7 +94,7 @@ class Title {
|
||||
Anchor anchor_; // Anclas para definir la posición de los elementos del título
|
||||
Section::Name next_section_; // Siguiente sección a cargar
|
||||
Section::Options selection_ = Section::Options::TITLE_TIME_OUT; // Opción elegida en el título
|
||||
State state_; // Estado actual de la sección
|
||||
State state_ = State::LOGO_ANIMATING; // Estado actual de la sección
|
||||
Uint64 last_time_ = 0; // Último timestamp para calcular delta-time
|
||||
float counter_time_ = 0.0F; // Temporizador para la pantalla de título (en segundos)
|
||||
float blink_accumulator_ = 0.0F; // Acumulador para el parpadeo (en segundos)
|
||||
|
||||
@@ -39,11 +39,11 @@ class MenuOption {
|
||||
|
||||
[[nodiscard]] virtual auto getBehavior() const -> Behavior = 0;
|
||||
[[nodiscard]] virtual auto getValueAsString() const -> std::string { return ""; }
|
||||
virtual void adjustValue(bool adjust_up) {}
|
||||
virtual void adjustValue(bool /*adjust_up*/) {}
|
||||
[[nodiscard]] virtual auto getTargetGroup() const -> ServiceMenu::SettingsGroup { return ServiceMenu::SettingsGroup::MAIN; }
|
||||
virtual void executeAction() {}
|
||||
|
||||
virtual auto getMaxValueWidth(Text* text_renderer) const -> int { return 0; } // Método virtual para que cada opción calcule el ancho de su valor más largo
|
||||
virtual auto getMaxValueWidth(Text* /*text_renderer*/) const -> int { return 0; } // Método virtual para que cada opción calcule el ancho de su valor más largo
|
||||
|
||||
protected:
|
||||
// --- Variables ---
|
||||
|
||||
@@ -45,7 +45,7 @@ void MenuRenderer::ShowHideAnimation::stop() {
|
||||
elapsed = 0.0F;
|
||||
}
|
||||
|
||||
MenuRenderer::MenuRenderer(const ServiceMenu* menu_state, std::shared_ptr<Text> element_text, std::shared_ptr<Text> title_text)
|
||||
MenuRenderer::MenuRenderer(const ServiceMenu* /*menu_state*/, std::shared_ptr<Text> element_text, std::shared_ptr<Text> title_text)
|
||||
: element_text_(std::move(element_text)),
|
||||
title_text_(std::move(title_text)) {
|
||||
initializeMaxSizes();
|
||||
@@ -145,7 +145,7 @@ void MenuRenderer::render(const ServiceMenu* menu_state) {
|
||||
}
|
||||
}
|
||||
|
||||
void MenuRenderer::update(const ServiceMenu* menu_state, float delta_time) {
|
||||
void MenuRenderer::update(const ServiceMenu* /*menu_state*/, float delta_time) {
|
||||
updateAnimations(delta_time);
|
||||
|
||||
if (visible_) {
|
||||
@@ -380,7 +380,7 @@ void MenuRenderer::updatePosition() {
|
||||
|
||||
// Resto de métodos (sin cambios significativos)
|
||||
|
||||
void MenuRenderer::precalculateMenuWidths(const std::vector<std::unique_ptr<MenuOption>>& all_options, const ServiceMenu* menu_state) { // NOLINT(readability-named-parameter)
|
||||
void MenuRenderer::precalculateMenuWidths(const std::vector<std::unique_ptr<MenuOption>>& all_options, const ServiceMenu* menu_state) {
|
||||
std::ranges::fill(group_menu_widths_, ServiceMenu::MIN_WIDTH);
|
||||
for (int group = 0; group < 5; ++group) {
|
||||
auto sg = static_cast<ServiceMenu::SettingsGroup>(group);
|
||||
|
||||
@@ -140,7 +140,7 @@ class MenuRenderer {
|
||||
void updateSwapAnimation(float delta_time);
|
||||
void updatePosition();
|
||||
|
||||
void precalculateMenuWidths(const std::vector<std::unique_ptr<MenuOption>>& all_options, const ServiceMenu* menu_state); // NOLINT(readability-avoid-const-params-in-decls)
|
||||
void precalculateMenuWidths(const std::vector<std::unique_ptr<MenuOption>>& all_options, const ServiceMenu* menu_state);
|
||||
[[nodiscard]] auto getMenuWidthForGroup(ServiceMenu::SettingsGroup group) const -> int;
|
||||
[[nodiscard]] auto getAnimatedSelectedColor() const -> Color;
|
||||
void updateColorCounter();
|
||||
|
||||
@@ -33,7 +33,6 @@ Notifier::Notifier(const std::string& icon_file, std::shared_ptr<Text> text)
|
||||
icon_texture_(!icon_file.empty() ? std::make_unique<Texture>(renderer_, icon_file) : nullptr),
|
||||
text_(std::move(text)),
|
||||
bg_color_(param.notification.color),
|
||||
stack_(false),
|
||||
has_icons_(!icon_file.empty()) {}
|
||||
|
||||
// Dibuja las notificaciones por pantalla
|
||||
@@ -260,7 +259,7 @@ void Notifier::show(std::vector<std::string> texts, int icon, const std::string&
|
||||
|
||||
// Dibuja el icono de la notificación
|
||||
if (has_icons_ && icon >= 0 && texts.size() >= 2) {
|
||||
auto sp = std::make_unique<Sprite>(icon_texture_, (SDL_FRect){.x = 0, .y = 0, .w = ICON_SIZE, .h = ICON_SIZE});
|
||||
auto sp = std::make_unique<Sprite>(icon_texture_, SDL_FRect{.x = 0, .y = 0, .w = ICON_SIZE, .h = ICON_SIZE});
|
||||
sp->setPosition({.x = PADDING_IN_H, .y = PADDING_IN_V, .w = ICON_SIZE, .h = ICON_SIZE});
|
||||
sp->setSpriteClip(SDL_FRect{
|
||||
.x = static_cast<float>(ICON_SIZE * (icon % 10)),
|
||||
|
||||
@@ -88,8 +88,8 @@ class Notifier {
|
||||
std::vector<Notification> notifications_; // Lista de notificaciones activas
|
||||
Color bg_color_; // Color de fondo de las notificaciones
|
||||
// Nota: wait_time_ eliminado, ahora se usa STAY_DURATION_S
|
||||
bool stack_; // Indica si las notificaciones se apilan
|
||||
bool has_icons_; // Indica si el notificador tiene textura para iconos
|
||||
bool stack_ = false; // Indica si las notificaciones se apilan
|
||||
bool has_icons_; // Indica si el notificador tiene textura para iconos
|
||||
|
||||
// --- Métodos internos ---
|
||||
void clearFinishedNotifications(); // Elimina las notificaciones cuyo estado es FINISHED
|
||||
|
||||
@@ -28,9 +28,7 @@ void ServiceMenu::destroy() { delete ServiceMenu::instance; }
|
||||
auto ServiceMenu::get() -> ServiceMenu* { return ServiceMenu::instance; }
|
||||
|
||||
// Constructor
|
||||
ServiceMenu::ServiceMenu()
|
||||
: current_settings_group_(SettingsGroup::MAIN),
|
||||
previous_settings_group_(current_settings_group_) {
|
||||
ServiceMenu::ServiceMenu() {
|
||||
auto element_text = Resource::get()->getText("04b_25_flat");
|
||||
auto title_text = Resource::get()->getText("04b_25_flat_2x");
|
||||
|
||||
@@ -377,11 +375,6 @@ void ServiceMenu::addVideoOptions() {
|
||||
addVideoShaderOption();
|
||||
addVideoPresetOption();
|
||||
|
||||
options_.push_back(std::make_unique<BoolOption>(
|
||||
Lang::getText("[SERVICE_MENU] SUPERSAMPLING"),
|
||||
SettingsGroup::VIDEO,
|
||||
&Options::video.supersampling.enabled));
|
||||
|
||||
options_.push_back(std::make_unique<BoolOption>(
|
||||
Lang::getText("[SERVICE_MENU] VSYNC"),
|
||||
SettingsGroup::VIDEO,
|
||||
@@ -543,6 +536,22 @@ void ServiceMenu::addSettingsOptions() {
|
||||
Options::checkPendingChanges();
|
||||
}));
|
||||
|
||||
// Preset de paràmetres (requereix reinici): cicla classic/arcade/red
|
||||
std::vector<std::string> preset_display_names;
|
||||
preset_display_names.reserve(Options::PARAMS_PRESETS.size());
|
||||
std::ranges::transform(Options::PARAMS_PRESETS, std::back_inserter(preset_display_names), Options::getParamsPresetDisplayName);
|
||||
options_.push_back(std::make_unique<ListOption>(
|
||||
Lang::getText("[SERVICE_MENU] GAME_PRESET"),
|
||||
SettingsGroup::SETTINGS,
|
||||
preset_display_names,
|
||||
[]() -> std::string {
|
||||
return Options::getParamsPresetDisplayName(Options::pending_changes.new_params_preset);
|
||||
},
|
||||
[](const std::string& val) -> void {
|
||||
Options::pending_changes.new_params_preset = Options::getParamsPresetIdFromDisplay(val);
|
||||
Options::checkPendingChanges();
|
||||
}));
|
||||
|
||||
options_.push_back(std::make_unique<BoolOption>(
|
||||
Lang::getText("[SERVICE_MENU] ENABLE_SHUTDOWN"),
|
||||
SettingsGroup::SETTINGS,
|
||||
@@ -550,13 +559,21 @@ void ServiceMenu::addSettingsOptions() {
|
||||
}
|
||||
|
||||
void ServiceMenu::addSystemOptions() {
|
||||
// Al navegador no podem reiniciar el procés (execv no existeix), així que amaguem
|
||||
// l'opció en compte de mostrar un fals reset que no recarrega params/finestra.
|
||||
#ifdef __EMSCRIPTEN__
|
||||
constexpr bool RESET_HIDDEN = true;
|
||||
#else
|
||||
constexpr bool RESET_HIDDEN = false;
|
||||
#endif
|
||||
options_.push_back(std::make_unique<ActionOption>(
|
||||
Lang::getText("[SERVICE_MENU] RESET"),
|
||||
SettingsGroup::SYSTEM,
|
||||
[this]() -> void {
|
||||
Section::name = Section::Name::RESET;
|
||||
toggle();
|
||||
}));
|
||||
},
|
||||
RESET_HIDDEN));
|
||||
|
||||
options_.push_back(std::make_unique<ActionOption>(
|
||||
Lang::getText("[SERVICE_MENU] QUIT"),
|
||||
@@ -680,13 +697,6 @@ void ServiceMenu::setHiddenOptions() {
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
auto* option = getOptionByCaption(Lang::getText("[SERVICE_MENU] SUPERSAMPLING"));
|
||||
if (option != nullptr) {
|
||||
option->setHidden(!HW_ACCEL || !Options::video.shader.enabled || Options::video.shader.current_shader != Rendering::ShaderType::POSTFX);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
// En la versión web no tiene sentido exponer: permitir apagar el sistema
|
||||
// (no aplica al navegador) ni salir del juego (lo gestiona el navegador).
|
||||
|
||||
@@ -88,8 +88,8 @@ class ServiceMenu {
|
||||
std::vector<std::unique_ptr<MenuOption>> options_;
|
||||
std::vector<MenuOption*> display_options_;
|
||||
std::vector<std::pair<std::string, std::string>> option_pairs_;
|
||||
SettingsGroup current_settings_group_;
|
||||
SettingsGroup previous_settings_group_;
|
||||
SettingsGroup current_settings_group_ = SettingsGroup::MAIN;
|
||||
SettingsGroup previous_settings_group_ = SettingsGroup::MAIN;
|
||||
std::string title_;
|
||||
size_t selected_ = 0;
|
||||
size_t main_menu_selected_ = 0;
|
||||
@@ -103,15 +103,15 @@ class ServiceMenu {
|
||||
void updateDisplayOptions();
|
||||
void updateOptionPairs();
|
||||
void initializeOptions();
|
||||
void addControlsOptions(); // CONTROLS: mandos 1/2, teclat, swap
|
||||
void addVideoOptions(); // VIDEO: orquestra els blocs de vídeo
|
||||
void addVideoShaderOption(); // VIDEO: tria de shader (Disabled/PostFX/CrtPi)
|
||||
void addVideoPresetOption(); // VIDEO: cicla presets del shader actiu
|
||||
void addVideoFilterOption(); // VIDEO: filtre Nearest/Linear (fallback SDL)
|
||||
void addAudioOptions(); // AUDIO: enabled + tres volums
|
||||
void addSettingsOptions(); // SETTINGS: autofire, idioma, dificultat, shutdown
|
||||
void addSystemOptions(); // SYSTEM: reset, quit, shutdown
|
||||
void addMainMenuOptions(); // MAIN: carpetes de menú
|
||||
void addControlsOptions(); // CONTROLS: mandos 1/2, teclat, swap
|
||||
void addVideoOptions(); // VIDEO: orquestra els blocs de vídeo
|
||||
void addVideoShaderOption(); // VIDEO: tria de shader (Disabled/PostFX/CrtPi)
|
||||
void addVideoPresetOption(); // VIDEO: cicla presets del shader actiu
|
||||
void addVideoFilterOption(); // VIDEO: filtre Nearest/Linear (fallback SDL)
|
||||
void addAudioOptions(); // AUDIO: enabled + tres volums
|
||||
void addSettingsOptions(); // SETTINGS: autofire, idioma, dificultat, shutdown
|
||||
void addSystemOptions(); // SYSTEM: reset, quit, shutdown
|
||||
void addMainMenuOptions(); // MAIN: carpetes de menú
|
||||
void updateMenu();
|
||||
void applySettings();
|
||||
void applyControlsSettings();
|
||||
|
||||
+2
-2
@@ -12,8 +12,8 @@ Actualizando a la versión "Arcade Edition" en 08/05/2024
|
||||
|
||||
#include "core/system/director.hpp" // Para Director
|
||||
|
||||
auto SDL_AppInit(void** appstate, int /*argc*/, char** /*argv*/) -> SDL_AppResult {
|
||||
*appstate = new Director();
|
||||
auto SDL_AppInit(void** appstate, int argc, char** argv) -> SDL_AppResult {
|
||||
*appstate = new Director(argc, argv);
|
||||
return SDL_APP_CONTINUE;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// NOLINTNEXTLINE(bugprone-reserved-identifier) -- requerido por <cmath> para exponer M_PI en MSVC
|
||||
#define _USE_MATH_DEFINES
|
||||
#include "utils/color.hpp"
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
namespace Texts {
|
||||
constexpr const char* VERSION = "2026.04.14"; // Versión del juego (también usada por el Makefile)
|
||||
constexpr const char* VERSION = "2026.05.17"; // Versión del juego (también usada por el Makefile)
|
||||
} // namespace Texts
|
||||
|
||||
+11
-10
@@ -10,9 +10,10 @@
|
||||
#include <string> // Para string, basic_string, stoi, stof, hash, allocator, operator==, char_traits, operator+, operator>>, getline
|
||||
#include <unordered_map> // Para unordered_map, operator==, _Node_iterator_base
|
||||
|
||||
#include "game/ui/notifier.hpp" // Para Notifier
|
||||
#include "utils/color.hpp" // Para Color
|
||||
#include "utils/utils.hpp" // Para Zone, stringToBool, getFileName
|
||||
#include "core/resources/resource_helper.hpp" // Para ResourceHelper::loadFile
|
||||
#include "game/ui/notifier.hpp" // Para Notifier
|
||||
#include "utils/color.hpp" // Para Color
|
||||
#include "utils/utils.hpp" // Para Zone, stringToBool, getFileName
|
||||
|
||||
// Variable global - ahora se inicializa automáticamente con valores por defecto
|
||||
Param param;
|
||||
@@ -42,22 +43,24 @@ void Param::precalculateZones() {
|
||||
game.game_area.third_quarter_y = game.game_area.rect.h / 4 * 3;
|
||||
}
|
||||
|
||||
// Carga los parámetros desde un archivo
|
||||
// Carga los parámetros desde un archivo (busca primer al pack, fallback al filesystem)
|
||||
void loadParamsFromFile(const std::string& file_path) {
|
||||
// Los parámetros ya están inicializados con valores por defecto
|
||||
// Solo necesitamos abrir el archivo y sobrescribir los valores que aparezcan
|
||||
|
||||
std::ifstream file(file_path);
|
||||
if (!file.is_open()) {
|
||||
auto buffer = ResourceHelper::loadFile(file_path);
|
||||
if (buffer.empty()) {
|
||||
std::cout << "Error: No se pudo abrir el archivo " << file_path << '\n';
|
||||
throw std::runtime_error("No se pudo abrir el archivo: " + file_path);
|
||||
}
|
||||
|
||||
std::string content(buffer.begin(), buffer.end());
|
||||
std::istringstream stream(content);
|
||||
|
||||
std::string line;
|
||||
std::string param_name;
|
||||
std::string param_value;
|
||||
|
||||
while (std::getline(file, line)) {
|
||||
while (std::getline(stream, line)) {
|
||||
// Elimina comentarios
|
||||
auto comment_pos = line.find('#');
|
||||
if (comment_pos != std::string::npos) {
|
||||
@@ -73,8 +76,6 @@ void loadParamsFromFile(const std::string& file_path) {
|
||||
}
|
||||
}
|
||||
|
||||
file.close();
|
||||
|
||||
// Recalcula las zonas después de cargar todos los parámetros
|
||||
param.precalculateZones();
|
||||
}
|
||||
|
||||
@@ -1,109 +1,96 @@
|
||||
#include "core/resources/resource_pack.hpp"
|
||||
#include "../../build/version.h" // Para Version::APP_NAME
|
||||
#include <iostream>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
void showHelp() {
|
||||
std::cout << Version::APP_NAME << " - Resource Packer" << std::endl;
|
||||
std::cout << "===============================================" << std::endl;
|
||||
std::cout << "Usage: pack_resources [options] [input_dir] [output_file]" << std::endl;
|
||||
std::cout << std::endl;
|
||||
std::cout << "Options:" << std::endl;
|
||||
std::cout << " --help Show this help message" << std::endl;
|
||||
std::cout << " --list List contents of an existing pack file" << std::endl;
|
||||
std::cout << std::endl;
|
||||
std::cout << "Arguments:" << std::endl;
|
||||
std::cout << " input_dir Directory to pack (default: data)" << std::endl;
|
||||
std::cout << " output_file Pack file name (default: resources.pack)" << std::endl;
|
||||
std::cout << std::endl;
|
||||
std::cout << "Examples:" << std::endl;
|
||||
std::cout << " pack_resources # Pack 'data' to 'resources.pack'" << std::endl;
|
||||
std::cout << " pack_resources mydata # Pack 'mydata' to 'resources.pack'" << std::endl;
|
||||
std::cout << " pack_resources data my.pack # Pack 'data' to 'my.pack'" << std::endl;
|
||||
std::cout << " pack_resources --list my.pack # List contents of 'my.pack'" << std::endl;
|
||||
}
|
||||
#include "core/resources/resource_pack.hpp"
|
||||
#include "../../build/version.h" // Version::APP_NAME
|
||||
|
||||
void listPackContents(const std::string& packFile) {
|
||||
ResourcePack pack;
|
||||
if (!pack.loadPack(packFile)) {
|
||||
std::cerr << "Error: Cannot open pack file: " << packFile << std::endl;
|
||||
return;
|
||||
namespace {
|
||||
|
||||
void showHelp() {
|
||||
std::cout << Version::APP_NAME << " - Resource Packer\n";
|
||||
std::cout << "==============================================\n";
|
||||
std::cout << "Usage: pack_resources [options] [input_dir] [output_file]\n\n";
|
||||
std::cout << "Options:\n";
|
||||
std::cout << " --help Show this help message\n";
|
||||
std::cout << " --list List contents of an existing pack file\n\n";
|
||||
std::cout << "Arguments:\n";
|
||||
std::cout << " input_dir Directory to pack (default: data)\n";
|
||||
std::cout << " output_file Pack file name (default: resources.pack)\n";
|
||||
}
|
||||
|
||||
auto resources = pack.getResourceList();
|
||||
std::cout << "Pack file: " << packFile << std::endl;
|
||||
std::cout << "Resources: " << resources.size() << std::endl;
|
||||
std::cout << "Contents:" << std::endl;
|
||||
|
||||
for (const auto& resource : resources) {
|
||||
std::cout << " " << resource << std::endl;
|
||||
|
||||
void listPackContents(const std::string& pack_file) {
|
||||
ResourcePack pack;
|
||||
if (!pack.loadPack(pack_file)) {
|
||||
std::cerr << "Error: cannot open pack file: " << pack_file << '\n';
|
||||
return;
|
||||
}
|
||||
auto resources = pack.getResourceList();
|
||||
std::cout << "Pack file: " << pack_file << '\n';
|
||||
std::cout << "Resources: " << resources.size() << '\n';
|
||||
for (const auto& r : resources) { std::cout << " " << r << '\n'; }
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
std::string dataDir = "data";
|
||||
std::string outputFile = "resources.pack";
|
||||
bool listMode = false;
|
||||
bool dataDirSet = false;
|
||||
std::string data_dir = "data";
|
||||
std::string output_file = "resources.pack";
|
||||
bool list_mode = false;
|
||||
bool data_dir_set = false;
|
||||
|
||||
// Parse arguments
|
||||
for (int i = 1; i < argc; i++) {
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
std::string arg = argv[i];
|
||||
if (arg == "--help" || arg == "-h") {
|
||||
showHelp();
|
||||
return 0;
|
||||
} else if (arg == "--list") {
|
||||
listMode = true;
|
||||
if (i + 1 < argc) {
|
||||
outputFile = argv[++i]; // Next argument is pack file to list
|
||||
}
|
||||
} else if (!arg.empty() && arg[0] != '-') {
|
||||
if (!dataDirSet) {
|
||||
dataDir = arg;
|
||||
dataDirSet = true;
|
||||
}
|
||||
if (arg == "--list") {
|
||||
list_mode = true;
|
||||
if (i + 1 < argc) { output_file = argv[++i]; }
|
||||
continue;
|
||||
}
|
||||
if (!arg.empty() && arg[0] != '-') {
|
||||
if (!data_dir_set) {
|
||||
data_dir = arg;
|
||||
data_dir_set = true;
|
||||
} else {
|
||||
outputFile = arg;
|
||||
output_file = arg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (listMode) {
|
||||
listPackContents(outputFile);
|
||||
|
||||
if (list_mode) {
|
||||
listPackContents(output_file);
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::cout << Version::APP_NAME << " - Resource Packer" << std::endl;
|
||||
std::cout << "===============================================" << std::endl;
|
||||
std::cout << "Input directory: " << dataDir << std::endl;
|
||||
std::cout << "Output file: " << outputFile << std::endl;
|
||||
std::cout << std::endl;
|
||||
|
||||
if (!std::filesystem::exists(dataDir)) {
|
||||
std::cerr << "Error: Input directory does not exist: " << dataDir << std::endl;
|
||||
|
||||
std::cout << Version::APP_NAME << " - Resource Packer\n";
|
||||
std::cout << "==============================================\n";
|
||||
std::cout << "Input directory: " << data_dir << '\n';
|
||||
std::cout << "Output file: " << output_file << '\n';
|
||||
|
||||
if (!std::filesystem::exists(data_dir)) {
|
||||
std::cerr << "Error: input directory does not exist: " << data_dir << '\n';
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
ResourcePack pack;
|
||||
|
||||
std::cout << "Scanning and packing resources..." << std::endl;
|
||||
if (!pack.addDirectory(dataDir)) {
|
||||
std::cerr << "Error: Failed to add directory to pack" << std::endl;
|
||||
std::cout << "Scanning and packing resources...\n";
|
||||
if (!pack.addDirectory(data_dir)) {
|
||||
std::cerr << "Error: failed to add directory to pack\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::cout << "Found " << pack.getResourceCount() << " resources" << std::endl;
|
||||
|
||||
std::cout << "Saving pack file..." << std::endl;
|
||||
if (!pack.savePack(outputFile)) {
|
||||
std::cerr << "Error: Failed to save pack file" << std::endl;
|
||||
std::cout << "Found " << pack.getResourceCount() << " resources\n";
|
||||
|
||||
std::cout << "Saving pack file...\n";
|
||||
if (!pack.savePack(output_file)) {
|
||||
std::cerr << "Error: failed to save pack file\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::filesystem::path packPath(outputFile);
|
||||
auto fileSize = std::filesystem::file_size(packPath);
|
||||
|
||||
std::cout << "Pack file created successfully!" << std::endl;
|
||||
std::cout << "File size: " << (fileSize / 1024.0 / 1024.0) << " MB" << std::endl;
|
||||
|
||||
|
||||
auto file_size = std::filesystem::file_size(std::filesystem::path(output_file));
|
||||
std::cout << "Pack file created: " << output_file << " ("
|
||||
<< (static_cast<double>(file_size) / 1024.0 / 1024.0) << " MB)\n";
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,6 @@ cmake_policy(SET CMP0007 NEW)
|
||||
set(SHADER_SOURCES
|
||||
"postfx.vert"
|
||||
"postfx.frag"
|
||||
"upscale.frag"
|
||||
"downscale.frag"
|
||||
"crtpi_frag.glsl"
|
||||
)
|
||||
|
||||
@@ -24,15 +22,11 @@ set(SHADER_SOURCES
|
||||
set(SHADER_VARS
|
||||
"kpostfx_vert_spv"
|
||||
"kpostfx_frag_spv"
|
||||
"kupscale_frag_spv"
|
||||
"kdownscale_frag_spv"
|
||||
"kcrtpi_frag_spv"
|
||||
)
|
||||
|
||||
# Flags extra de glslc para cada shader (vacío si no hay)
|
||||
set(SHADER_FLAGS
|
||||
""
|
||||
""
|
||||
""
|
||||
""
|
||||
"-fshader-stage=frag"
|
||||
|
||||
Reference in New Issue
Block a user