diff --git a/CLAUDE.md b/CLAUDE.md index 9afdcf4..8e08171 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 0–7b 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 (0–7) i la migració `scenes::` (Steps 0–10) 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`, `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` i `std::vector` ([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` 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 1–9) 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 1–10) + `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) diff --git a/CMakeLists.txt b/CMakeLists.txt index b4bc3fd..bf94874 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 $<$:-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) diff --git a/Makefile b/Makefile index e260085..84b4989 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/source/core/jail/jail_audio.hpp b/source/core/jail/jail_audio.hpp index 0d35c0d..6c86727 100644 --- a/source/core/jail/jail_audio.hpp +++ b/source/core/jail/jail_audio.hpp @@ -7,12 +7,22 @@ #include #include +#include #include #include #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` — 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 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(); + 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(); + 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; } diff --git a/source/core/jail/jfile.cpp b/source/core/jail/jfile.cpp index 8e8dfcb..ae4a9a2 100644 --- a/source/core/jail/jfile.cpp +++ b/source/core/jail/jfile.cpp @@ -161,18 +161,31 @@ std::vector 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() { diff --git a/source/core/rendering/screen.cpp b/source/core/rendering/screen.cpp index 314d181..6701387 100644 --- a/source/core/rendering/screen.cpp +++ b/source/core/rendering/screen.cpp @@ -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(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(); @@ -122,6 +133,7 @@ void Screen::initShaders() { applyCurrentPostFXPreset(); applyCurrentCrtPiPreset(); +#endif } void Screen::present(Uint32* pixel_data) {