refactor: JA_Sound_t RAII — buffer amb unique_ptr + SDLFreeDeleter, elimina JA_NewSound

This commit is contained in:
2026-04-16 13:28:31 +02:00
parent 96a3cf9ebc
commit 550e3e0e12
6 changed files with 204 additions and 74 deletions

110
CLAUDE.md
View File

@@ -38,18 +38,25 @@ The five current objectives are:
**Iron rule: zero gameplay regressions.** Each phase of the modernization must leave the game playing identically — same feel, same timings, same collisions, same scoring. See [docs/scenes-migration-plan.md](docs/scenes-migration-plan.md) for the phased plan.
The current emulator-thread architecture (Director + game thread + `publishFrame` mutex/cv) is **transitional**. It will be dismantled in Phase 5 and replaced by a single-threaded `SDL_AppIterate` loop in Phase 7.
### Migration Status (2026-04-16)
Phases 07b of the original runtime plan are **done**. Current effort is the **scene-by-scene rewrite of `source/game/modulesequence.cpp`** over a `scenes::` layer in [source/scenes/](source/scenes/):
**Completat.** Totes les fases del pla original (07) i la migració `scenes::` (Steps 010) estan fetes, ModuleGame és una `scenes::Scene` tick-based, el cooperative fiber s'ha eliminat, i el build emscripten/WASM arrenca i es publica a maverick.
- **Done**: `MortScene` (state 100), `BannerScene` (2..5), `MenuScene` (0), `IntroNewLogoScene` (255 when `use_new_logo`), `SlidesScene` (1, 7), `CreditsScene` (8), `SecretaScene` (6). Each registered in `Director::init` via `SceneRegistry`. Each removed from the legacy `ModuleSequence::Go()` switch and deleted from `modulesequence.cpp`.
- **Pending**: `IntroScene` (state 255 when `!use_new_logo` — the old JAILGAMES letter-by-letter), `IntroSpritesScene` (the Sam + momies animation with 3 random variants, hardest of the lot, currently still called from `IntroNewLogoScene::Phase::Delegate` via a temporary `doIntroSprites` exposed as `public` in `ModuleSequence`). Final cleanup of `modulesequence.cpp` comes after those two.
- `SceneRegistry` lookup happens inside `gameFiberEntry()` before falling back to legacy `ModuleSequence::Go()`, with a redirect `num_piramide == 6 && diners < 200 → 7` replicated ahead of the lookup to match the legacy flow.
- For quick tests, `Options::game` exposes `piramide_inicial`, `habitacio_inicial`, `vides`, `diamants_inicial`, `diners_inicial`, `use_new_logo`, `show_title_credits` — all persisted in `config.yaml`.
**Arquitectura actual**:
- Un sol thread (Director). Main loop via SDL3 Callback API (`SDL_MAIN_USE_CALLBACKS`): `SDL_AppInit/Iterate/Event/Quit` a [main.cpp](source/main.cpp).
- `Director::iterate()` posseeix l'estat d'escena (`current_scene_`, `game_state_`) i fa input → tick de l'escena → `JD8_Flip` (sense yield, només converteix `screen``pixel_data`) → overlay → present. Tot en línia recta, zero fibers, zero mutex.
- Totes les escenes (inclòs `ModuleGame`) implementen `scenes::Scene` amb `onEnter/tick(delta_ms)/done/nextState`.
- `ModuleSequence` (el vell dispatcher) eliminat. Despatxa via `game_state_ == 0` (gameplay → `ModuleGame`) o `game_state_ == 1` (cinemàtica → `SceneRegistry::tryCreate(num_piramide)`).
The scenes layer itself lives in [source/scenes/](source/scenes/): `scene.hpp` (interface), `scene_registry.hpp/.cpp`, `timeline.hpp/.cpp`, `sprite_mover.hpp/.cpp`, `frame_animator.hpp/.cpp`, `palette_fade.hpp/.cpp`, `surface_handle.hpp/.cpp`, `scene_utils.hpp/.cpp` (`playMusic`). Scenes are pure tick-based (no fibers, no `while`, no `JG_ShouldUpdate`) — the cooperative fiber still runs underneath them but `JD8_Flip()` inside the mini-while in `gameFiberEntry` is what yields. Once `IntroScene` + `IntroSpritesScene` are migrated, the fiber can be dismantled along with `ModuleGame`.
**Escenes migrades** (totes registrades a `Director::init` via `SceneRegistry`):
- `MortScene` (state 100) · `BannerScene` (2..5) · `MenuScene` (0) · `SlidesScene` (1, 7)
- `CreditsScene` (8) · `SecretaScene` (6) · `IntroNewLogoScene` (255, `use_new_logo=true`)
- `IntroScene` (255, `use_new_logo=false`) · `IntroSpritesScene` (sub-escena de les dues intros)
**Files d'`Options::game` exposats per a tests ràpids** (persistits a `config.yaml`):
`piramide_inicial`, `habitacio_inicial`, `vides`, `diamants_inicial`, `diners_inicial`, `use_new_logo`, `show_title_credits`.
**La capa `scenes::`** ([source/scenes/](source/scenes/)): `scene.hpp` (interfície), `scene_registry.hpp/.cpp`, `timeline`, `sprite_mover`, `frame_animator`, `palette_fade`, `surface_handle`, `scene_utils` (`playMusic`). Pures tick-based, zero while, zero `JG_ShouldUpdate`.
### Modernization Targets
@@ -94,7 +101,7 @@ Flat C-style APIs (no classes), prefixed by subsystem. Being progressively conve
### System Layer (`source/core/system/`)
- **Director** (`director.hpp/cpp`) — **Orchestrator singleton**. Owns main thread. Launches game thread that runs `ModuleGame`/`ModuleSequence::Go()`. Emulator-style architecture: Director runs at ~60 FPS independently, polls SDL events, updates overlay, presents frames. Game thread blocks at `JD8_Flip()` `Director::publishFrame()` until Director consumes the frame. Director is **non-blocking**: if no new frame is available, it re-presents the last known game frame with fresh overlay on top
- **Director** (`director.hpp/cpp`) — **Orchestrator singleton**, únic thread del runtime. Posseeix l'estat d'escena (`current_scene_: unique_ptr<Scene>`, `game_state_`, `last_tick_ms_`) directament com a members. `iterate()` fa: poll events (via `SDL_AppEvent`) → input (Gamepad/KeyRemap/GlobalInputs/Mouse) → `JA_Update` → transició d'escena si `done()``scene->tick(delta_ms)` `JD8_Flip` (converteix `screen``pixel_data`) → overlay → present → `SDL_Delay` al frame target. Dispatcher: `game_state_ == 0``new ModuleGame`, `game_state_ == 1``SceneRegistry::tryCreate(info::ctx.num_piramide)` (amb redirect `num_piramide == 6 && diners < 200 → 7` replicant el vell `ModuleSequence::Go`).
### Presentation Layer (`source/core/rendering/`)
@@ -142,7 +149,7 @@ Follows the pattern from `jaildoctors_dilemma`, persists to YAML:
| F8 | Cycle shader presets |
| F9 | Toggle stretch filter (nearest ↔ linear) |
| F10 | Cycle render info (off → top → bottom → off) |
| F11 | Toggle pause (Director stops resuming the game fiber + `JA_PauseMusic`/`JA_ResumeMusic`) |
| F11 | Toggle pause (Director skips `scene->tick()` + `JA_PauseMusic`/`JA_ResumeMusic`) |
| F12 | Toggle floating options menu |
| ESC | Double-press to quit (with overlay notification) / close menu if open |
| Backspace | Go up one menu level / close menu if at root |
@@ -150,45 +157,48 @@ Follows the pattern from `jaildoctors_dilemma`, persists to YAML:
All key bindings are configurable via `Options::keys_gui` and stored in `config.yaml` (section `controls:` with SDL scancode names). Game movement keys (`Options::keys_game.up/down/left/right`) can be remapped via the CONTROLS submenu — the `KeyRemap` module mirrors custom physical keys to virtual standard scancodes so hardcoded game code keeps working.
### Execution Model (Single-threaded Fibers)
### Execution Model (Single-threaded, Scene-based)
Since Phase 4+5, the old game thread + `publishFrame` mutex/cv has been **removed**. The game code (`ModuleGame`, `ModuleSequence`, all their `Go()` methods with internal `while` loops) runs inside a **cooperative fiber** (see [fiber.hpp](source/core/system/fiber.hpp) / [fiber.cpp](source/core/system/fiber.cpp)). The whole process is single-threaded.
Zero threads, zero fibers, zero mutex. Main loop via SDL3 Callback API (`SDL_MAIN_USE_CALLBACKS`) a [main.cpp](source/main.cpp). Cada frame entra pel `SDL_AppIterate``Director::iterate()`:
```
Main thread (only thread)
─────────────────────────
Director::run() loop {
SDL_PollEvent()
GlobalInputs, Mouse, KeyRemap
SDL_AppIterate → Director::iterate() {
if (quit_requested_) { scene.reset(); return false; }
if (!context_initialized_) initGameContext();
Gamepad/KeyRemap/GlobalInputs/Mouse::update
JA_Update() ← audio pump
if !paused:
GameFiber::resume() ← hands control to game code
↓ (runs until next JD8_Flip)
... game code runs ...
JD8_Flip():
palette → ARGB → pixel_data
GameFiber::yield() ← returns control to Director
copy JD8_GetFramebuffer() → game_frame
if (!paused_) {
if (scene && (scene->done() || JG_Quitting()))
game_state_ = scene->nextState(); scene.reset();
if (!scene) {
if (game_state_ == -1 || JG_Quitting()) return false;
scene = createNextScene(); ← ModuleGame o registry.tryCreate()
scene->onEnter();
}
JI_Update()
scene->tick(now - last_tick_ms_)
JD8_Flip() ← converteix screen indexat → pixel_data
memcpy pixel_data → game_frame
}
memcpy game_frame → presentation_buffer
Overlay::render(presentation_buffer)
Screen::present(presentation_buffer)
SDL_Delay to hit 60fps
SDL_Delay(frame_target - elapsed)
}
SDL_AppEvent → Director::handleEvent() ← events lliurats un a un per SDL
SDL_AppQuit → Director::teardown() ← Options::save + descàrrega ordenada
```
**Fiber backend** ([fiber.cpp](source/core/system/fiber.cpp)):
- **Linux / macOS**: `ucontext_t` + `makecontext`/`swapcontext` (deprecated in POSIX.1-2008 but still functional in glibc and macOS libc; warning silenced with `#pragma`).
- **Windows**: `ConvertThreadToFiber` / `CreateFiber` / `SwitchToFiber` (native Fibers API).
- **Emscripten**: not yet. Phase 7 will add an `emscripten_fiber_*` or Asyncify backend.
**Key points:**
- Single-threaded: zero `std::thread`, zero `std::mutex`, zero `std::condition_variable`.
- `JD8_Flip()` is the natural sync point: it calls `GameFiber::yield()` instead of the old blocking `publishFrame`.
- Pause (F11) works by Director skipping `resume()`: the fiber stays frozen at its last yield, and Director keeps repainting the last frame with fresh overlay.
- Double buffer still exists (`game_frame` + `presentation_buffer`) because Director can present multiple frames per game frame during pause or slow sections. Eliminating it is marginal work and the extra 256 KB copy is cheap at 320×200.
- The state machine alternating `ModuleSequence` (state=1) and `ModuleGame` (state=0) now lives in `gameFiberEntry()` inside an anonymous namespace in [director.cpp](source/core/system/director.cpp), called once as the fiber entry point.
- SDL events still processed only on the main thread (which is now the only thread anyway).
- `Director` posseeix `current_scene_`, `game_state_`, `last_tick_ms_`, `context_initialized_` com a members — abans vivien al stack del fiber.
- `JD8_Flip()` només converteix paleta + `screen` a ARGB (`pixel_data`). Ja **no fa yield** — tot corre lineal.
- `JG_ShouldUpdate()` encara existeix a `jgame.cpp` com a timing-gate per a `ModuleGame::Update()` (10 ms fix), però ja no fa yield. Cap caller fa spin-wait.
- Pausa (F11) simplement salta el bloc de tick; overlay i present continuen, es re-presenta l'últim frame congelat.
- Doble buffer (`game_frame` + `presentation_buffer`) es manté perquè el Director pot presentar múltiples frames per cada tick durant pausa; el cost (256 KB memcpy) és trivial a 320×200.
- SDL3 Callback API compatible amb emscripten: el navegador posseeix el main loop i ens crida via `requestAnimationFrame`. Zero canvis de codi per a portabilitat.
### Rendering Pipeline (inside Screen::present)
@@ -238,12 +248,29 @@ JD8_Flip produces ABGR byte order: `0xFF000000 + R + (G<<8) + (B<<16)`. SDL text
2. ~~**Cheats are broken (`reviu`, `alone`, `obert`)**~~: Fixed. `JI_moveCheats` tradueix `SDL_Scancode` → ASCII via `scancode_to_ascii` abans de ficar-los al buffer ([jinput.cpp:32-37, 55-61](source/core/jail/jinput.cpp#L32-L61)), i `JI_CheatActivated` compara ASCII amb ASCII.
3. **No sound effects in game**: Game code never calls `JA_PlaySound*`/`JA_LoadSound` — only music via `JA_PlayMusic`/`JA_FadeOutMusic`. The SONS and VOL SONS menu items control volume of an empty channel pool. Infrastructure ready for future SFX.
4. ~~**Raw `malloc`/`free` in gameplay structs**~~: Majoritàriament arreglat. `Sprite`/`Entitat` usen `std::vector<Frame>` i `std::vector<Animacio>` ([sprite.hpp](source/game/sprite.hpp)). `jfile.cpp` ja no té el global `scratch[255]` (substituït per `thread_local std::string`). L'API `file_getfilebuffer` (que tornava raw `char*` amb `malloc`) s'ha substituït per `file_readfile` que retorna `std::vector<char>` RAII — elimina els leaks silenciosos que hi havia a `JD8_LoadPalette`, `ModuleGame::Go()` i `scenes::playMusic`. Queda `jail_audio.hpp` barrejant `new`/`malloc`/`SDL_malloc` de forma pairada i correcta (no leak), pendent de polir amb `std::unique_ptr` quan toque.
5. ~~**Blocking loops in cinematics and fades**~~: Fixed. La migració completa de `ModuleSequence::do*()` a la capa `scenes::` (Steps 19) ha eliminat tots els `while(!JG_ShouldUpdate())` i `wait_frame_or_skip()`. Les cinemàtiques ara són tick-based amb acumuladors ms. `JD8_FadeOut`/`JD8_FadeToPal` encara tenen el seu bucle intern de 32 passos (usat per a transicions fora d'escena com al final de `ModuleGame`); el wrapper tick-based `scenes::PaletteFade` el consumeix un pas per tick quan es crida des d'una escena.
6. ~~**`SDL_AddTimer` in audio**~~: Fixed in Phase 3. `jail_audio` is now a header-only `inline` module (single `.cpp` stub only hosts the `stb_vorbis` implementation to avoid multiple definitions). Music uses true streaming via `stb_vorbis_open_memory` + `JA_PumpMusic` with a 0.5s low-water-mark instead of decoding the whole OGG into RAM. Mixing/fade update runs manually via `JA_Update()` called once per frame from `Director::run()`. Ported from the `jaildoctors_dilemma` codebase.
7. ~~**Game thread + `publishFrame` mutex/cv**~~: Fixed in Phase 4+5. Replaced by a cooperative `GameFiber` (ucontext on POSIX, Fibers API on Windows). `JD8_Flip()` calls `GameFiber::yield()`, Director calls `GameFiber::resume()` once per frame. Zero threads, zero mutexes. Emscripten fiber backend still pending for Phase 7.
5. ~~**Blocking loops in cinematics and fades**~~: Fixed. Migració completa de `ModuleSequence::do*()` a la capa `scenes::` (Steps 110) + `ModuleGame` també tick-based (Phase A). Tot `while(!JG_ShouldUpdate())` i `wait_frame_or_skip()` eliminat. Els fades bloquejants `JD8_FadeOut`/`JD8_FadeToPal` també eliminats (Phase B.2): només queda l'API tick-step `JD8_FadeStart*` + `JD8_FadeTickStep`, encapsulada pel wrapper `scenes::PaletteFade`. ModuleGame té fases `FadingIn`/`FadingOut` pròpies.
6. ~~**`SDL_AddTimer` in audio**~~: Fixed in Phase 3. `jail_audio` is now a header-only `inline` module (single `.cpp` stub only hosts the `stb_vorbis` implementation to avoid multiple definitions). Music uses true streaming via `stb_vorbis_open_memory` + `JA_PumpMusic` with a 0.5s low-water-mark instead of decoding the whole OGG into RAM. Mixing/fade update runs manually via `JA_Update()` called once per frame from `Director::iterate()`. Ported from the `jaildoctors_dilemma` codebase.
7. ~~**Game thread + `publishFrame` mutex/cv**~~: Fixed in Phase 4+5 via cooperative `GameFiber`; **eliminated entirely in Phase B.2**. `JD8_Flip()` ja no fa yield — només converteix `screen``pixel_data`. Director posseeix l'estat d'escena (`current_scene_`, `game_state_`) i crida `scene->tick()` directament des d'`iterate()`. Fitxers `source/core/system/fiber.{hpp,cpp}` esborrats. Zero threads, zero mutex, zero fibers.
8. ~~**`ModuleSequence` legacy dispatcher**~~: Eliminated in Step 10. Era el vell switch per `num_piramide`, ara substituït per `SceneRegistry::tryCreate()` i dispatch directe des de `Director::iterate()`. `modulesequence.{hpp,cpp}` esborrats.
### WebAssembly Build
`make wasm` genera el build WASM via Docker (`emscripten/emsdk:latest`) i copia els 3 fitxers (`.js`/`.wasm`/`.data`) a `maverick:/home/sergio/gitea/web_jailgames/static/games/aee/wasm/`, amb un `ssh maverick './deploy.sh'` final. Output local a `dist/wasm/`.
**Diferències respecte build natiu** (a [CMakeLists.txt](CMakeLists.txt) dins `if(EMSCRIPTEN)`):
- SDL3 compilat des de font via `FetchContent` (no hi ha paquet de sistema).
- Shaders SPIR-V omesos (SDL3 GPU no suportat a WebGL2).
- `sdl3gpu_shader.cpp` exclòs dels sources — el fallback `SDL_Renderer` fa tota la presentació.
- [screen.cpp](source/core/rendering/screen.cpp) guarda `#ifndef NO_SHADERS` al voltant de l'include i les crides a `SDL3GPUShader` directes. La resta del codi va via interfície base `ShaderBackend`.
- Link flags: `--preload-file data@/data`, `-fexceptions`, `-sALLOW_MEMORY_GROWTH=1`, `-sMAX_WEBGL_VERSION=2`, `-sINITIAL_MEMORY=67108864`, `-sASSERTIONS=1`, `-sASYNCIFY=1`.
- Defines: `EMSCRIPTEN_BUILD`, `NO_SHADERS`.
**Filesystem**: MEMFS default — no persistent entre recàrregues. `file_setconfigfolder` té fallbacks robustos (`getpwuid``getenv("HOME")``/tmp`) perquè no pete quan emscripten no té `/etc/passwd`. La config es carrega per defecte cada vegada. IDBFS pendent si mai volguéssem persistència a web.
### Pending / Ideas for Later
- **Sound effects**: infraestructura `JA_PlaySound*`/`JA_LoadSound` ja preparada. Cablejar events de gameplay (col·lisió momia, mort, recollir objecte, trencar tomba, cheat activat). Menus SONS/VOL SONS ja controlen el volum del pool.
- **IDBFS persistence a WASM**: montar `/home/web_user/.config` com a IDBFS a l'init i `FS.syncfs` després de cada save. Opcional — ara per ara la config no persistix entre recàrregues de pàgina.
- **Gamepad**: map Y button (North) to `P` key for Pepe character selection at title screen (only input path not covered by current mapping).
- **Menu items for game**: habitacio_inicial, piramide_inicial, vides (already in `Options::game`, not exposed).
- **Multi-language**: add `data/locale/es.yaml` / `en.yaml` and a language selector item in menu. `Locale::load()` already handles arbitrary files.
@@ -251,6 +278,7 @@ JD8_Flip produces ABGR byte order: `0xFF000000 + R + (G<<8) + (B<<16)`. SDL text
- **Notification persistence**: notifications clear on each new one (`showNotification` does `notifications_.clear()`). Could queue instead.
- **FPS counter jitter**: time segment width changes per frame (100 Hz centi updates) causes ~1-2 px horizontal jitter in centered layout. Could lock to max-width or use monospace digits.
- **Notification messages partially hardcoded**: overlay/global_inputs/director now use Locale, but the window title (`Texts::WINDOW_TITLE`) and some game-layer strings remain hardcoded.
- **jail_audio `JA_Sound_t` RAII**: `JA_Music_t` ja està net (vector + string), però `JA_Sound_t` encara usa `Uint8*` via `SDL_LoadWAV` out-param. Petit polish per a completar la coherència RAII.
### Previously Fixed (kept for reference)

View File

@@ -80,8 +80,22 @@ set(APP_SOURCES
# Configuración de SDL3
# En macOS bundle mode usamos el xcframework (universal arm64+x86_64).
# En el resto de casos, o en macOS sin bundle, usamos SDL3 del sistema via find_package.
if(APPLE AND MACOS_BUNDLE)
# En emscripten compilamos SDL3 desde source con FetchContent (no hi ha paquet de sistema).
# En el resto de casos, usamos SDL3 del sistema via find_package.
if(EMSCRIPTEN)
include(FetchContent)
FetchContent_Declare(
SDL3
GIT_REPOSITORY https://github.com/libsdl-org/SDL.git
GIT_TAG release-3.4.4
GIT_SHALLOW TRUE
)
set(SDL_SHARED OFF CACHE BOOL "" FORCE)
set(SDL_STATIC ON CACHE BOOL "" FORCE)
set(SDL_TEST_LIBRARY OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(SDL3)
message(STATUS "SDL3: compilat des de source per a Emscripten (FetchContent)")
elseif(APPLE AND MACOS_BUNDLE)
set(SDL3_XCFRAMEWORK_SLICE "${CMAKE_SOURCE_DIR}/release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64")
message(STATUS "SDL3: usando xcframework (${SDL3_XCFRAMEWORK_SLICE})")
else()
@@ -89,8 +103,8 @@ else()
message(STATUS "SDL3 encontrado: ${SDL3_INCLUDE_DIRS}")
endif()
# --- COMPILACIÓ SHADERS SPIR-V (Linux/Windows — macOS usa Metal) ---
if(NOT APPLE)
# --- COMPILACIÓ SHADERS SPIR-V (Linux/Windows — macOS usa Metal, Emscripten no suporta SDL3 GPU) ---
if(NOT APPLE AND NOT EMSCRIPTEN)
find_program(GLSLC_EXE NAMES glslc)
set(SHADERS_DIR "${CMAKE_SOURCE_DIR}/data/shaders")
@@ -137,15 +151,25 @@ if(NOT APPLE)
endforeach()
message(STATUS "glslc no trobat — usant headers SPIR-V precompilats")
endif()
elseif(EMSCRIPTEN)
message(STATUS "Emscripten: shaders SPIR-V omesos (SDL3 GPU no suportat a WebGL2)")
else()
message(STATUS "macOS: shaders SPIR-V omesos (usa Metal)")
endif()
# --- EJECUTABLE ---
add_executable(${PROJECT_NAME} ${APP_SOURCES})
# A emscripten excloïm sdl3gpu_shader.cpp — SDL3 GPU no suporta WebGL2, i el
# fallback SDL_Renderer de Screen (amb NO_SHADERS) fa tota la presentació.
if(EMSCRIPTEN)
set(APP_SOURCES_WASM ${APP_SOURCES})
list(REMOVE_ITEM APP_SOURCES_WASM source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp)
add_executable(${PROJECT_NAME} ${APP_SOURCES_WASM})
else()
add_executable(${PROJECT_NAME} ${APP_SOURCES})
endif()
# Shaders han de compilar-se abans que l'executable (Linux/Windows amb glslc)
if(NOT APPLE AND GLSLC_EXE)
if(NOT APPLE AND NOT EMSCRIPTEN AND GLSLC_EXE)
add_dependencies(${PROJECT_NAME} shaders)
endif()
@@ -176,10 +200,30 @@ target_compile_options(${PROJECT_NAME} PRIVATE $<$<CONFIG:RELEASE>:-Os -ffunctio
# --- CONFIGURACIÓN POR PLATAFORMA ---
if(WIN32)
target_link_libraries(${PROJECT_NAME} PRIVATE mingw32)
elseif(EMSCRIPTEN)
target_compile_definitions(${PROJECT_NAME} PRIVATE EMSCRIPTEN_BUILD NO_SHADERS)
# -fexceptions: SDL3 i fkyaml llancen std::exception; sense això, `throw`
# acaba en `abort()`. També requerit al link per congruència ABI.
target_compile_options(${PROJECT_NAME} PRIVATE -fexceptions)
target_link_options(${PROJECT_NAME} PRIVATE
"SHELL:--preload-file ${CMAKE_SOURCE_DIR}/data@/data"
-fexceptions
-sALLOW_MEMORY_GROWTH=1
-sMAX_WEBGL_VERSION=2
-sINITIAL_MEMORY=67108864
-sASSERTIONS=1
# ASYNCIFY permet que Emscripten gestione yields durant la precarga
# d'assets. El main loop del joc ja usa SDL3 Callback API i no depén
# d'Asyncify — però el preloader del `.data` sí.
-sASYNCIFY=1
)
set_target_properties(${PROJECT_NAME} PROPERTIES SUFFIX ".html")
endif()
# Ejecutable en la raíz del proyecto
set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR})
# Ejecutable en la raíz del proyecto (solo nativos). A Emscripten queda dins build/.
if(NOT EMSCRIPTEN)
set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR})
endif()
# --- CLANG-FORMAT TARGETS ---
find_program(CLANG_FORMAT_EXE NAMES clang-format)

View File

@@ -217,6 +217,32 @@ _macos_release:
$(RMDIR) build/arm
$(RMFILE) "$(DIST_DIR)"/rw.*
# ==============================================================================
# COMPILACIÓN PARA WEBASSEMBLY (requiere Docker amb emscripten/emsdk)
# ==============================================================================
# Genera aee.{html,js,wasm,data} a dist/wasm/. Es pot provar servint amb un
# servidor HTTP local (els navegadors no carreguen `file://` WASM):
# cd dist/wasm && python3 -m http.server 8000
# # després obrir http://localhost:8000/aee.html
wasm:
@echo "Creando release para WebAssembly - Version: $(VERSION)"
docker run --rm \
--user $(shell id -u):$(shell id -g) \
-v $(DIR_ROOT):/src \
-w /src \
emscripten/emsdk:latest \
bash -c "emcmake cmake -S . -B build/wasm -DCMAKE_BUILD_TYPE=Release && cmake --build build/wasm"
@$(MKDIR) "$(DIST_DIR)/wasm"
@cp build/wasm/$(TARGET_NAME).html $(DIST_DIR)/wasm/
@cp build/wasm/$(TARGET_NAME).js $(DIST_DIR)/wasm/
@cp build/wasm/$(TARGET_NAME).wasm $(DIST_DIR)/wasm/
@cp build/wasm/$(TARGET_NAME).data $(DIST_DIR)/wasm/
@echo "Output: $(DIST_DIR)/wasm/$(TARGET_NAME).html"
scp $(DIST_DIR)/wasm/$(TARGET_NAME).js $(DIST_DIR)/wasm/$(TARGET_NAME).wasm $(DIST_DIR)/wasm/$(TARGET_NAME).data \
maverick:/home/sergio/gitea/web_jailgames/static/games/aee/wasm/
ssh maverick 'cd /home/sergio/gitea/web_jailgames && ./deploy.sh'
@echo "Deployed to maverick"
# ==============================================================================
# COMPILACIÓN PARA LINUX (RELEASE)
# ==============================================================================
@@ -247,4 +273,4 @@ _linux_release:
# Elimina la carpeta temporal
$(RMDIR) "$(RELEASE_FOLDER)"
.PHONY: all debug release _windows_release _linux_release _macos_release
.PHONY: all debug release wasm _windows_release _linux_release _macos_release

View File

@@ -7,12 +7,22 @@
#include <stdlib.h>
#include <string.h>
#include <memory>
#include <string>
#include <vector>
#define STB_VORBIS_HEADER_ONLY
#include "external/stb_vorbis.h"
// Deleter stateless per a buffers reservats amb `SDL_malloc` / `SDL_LoadWAV*`.
// Compatible amb `std::unique_ptr<Uint8[], SDLFreeDeleter>` — zero size
// overhead gràcies a EBO, igual que un unique_ptr amb default_delete.
struct SDLFreeDeleter {
void operator()(Uint8* p) const noexcept {
if (p) SDL_free(p);
}
};
// --- Public Enums ---
enum JA_Channel_state {
JA_CHANNEL_INVALID,
@@ -36,7 +46,9 @@ enum JA_Music_state {
struct JA_Sound_t {
SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000};
Uint32 length{0};
Uint8* buffer{nullptr};
// Buffer descomprimit (PCM) propietat del sound. Reservat per SDL_LoadWAV
// via SDL_malloc; el deleter `SDLFreeDeleter` allibera amb SDL_free.
std::unique_ptr<Uint8[], SDLFreeDeleter> buffer;
};
struct JA_Channel_t {
@@ -172,7 +184,7 @@ inline void JA_Update() {
if (channels[i].state == JA_CHANNEL_PLAYING) {
if (channels[i].times != 0) {
if ((Uint32)SDL_GetAudioStreamAvailable(channels[i].stream) < (channels[i].sound->length / 2)) {
SDL_PutAudioStreamData(channels[i].stream, channels[i].sound->buffer, channels[i].sound->length);
SDL_PutAudioStreamData(channels[i].stream, channels[i].sound->buffer.get(), channels[i].sound->length);
if (channels[i].times > 0) channels[i].times--;
}
} else {
@@ -355,31 +367,26 @@ inline void JA_EnableMusic(const bool value) {
// --- Sound Functions ---
inline JA_Sound_t* JA_NewSound(Uint8* buffer, Uint32 length) {
JA_Sound_t* sound = new JA_Sound_t();
sound->buffer = buffer;
sound->length = length;
return sound;
}
inline JA_Sound_t* JA_LoadSound(uint8_t* buffer, uint32_t size) {
JA_Sound_t* sound = new JA_Sound_t();
if (!SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), 1, &sound->spec, &sound->buffer, &sound->length)) {
auto sound = std::make_unique<JA_Sound_t>();
Uint8* raw = nullptr;
if (!SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), 1, &sound->spec, &raw, &sound->length)) {
SDL_Log("Failed to load WAV from memory: %s", SDL_GetError());
delete sound;
return nullptr;
}
return sound;
sound->buffer.reset(raw); // adopta el SDL_malloc'd buffer
return sound.release();
}
inline JA_Sound_t* JA_LoadSound(const char* filename) {
JA_Sound_t* sound = new JA_Sound_t();
if (!SDL_LoadWAV(filename, &sound->spec, &sound->buffer, &sound->length)) {
auto sound = std::make_unique<JA_Sound_t>();
Uint8* raw = nullptr;
if (!SDL_LoadWAV(filename, &sound->spec, &raw, &sound->length)) {
SDL_Log("Failed to load WAV file: %s", SDL_GetError());
delete sound;
return nullptr;
}
return sound;
sound->buffer.reset(raw); // adopta el SDL_malloc'd buffer
return sound.release();
}
inline int JA_PlaySound(JA_Sound_t* sound, const int loop = 0, const int group = 0) {
@@ -411,7 +418,7 @@ inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int
return -1;
}
SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer, channels[channel].sound->length);
SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer.get(), channels[channel].sound->length);
SDL_SetAudioStreamGain(channels[channel].stream, JA_soundVolume[group]);
SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream);
@@ -423,7 +430,7 @@ inline void JA_DeleteSound(JA_Sound_t* sound) {
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
if (channels[i].sound == sound) JA_StopChannel(i);
}
SDL_free(sound->buffer);
// buffer es destrueix automàticament via RAII (SDLFreeDeleter).
delete sound;
}

View File

@@ -161,18 +161,31 @@ std::vector<char> file_readfile(const char* resourcename) {
// Accepta rutes amb subdirectoris (ex: "jailgames/aee") i crea tota la jerarquia.
void file_setconfigfolder(const char* foldername) {
#ifdef _WIN32
config_folder = std::string(getenv("APPDATA")) + "/" + foldername;
const char* base = getenv("APPDATA");
if (!base) base = "C:/";
config_folder = std::string(base) + "/" + foldername;
#elif __APPLE__
struct passwd* pw = getpwuid(getuid());
const char* homedir = pw->pw_dir;
const char* homedir = (pw && pw->pw_dir) ? pw->pw_dir : nullptr;
if (!homedir) homedir = getenv("HOME");
if (!homedir) homedir = "/tmp";
config_folder = std::string(homedir) + "/Library/Application Support/" + foldername;
#elif __linux__
// Nota emscripten: `__linux__` també està definit, però `getpwuid` no
// troba cap /etc/passwd al MEMFS i retorna nullptr. Amb els fallbacks
// HOME → /tmp evitem crashejar al primer arranque dins del navegador.
// La config no persistirà entre recàrregues de la pàgina (MEMFS és
// volàtil); caldria IDBFS si volguéssem persistència a web.
struct passwd* pw = getpwuid(getuid());
const char* homedir = pw->pw_dir;
const char* homedir = (pw && pw->pw_dir) ? pw->pw_dir : nullptr;
if (!homedir) homedir = getenv("HOME");
if (!homedir) homedir = "/tmp";
config_folder = std::string(homedir) + "/.config/" + foldername;
#endif
std::filesystem::create_directories(config_folder);
if (!config_folder.empty()) {
std::filesystem::create_directories(config_folder);
}
}
const char* file_getconfigfolder() {

View File

@@ -5,7 +5,9 @@
#include "core/locale/locale.hpp"
#include "core/rendering/overlay.hpp"
#ifndef NO_SHADERS
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp"
#endif
#include "game/defines.hpp"
#include "game/options.hpp"
#include "utils/utils.hpp"
@@ -56,10 +58,12 @@ Screen::~Screen() {
Options::window.zoom = zoom_;
Options::window.fullscreen = fullscreen_;
// Destrueix el backend GPU
// Destrueix el backend GPU (només existeix si s'ha compilat amb shaders)
if (shader_backend_) {
#ifndef NO_SHADERS
auto* gpu = dynamic_cast<Rendering::SDL3GPUShader*>(shader_backend_.get());
if (gpu) gpu->destroy();
#endif
shader_backend_.reset();
}
@@ -69,6 +73,13 @@ Screen::~Screen() {
}
void Screen::initShaders() {
#ifdef NO_SHADERS
// Build sense shaders (p.ex. emscripten/WebGL2, on SDL3 GPU no està
// disponible). Es salta tota la inicialització — shader_backend_ es
// queda nul·lptr i tots els `if (shader_backend_)` del render path
// curtcircuiten cap al fallback SDL_Renderer.
return;
#else
if (!Options::video.gpu_acceleration) return;
shader_backend_ = std::make_unique<Rendering::SDL3GPUShader>();
@@ -122,6 +133,7 @@ void Screen::initShaders() {
applyCurrentPostFXPreset();
applyCurrentCrtPiPreset();
#endif
}
void Screen::present(Uint32* pixel_data) {