From 1507a1c7401e7312ba873a4f2322f4cdf512de3f Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Wed, 15 Apr 2026 18:50:43 +0200 Subject: [PATCH] fase 4+5: fibers cooperatius substitueixen el game thread, sense mutex ni cv --- CLAUDE.md | 56 ++++++----- CMakeLists.txt | 1 + source/core/jail/jdraw8.cpp | 11 ++- source/core/jail/jdraw8.hpp | 8 ++ source/core/jail/jgame.cpp | 8 ++ source/core/system/director.cpp | 164 ++++++++++++++------------------ source/core/system/director.hpp | 29 ++---- source/core/system/fiber.cpp | 141 +++++++++++++++++++++++++++ source/core/system/fiber.hpp | 36 +++++++ 9 files changed, 313 insertions(+), 141 deletions(-) create mode 100644 source/core/system/fiber.cpp create mode 100644 source/core/system/fiber.hpp diff --git a/CLAUDE.md b/CLAUDE.md index 515471a..f1b7f0b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -131,7 +131,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 (blocks game thread at publishFrame + `JA_PauseMusic`/`JA_ResumeMusic`) | +| F11 | Toggle pause (Director stops resuming the game fiber + `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 | @@ -139,33 +139,45 @@ 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. -### Threading Model (Emulator Architecture — transitional) - -> ⚠️ This architecture is **transitional**. It will be dismantled in Phase 5 of the modernization plan (the game thread + `publishFrame` mutex/cv disappear) and replaced by a single-threaded `SDL_AppIterate` loop in Phase 7. Document changes here when that happens. +### Execution Model (Single-threaded Fibers) +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. ``` -Main thread (Director) Game thread (ModuleGame/Sequence::Go()) -──────────────────── ──────────────────────────────────── -loop at ~60 FPS { loop { - SDL_PollEvent() ... game logic ... - GlobalInputs, Mouse JD8_Flip(): - if new_frame_available: palette→ARGB in pixel_data - copy to game_frame publishFrame(pixel_data) ⏸ - signal → ────────────────────→ (blocks until Director consumes) - copy game_frame → present_buffer ←──── signal_consumed - Overlay::render(present_buffer) continue game loop - Screen::present(present_buffer) } +Main thread (only thread) +───────────────────────── +Director::run() loop { + SDL_PollEvent() + GlobalInputs, Mouse, KeyRemap + 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 + memcpy game_frame → presentation_buffer + Overlay::render(presentation_buffer) + Screen::present(presentation_buffer) SDL_Delay to hit 60fps } ``` +**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:** -- Director is NON-BLOCKING: if no new frame, re-presents the last one with fresh overlay -- Double buffer: `game_frame` (untouched copy from game) + `presentation_buffer` (regenerated with overlay each frame) -- Game thread pauses at `JD8_Flip()` waiting for Director — natural sync point -- SDL events processed ONLY on main thread (SDL requirement) -- `JI_Update()` no longer polls events — reads Director's state +- 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). ### Rendering Pipeline (inside Screen::present) @@ -217,7 +229,7 @@ JD8_Flip produces ABGR byte order: `0xFF000000 + R + (G<<8) + (B<<16)`. SDL text 4. **Raw `malloc`/`free` in gameplay structs**: `Sprite`/`Entitat` use `malloc` for `Frame[]` and `Animacio[]`; `jfile.cpp` uses a global `scratch[255]` buffer (UB under concurrent calls); `jail_audio.cpp` mixes `new`/`malloc`/`SDL_malloc`. Scheduled for Phase 1 (RAII pass). 5. **Blocking loops in cinematics and fades**: `ModuleSequence::doIntro()` has 15+ `while(!JG_ShouldUpdate())` spin-waits; `JD8_FadeOut`/`JD8_FadeToPal` run 32 internal iterations calling `JD8_Flip`. Incompatible with `SDL_AppIterate`. Scheduled for Phase 2 (state-machine refactor). 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**: the emulator-style architecture is only tenable while native. Incompatible with `SDL_AppIterate`. Scheduled for Phase 5 (single-threaded state machine). +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. ### Pending / Ideas for Later @@ -249,4 +261,4 @@ Director loop uses `FRAME_MS_VSYNC = 16` (60 FPS) or `FRAME_MS_NO_VSYNC = 4` (~2 Init order: `file_setconfigfolder` → `Options::load` → `Locale::load("locale/ca.yaml")` → `Options::loadPostFX/CrtPi` → `JG_Init` → `Screen::init` → `JD8_Init` → `JA_Init` → `Options::applyAudio()` → `Overlay::init` → `Menu::init` → `Director::init` (also calls `Gamepad::init()`) → `Director::run()` (blocks until quit). Shutdown: `Options::save` → `Director::destroy` → `Menu::destroy` → `Overlay::destroy` → `JA_Quit` → `JD8_Quit` → `Screen::destroy` → `JG_Finalize`. -The state machine (alternating `ModuleSequence` state=1 and `ModuleGame` state=0) now lives inside `Director::gameThreadFunc()`, running on the game thread. +The state machine (alternating `ModuleSequence` state=1 and `ModuleGame` state=0) lives inside `gameFiberEntry()` in an anonymous namespace in [director.cpp](source/core/system/director.cpp), invoked as the entry of the cooperative fiber (single-threaded). diff --git a/CMakeLists.txt b/CMakeLists.txt index db190e7..d919e8f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,6 +39,7 @@ set(APP_SOURCES # Core - System (nova capa) source/core/system/director.cpp + source/core/system/fiber.cpp # Game source/game/options.cpp diff --git a/source/core/jail/jdraw8.cpp b/source/core/jail/jdraw8.cpp index ad85bf4..09febce 100644 --- a/source/core/jail/jdraw8.cpp +++ b/source/core/jail/jdraw8.cpp @@ -3,7 +3,7 @@ #include #include "core/jail/jfile.hpp" -#include "core/system/director.hpp" +#include "core/system/fiber.hpp" #if defined(__clang__) #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-but-set-variable" @@ -165,7 +165,14 @@ void JD8_Flip() { pixel_data[x + (y * 320)] = color; } } - Director::get()->publishFrame(pixel_data); + // Cedeix el control al Director. Quan Director::run() ens torne a fer + // resume(), continuarem just ací i el joc continuarà amb la següent + // iteració del seu loop sense bloquejos de mutex/cv. + GameFiber::yield(); +} + +Uint32* JD8_GetFramebuffer() { + return pixel_data; } void JD8_FreeSurface(JD8_Surface surface) { diff --git a/source/core/jail/jdraw8.hpp b/source/core/jail/jdraw8.hpp index fa4512a..1d5aa07 100644 --- a/source/core/jail/jdraw8.hpp +++ b/source/core/jail/jdraw8.hpp @@ -40,8 +40,16 @@ void JD8_BlitCKScroll(int y, JD8_Surface surface, int sx, int sy, int sh, Uint8 void JD8_BlitCKToSurface(int x, int y, JD8_Surface surface, int sx, int sy, int sw, int sh, JD8_Surface dest, Uint8 colorkey); +// Converteix la pantalla indexada a ARGB i cedeix el control al Director +// (GameFiber::yield). El Director llegirà el framebuffer convertit via +// JD8_GetFramebuffer() i tornarà a cridar Fiber::resume() quan toque el +// pròxim frame. void JD8_Flip(); +// Accés al framebuffer ARGB de 320x200 actualitzat per l'última crida a +// JD8_Flip(). Propietat de jdraw8 — el caller no ha de lliberar-lo. +Uint32* JD8_GetFramebuffer(); + void JD8_FreeSurface(JD8_Surface surface); Uint8 JD8_GetPixel(JD8_Surface surface, int x, int y); diff --git a/source/core/jail/jgame.cpp b/source/core/jail/jgame.cpp index 00031c4..6e8673d 100644 --- a/source/core/jail/jgame.cpp +++ b/source/core/jail/jgame.cpp @@ -1,5 +1,7 @@ #include "core/jail/jgame.hpp" +#include "core/system/fiber.hpp" + namespace { bool quitting = false; @@ -39,6 +41,12 @@ bool JG_ShouldUpdate() { cycle_counter++; return true; } + // Encara no toca update: cedim el control al Director per a que puga + // processar events, animar l'overlay i mantindre l'àudio viu. Sense + // aquest yield, els spin-waits típics de les cinemàtiques + // (`while (!JG_ShouldUpdate()) { JI_Update(); ... }`) congelarien + // tot el main loop — el fiber no cediria mai. + GameFiber::yield(); return false; } diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index 347d6fc..6e1ecb1 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -8,12 +8,14 @@ #include "core/input/key_remap.hpp" #include "core/input/mouse.hpp" #include "core/jail/jail_audio.hpp" +#include "core/jail/jdraw8.hpp" #include "core/jail/jgame.hpp" #include "core/jail/jinput.hpp" #include "core/locale/locale.hpp" #include "core/rendering/menu.hpp" #include "core/rendering/overlay.hpp" #include "core/rendering/screen.hpp" +#include "core/system/fiber.hpp" #include "game/info.hpp" #include "game/modulegame.hpp" #include "game/modulesequence.hpp" @@ -24,12 +26,57 @@ extern void JI_moveCheats(Uint8 new_key); Director* Director::instance_ = nullptr; +namespace { + +// Entry del fiber del joc. Executa la màquina d'estats que alterna entre +// ModuleSequence (state=1) i ModuleGame (state=0) fins que el joc demana +// eixir. Quan el joc crida JD8_Flip() des de dins d'aquest fiber, el +// control torna automàticament al Director. +void gameFiberEntry() { + info::ctx.num_habitacio = Options::game.habitacio_inicial; + info::ctx.num_piramide = Options::game.piramide_inicial; + info::ctx.diners = 0; + info::ctx.diamants = 0; + info::ctx.vida = Options::game.vides; + info::ctx.momies = 0; + info::ctx.nou_personatge = false; + info::ctx.pepe_activat = false; + + FILE* ini = fopen("trick.ini", "rb"); + if (ini != nullptr) { + info::ctx.nou_personatge = true; + fclose(ini); + } + + int gameState = 1; + while (gameState != -1 && !JG_Quitting()) { + switch (gameState) { + case 0: { + auto* moduleGame = new ModuleGame(); + gameState = moduleGame->Go(); + delete moduleGame; + break; + } + case 1: { + auto* moduleSequence = new ModuleSequence(); + gameState = moduleSequence->Go(); + delete moduleSequence; + break; + } + } + } +} + +} // namespace + void Director::init() { instance_ = new Director(); Gamepad::init(); + GameFiber::init(gameFiberEntry); } void Director::destroy() { + GameFiber::destroy(); Gamepad::destroy(); delete instance_; instance_ = nullptr; @@ -49,11 +96,11 @@ void Director::togglePause() { } void Director::run() { - // Llança el game thread - game_thread_ = std::thread(&Director::gameThreadFunc, this); - // Doble buffer: game_frame és el frame net del joc, presentation_buffer - // és el frame + overlay (es regenera cada iteració des de game_frame) + // és el frame + overlay (es regenera cada iteració des de game_frame). + // El doble buffer encara té sentit perquè el Director pot presentar + // més frames que els que genera el joc (p.ex. durant pauses o mentre + // el joc està en un "wait" intern que triga milisegons). Uint32 game_frame[320 * 200]{}; Uint32 presentation_buffer[320 * 200]{}; bool has_frame = false; @@ -61,8 +108,7 @@ void Director::run() { constexpr Uint32 FRAME_MS_VSYNC = 16; // ~60 FPS amb VSync constexpr Uint32 FRAME_MS_NO_VSYNC = 4; // ~250 FPS sense VSync (límit superior) - // Bucle principal del director (no-bloquejant) - while (!game_thread_done_ && !quit_requested_) { + while (!GameFiber::is_done() && !quit_requested_) { Uint32 frame_start = SDL_GetTicks(); handleEvents(); @@ -77,8 +123,7 @@ void Director::run() { JA_Update(); // Dispara els crèdits cinematogràfics la primera vegada que el joc - // arriba al menú del títol (info::ctx.num_piramide == 0). Lectura no - // atòmica d'un int global: race benigna, tard d'1 frame en el pitjor cas. + // arriba al menú del títol (info::ctx.num_piramide == 0). static bool credits_triggered = false; if (!credits_triggered && info::ctx.num_piramide == 0) { if (Options::game.show_title_credits) { @@ -92,21 +137,15 @@ void Director::run() { esc_blocked_ = false; } - // Consumeix un frame nou si n'hi ha un disponible (no bloqueja). - // Si estem en pausa, no consumim: el game thread es queda bloquejat a publishFrame. - bool new_frame = false; + // Cedeix el control al fiber del joc. Quan retorne (per un + // JD8_Flip() dins del joc) tindrem un nou frame a pixel_data. + // Si estem en pausa, no executem el fiber: es queda congelat al + // seu últim yield i continuem presentant l'últim frame conegut. if (!paused_) { - std::lock_guard lock(mutex_); - if (frame_ready_ && latest_frame_ != nullptr) { - memcpy(game_frame, latest_frame_, sizeof(game_frame)); - frame_ready_ = false; - frame_consumed_ = true; - has_frame = true; - new_frame = true; - } - } - if (new_frame) { - frame_consumed_cv_.notify_one(); // desbloqueja el joc + GameFiber::resume(); + if (GameFiber::is_done()) break; + memcpy(game_frame, JD8_GetFramebuffer(), sizeof(game_frame)); + has_frame = true; } // Presenta sempre: parteix del frame net del joc, afegeix overlay i envia @@ -123,17 +162,12 @@ void Director::run() { } } - // Assegura que el game thread ix (despertar-lo per si està esperant) - quit_requested_ = true; + // Si el joc encara no ha acabat (p.ex. eixida per ESC doble-press), + // li donem l'oportunitat de tornar net: marquem quit i reprenem el + // fiber fins que detecte JG_Quitting() i retorne de forma natural. JG_QuitSignal(); - { - std::lock_guard lock(mutex_); - frame_consumed_ = true; - } - frame_consumed_cv_.notify_all(); - - if (game_thread_.joinable()) { - game_thread_.join(); + while (!GameFiber::is_done()) { + GameFiber::resume(); } } @@ -218,10 +252,9 @@ void Director::handleEvents() { esc_blocked_ = false; key_pressed_ = true; JG_QuitSignal(); - // Si estem en pausa, la desactivem (sense reprendre la música, - // estem eixint): el game thread està bloquejat a publishFrame - // i necessita que Director consumeixca frames per despertar-lo - // i poder veure la senyal de quit. + // Si estem en pausa, la desactivem: el fiber del joc està + // congelat i necessita ser reprès per veure la senyal de + // quit i poder tornar de forma natural. paused_ = false; } continue; // no processa més aquest event @@ -254,70 +287,11 @@ void Director::handleEvents() { } } -void Director::publishFrame(Uint32* pixels) { - { - std::lock_guard lock(mutex_); - latest_frame_ = pixels; - frame_ready_ = true; - frame_consumed_ = false; - } - frame_produced_cv_.notify_one(); - - // Espera que el director consumeixca el frame - { - std::unique_lock lock(mutex_); - frame_consumed_cv_.wait(lock, [this] { - return frame_consumed_ || quit_requested_; - }); - } -} - void Director::requestQuit() { quit_requested_ = true; JG_QuitSignal(); - frame_consumed_cv_.notify_all(); - frame_produced_cv_.notify_all(); } auto Director::consumeKeyPressed() -> bool { return key_pressed_.exchange(false); } - -void Director::gameThreadFunc() { - info::ctx.num_habitacio = Options::game.habitacio_inicial; - info::ctx.num_piramide = Options::game.piramide_inicial; - info::ctx.diners = 0; - info::ctx.diamants = 0; - info::ctx.vida = Options::game.vides; - info::ctx.momies = 0; - info::ctx.nou_personatge = false; - info::ctx.pepe_activat = false; - - FILE* ini = fopen("trick.ini", "rb"); - if (ini != nullptr) { - info::ctx.nou_personatge = true; - fclose(ini); - } - - int gameState = 1; - while (gameState != -1 && !quit_requested_) { - switch (gameState) { - case 0: { - auto* moduleGame = new ModuleGame(); - gameState = moduleGame->Go(); - delete moduleGame; - break; - } - case 1: { - auto* moduleSequence = new ModuleSequence(); - gameState = moduleSequence->Go(); - delete moduleSequence; - break; - } - } - } - - game_thread_done_ = true; - // Despertar el director per si esperava un frame - frame_produced_cv_.notify_all(); -} diff --git a/source/core/system/director.hpp b/source/core/system/director.hpp index a72dc7b..eb7f068 100644 --- a/source/core/system/director.hpp +++ b/source/core/system/director.hpp @@ -3,15 +3,14 @@ #include #include -#include #include -#include -#include // El Director és el thread principal que controla la presentació i els inputs. -// Executa el joc en un thread secundari (game thread) com si fos una "fibra emulada": -// el joc produeix un frame, es bloqueja a JD8_Flip(), i el director el presenta -// abans de donar-li via per produir el següent. +// El codi del joc s'executa dins d'un *fiber* cooperatiu (veure fiber.hpp): +// el joc produeix un frame, crida JD8_Flip() que internament fa yield al +// Director, i el Director el presenta abans de tornar-lo a reprendre amb +// GameFiber::resume(). Tot ocorre en un únic thread — sense mutex, sense +// condition_variable, compatible amb el futur port a SDL_AppIterate. class Director { public: static void init(); @@ -21,10 +20,6 @@ class Director { // Bucle principal del director. Crida des de main(). void run(); - // Invocat pel game thread des de JD8_Flip(). Bloqueja fins que el director - // consumeix el frame i dona via per produir el següent. - void publishFrame(Uint32* pixels); - // Demana l'eixida (ex: segona pulsació d'ESC o SDL_QUIT) void requestQuit(); @@ -34,7 +29,8 @@ class Director { // Indica si ESC està bloquejada (el joc no l'ha de veure) auto isEscBlocked() const -> bool { return esc_blocked_ || esc_swallow_until_release_; } - // Pausa: bloqueja el consum de frames del game thread + pausa la música + // Pausa: mentre està activa, Director no fa resume() del fiber del joc, + // així que el joc queda congelat al seu últim JD8_Flip. void togglePause(); auto isPaused() const -> bool { return paused_; } @@ -44,20 +40,9 @@ class Director { static Director* instance_; - void gameThreadFunc(); void handleEvents(); - std::thread game_thread_; - std::mutex mutex_; - std::condition_variable frame_produced_cv_; - std::condition_variable frame_consumed_cv_; - - Uint32* latest_frame_{nullptr}; - bool frame_ready_{false}; - bool frame_consumed_{true}; - std::atomic quit_requested_{false}; - std::atomic game_thread_done_{false}; std::atomic key_pressed_{false}; std::atomic esc_blocked_{false}; std::atomic paused_{false}; diff --git a/source/core/system/fiber.cpp b/source/core/system/fiber.cpp new file mode 100644 index 0000000..2eb3e48 --- /dev/null +++ b/source/core/system/fiber.cpp @@ -0,0 +1,141 @@ +#include "core/system/fiber.hpp" + +#include +#include +#include + +#if defined(_WIN32) +#define WIN32_LEAN_AND_MEAN +#include +#else +// ucontext_t està marcat com a obsolet a POSIX.1-2008 però continua +// funcional a glibc Linux i macOS. Si en el futur migrem a una alternativa +// (boost::context, makecontext personalitzat) només cal tocar aquest fitxer. +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +#elif defined(__GNUC__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif +#include +#endif + +namespace GameFiber { + +namespace { + +bool initialized_ = false; +bool done_ = false; +EntryFn entry_fn_ = nullptr; + +#if defined(_WIN32) + +LPVOID main_fiber_ = nullptr; +LPVOID game_fiber_ = nullptr; + +void __stdcall trampoline(void* /*param*/) { + if (entry_fn_) entry_fn_(); + done_ = true; + SwitchToFiber(main_fiber_); +} + +#else + +ucontext_t main_ctx_{}; +ucontext_t fiber_ctx_{}; +void* fiber_stack_ = nullptr; + +void trampoline() { + if (entry_fn_) entry_fn_(); + done_ = true; + // Retornar al main: uc_link apunta a main_ctx_ en init(). +} + +#endif + +} // namespace + +void init(EntryFn entry, std::size_t stack_size) { + if (initialized_) destroy(); + entry_fn_ = entry; + done_ = false; + +#if defined(_WIN32) + main_fiber_ = ConvertThreadToFiber(nullptr); + if (!main_fiber_) { + // Ja era un fiber (no sol passar en el main thread d'una app SDL). + main_fiber_ = GetCurrentFiber(); + } + game_fiber_ = CreateFiber(stack_size, trampoline, nullptr); + if (!game_fiber_) { + std::cerr << "GameFiber::init: CreateFiber failed\n"; + return; + } +#else + fiber_stack_ = std::malloc(stack_size); + if (!fiber_stack_) { + std::cerr << "GameFiber::init: malloc failed\n"; + return; + } + getcontext(&fiber_ctx_); + fiber_ctx_.uc_stack.ss_sp = fiber_stack_; + fiber_ctx_.uc_stack.ss_size = stack_size; + fiber_ctx_.uc_link = &main_ctx_; + makecontext(&fiber_ctx_, trampoline, 0); +#endif + + initialized_ = true; +} + +void destroy() { + if (!initialized_) return; +#if defined(_WIN32) + if (game_fiber_) { + DeleteFiber(game_fiber_); + game_fiber_ = nullptr; + } + // No desconvertim el main thread: SDL pot estar-ne pendent i ja no + // tornem a crear fibers en aquesta execució. ConvertFiberToThread() + // només cal si volguerem reutilitzar el main com a thread normal. +#else + if (fiber_stack_) { + std::free(fiber_stack_); + fiber_stack_ = nullptr; + } +#endif + initialized_ = false; + done_ = false; + entry_fn_ = nullptr; +} + +void resume() { + if (!initialized_ || done_) return; +#if defined(_WIN32) + SwitchToFiber(game_fiber_); +#else + swapcontext(&main_ctx_, &fiber_ctx_); +#endif +} + +void yield() { + if (!initialized_) return; +#if defined(_WIN32) + SwitchToFiber(main_fiber_); +#else + swapcontext(&fiber_ctx_, &main_ctx_); +#endif +} + +bool is_done() { return done_; } +bool is_initialized() { return initialized_; } + +} // namespace GameFiber + +#if !defined(_WIN32) +#if defined(__clang__) +#pragma clang diagnostic pop +#elif defined(__GNUC__) +#pragma GCC diagnostic pop +#endif +#endif diff --git a/source/core/system/fiber.hpp b/source/core/system/fiber.hpp new file mode 100644 index 0000000..8692676 --- /dev/null +++ b/source/core/system/fiber.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include + +// Fiber minimalista sobre el suport natiu del SO (ucontext_t en POSIX, +// Fibers API en Windows). Serveix per a implementar un yield/resume +// cooperatiu entre el Director i el codi del joc sense un std::thread +// ni mutex/condition_variable. Substituïx el bloqueig de publishFrame/ +// consumeFrame amb un mecanisme de control explícit. +// +// Contracte: +// - GameFiber::init(entry) prepara un fiber que executarà `entry` +// en un stack dedicat. No el comença a executar encara. +// - GameFiber::resume() cedeix el control al fiber. Retorna quan el +// fiber crida GameFiber::yield() o quan la funció entry retorna. +// - GameFiber::yield() es crida des de dins del fiber per a tornar +// el control al main (al punt just després de resume()). +// - GameFiber::is_done() indica si la funció entry ha retornat. +// - GameFiber::destroy() allibera el stack i reinicia l'estat. +// +// Per al port a emscripten (Fase 7) caldrà substituir aquesta capa per +// Asyncify, però el contracte públic pot romandre idèntic. +namespace GameFiber { + +using EntryFn = void (*)(); + +void init(EntryFn entry, std::size_t stack_size = 256 * 1024); +void destroy(); + +void resume(); +void yield(); + +bool is_done(); +bool is_initialized(); + +} // namespace GameFiber