fase 4+5: fibers cooperatius substitueixen el game thread, sense mutex ni cv
This commit is contained in:
56
CLAUDE.md
56
CLAUDE.md
@@ -131,7 +131,7 @@ Follows the pattern from `jaildoctors_dilemma`, persists to YAML:
|
|||||||
| F8 | Cycle shader presets |
|
| F8 | Cycle shader presets |
|
||||||
| F9 | Toggle stretch filter (nearest ↔ linear) |
|
| F9 | Toggle stretch filter (nearest ↔ linear) |
|
||||||
| F10 | Cycle render info (off → top → bottom → off) |
|
| 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 |
|
| F12 | Toggle floating options menu |
|
||||||
| ESC | Double-press to quit (with overlay notification) / close menu if open |
|
| ESC | Double-press to quit (with overlay notification) / close menu if open |
|
||||||
| Backspace | Go up one menu level / close menu if at root |
|
| 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.
|
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)
|
### Execution Model (Single-threaded Fibers)
|
||||||
|
|
||||||
> ⚠️ 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.
|
|
||||||
|
|
||||||
|
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())
|
Main thread (only thread)
|
||||||
──────────────────── ────────────────────────────────────
|
─────────────────────────
|
||||||
loop at ~60 FPS { loop {
|
Director::run() loop {
|
||||||
SDL_PollEvent() ... game logic ...
|
SDL_PollEvent()
|
||||||
GlobalInputs, Mouse JD8_Flip():
|
GlobalInputs, Mouse, KeyRemap
|
||||||
if new_frame_available: palette→ARGB in pixel_data
|
JA_Update() ← audio pump
|
||||||
copy to game_frame publishFrame(pixel_data) ⏸
|
if !paused:
|
||||||
signal → ────────────────────→ (blocks until Director consumes)
|
GameFiber::resume() ← hands control to game code
|
||||||
copy game_frame → present_buffer ←──── signal_consumed
|
↓ (runs until next JD8_Flip)
|
||||||
Overlay::render(present_buffer) continue game loop
|
... game code runs ...
|
||||||
Screen::present(present_buffer) }
|
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
|
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:**
|
**Key points:**
|
||||||
- Director is NON-BLOCKING: if no new frame, re-presents the last one with fresh overlay
|
- Single-threaded: zero `std::thread`, zero `std::mutex`, zero `std::condition_variable`.
|
||||||
- Double buffer: `game_frame` (untouched copy from game) + `presentation_buffer` (regenerated with overlay each frame)
|
- `JD8_Flip()` is the natural sync point: it calls `GameFiber::yield()` instead of the old blocking `publishFrame`.
|
||||||
- Game thread pauses at `JD8_Flip()` waiting for Director — natural sync point
|
- 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.
|
||||||
- SDL events processed ONLY on main thread (SDL requirement)
|
- 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.
|
||||||
- `JI_Update()` no longer polls events — reads Director's state
|
- 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)
|
### 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).
|
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).
|
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.
|
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
|
### 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`.
|
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).
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ set(APP_SOURCES
|
|||||||
|
|
||||||
# Core - System (nova capa)
|
# Core - System (nova capa)
|
||||||
source/core/system/director.cpp
|
source/core/system/director.cpp
|
||||||
|
source/core/system/fiber.cpp
|
||||||
|
|
||||||
# Game
|
# Game
|
||||||
source/game/options.cpp
|
source/game/options.cpp
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
|
||||||
#include "core/jail/jfile.hpp"
|
#include "core/jail/jfile.hpp"
|
||||||
#include "core/system/director.hpp"
|
#include "core/system/fiber.hpp"
|
||||||
#if defined(__clang__)
|
#if defined(__clang__)
|
||||||
#pragma clang diagnostic push
|
#pragma clang diagnostic push
|
||||||
#pragma clang diagnostic ignored "-Wunused-but-set-variable"
|
#pragma clang diagnostic ignored "-Wunused-but-set-variable"
|
||||||
@@ -165,7 +165,14 @@ void JD8_Flip() {
|
|||||||
pixel_data[x + (y * 320)] = color;
|
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) {
|
void JD8_FreeSurface(JD8_Surface surface) {
|
||||||
|
|||||||
@@ -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);
|
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();
|
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);
|
void JD8_FreeSurface(JD8_Surface surface);
|
||||||
|
|
||||||
Uint8 JD8_GetPixel(JD8_Surface surface, int x, int y);
|
Uint8 JD8_GetPixel(JD8_Surface surface, int x, int y);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "core/jail/jgame.hpp"
|
#include "core/jail/jgame.hpp"
|
||||||
|
|
||||||
|
#include "core/system/fiber.hpp"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
bool quitting = false;
|
bool quitting = false;
|
||||||
@@ -39,6 +41,12 @@ bool JG_ShouldUpdate() {
|
|||||||
cycle_counter++;
|
cycle_counter++;
|
||||||
return true;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,14 @@
|
|||||||
#include "core/input/key_remap.hpp"
|
#include "core/input/key_remap.hpp"
|
||||||
#include "core/input/mouse.hpp"
|
#include "core/input/mouse.hpp"
|
||||||
#include "core/jail/jail_audio.hpp"
|
#include "core/jail/jail_audio.hpp"
|
||||||
|
#include "core/jail/jdraw8.hpp"
|
||||||
#include "core/jail/jgame.hpp"
|
#include "core/jail/jgame.hpp"
|
||||||
#include "core/jail/jinput.hpp"
|
#include "core/jail/jinput.hpp"
|
||||||
#include "core/locale/locale.hpp"
|
#include "core/locale/locale.hpp"
|
||||||
#include "core/rendering/menu.hpp"
|
#include "core/rendering/menu.hpp"
|
||||||
#include "core/rendering/overlay.hpp"
|
#include "core/rendering/overlay.hpp"
|
||||||
#include "core/rendering/screen.hpp"
|
#include "core/rendering/screen.hpp"
|
||||||
|
#include "core/system/fiber.hpp"
|
||||||
#include "game/info.hpp"
|
#include "game/info.hpp"
|
||||||
#include "game/modulegame.hpp"
|
#include "game/modulegame.hpp"
|
||||||
#include "game/modulesequence.hpp"
|
#include "game/modulesequence.hpp"
|
||||||
@@ -24,12 +26,57 @@ extern void JI_moveCheats(Uint8 new_key);
|
|||||||
|
|
||||||
Director* Director::instance_ = nullptr;
|
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() {
|
void Director::init() {
|
||||||
instance_ = new Director();
|
instance_ = new Director();
|
||||||
Gamepad::init();
|
Gamepad::init();
|
||||||
|
GameFiber::init(gameFiberEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Director::destroy() {
|
void Director::destroy() {
|
||||||
|
GameFiber::destroy();
|
||||||
Gamepad::destroy();
|
Gamepad::destroy();
|
||||||
delete instance_;
|
delete instance_;
|
||||||
instance_ = nullptr;
|
instance_ = nullptr;
|
||||||
@@ -49,11 +96,11 @@ void Director::togglePause() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Director::run() {
|
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
|
// 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 game_frame[320 * 200]{};
|
||||||
Uint32 presentation_buffer[320 * 200]{};
|
Uint32 presentation_buffer[320 * 200]{};
|
||||||
bool has_frame = false;
|
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_VSYNC = 16; // ~60 FPS amb VSync
|
||||||
constexpr Uint32 FRAME_MS_NO_VSYNC = 4; // ~250 FPS sense VSync (límit superior)
|
constexpr Uint32 FRAME_MS_NO_VSYNC = 4; // ~250 FPS sense VSync (límit superior)
|
||||||
|
|
||||||
// Bucle principal del director (no-bloquejant)
|
while (!GameFiber::is_done() && !quit_requested_) {
|
||||||
while (!game_thread_done_ && !quit_requested_) {
|
|
||||||
Uint32 frame_start = SDL_GetTicks();
|
Uint32 frame_start = SDL_GetTicks();
|
||||||
|
|
||||||
handleEvents();
|
handleEvents();
|
||||||
@@ -77,8 +123,7 @@ void Director::run() {
|
|||||||
JA_Update();
|
JA_Update();
|
||||||
|
|
||||||
// Dispara els crèdits cinematogràfics la primera vegada que el joc
|
// 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
|
// arriba al menú del títol (info::ctx.num_piramide == 0).
|
||||||
// atòmica d'un int global: race benigna, tard d'1 frame en el pitjor cas.
|
|
||||||
static bool credits_triggered = false;
|
static bool credits_triggered = false;
|
||||||
if (!credits_triggered && info::ctx.num_piramide == 0) {
|
if (!credits_triggered && info::ctx.num_piramide == 0) {
|
||||||
if (Options::game.show_title_credits) {
|
if (Options::game.show_title_credits) {
|
||||||
@@ -92,21 +137,15 @@ void Director::run() {
|
|||||||
esc_blocked_ = false;
|
esc_blocked_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Consumeix un frame nou si n'hi ha un disponible (no bloqueja).
|
// Cedeix el control al fiber del joc. Quan retorne (per un
|
||||||
// Si estem en pausa, no consumim: el game thread es queda bloquejat a publishFrame.
|
// JD8_Flip() dins del joc) tindrem un nou frame a pixel_data.
|
||||||
bool new_frame = false;
|
// Si estem en pausa, no executem el fiber: es queda congelat al
|
||||||
|
// seu últim yield i continuem presentant l'últim frame conegut.
|
||||||
if (!paused_) {
|
if (!paused_) {
|
||||||
std::lock_guard lock(mutex_);
|
GameFiber::resume();
|
||||||
if (frame_ready_ && latest_frame_ != nullptr) {
|
if (GameFiber::is_done()) break;
|
||||||
memcpy(game_frame, latest_frame_, sizeof(game_frame));
|
memcpy(game_frame, JD8_GetFramebuffer(), sizeof(game_frame));
|
||||||
frame_ready_ = false;
|
|
||||||
frame_consumed_ = true;
|
|
||||||
has_frame = true;
|
has_frame = true;
|
||||||
new_frame = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (new_frame) {
|
|
||||||
frame_consumed_cv_.notify_one(); // desbloqueja el joc
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Presenta sempre: parteix del frame net del joc, afegeix overlay i envia
|
// 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)
|
// Si el joc encara no ha acabat (p.ex. eixida per ESC doble-press),
|
||||||
quit_requested_ = true;
|
// 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();
|
JG_QuitSignal();
|
||||||
{
|
while (!GameFiber::is_done()) {
|
||||||
std::lock_guard lock(mutex_);
|
GameFiber::resume();
|
||||||
frame_consumed_ = true;
|
|
||||||
}
|
|
||||||
frame_consumed_cv_.notify_all();
|
|
||||||
|
|
||||||
if (game_thread_.joinable()) {
|
|
||||||
game_thread_.join();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,10 +252,9 @@ void Director::handleEvents() {
|
|||||||
esc_blocked_ = false;
|
esc_blocked_ = false;
|
||||||
key_pressed_ = true;
|
key_pressed_ = true;
|
||||||
JG_QuitSignal();
|
JG_QuitSignal();
|
||||||
// Si estem en pausa, la desactivem (sense reprendre la música,
|
// Si estem en pausa, la desactivem: el fiber del joc està
|
||||||
// estem eixint): el game thread està bloquejat a publishFrame
|
// congelat i necessita ser reprès per veure la senyal de
|
||||||
// i necessita que Director consumeixca frames per despertar-lo
|
// quit i poder tornar de forma natural.
|
||||||
// i poder veure la senyal de quit.
|
|
||||||
paused_ = false;
|
paused_ = false;
|
||||||
}
|
}
|
||||||
continue; // no processa més aquest event
|
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() {
|
void Director::requestQuit() {
|
||||||
quit_requested_ = true;
|
quit_requested_ = true;
|
||||||
JG_QuitSignal();
|
JG_QuitSignal();
|
||||||
frame_consumed_cv_.notify_all();
|
|
||||||
frame_produced_cv_.notify_all();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Director::consumeKeyPressed() -> bool {
|
auto Director::consumeKeyPressed() -> bool {
|
||||||
return key_pressed_.exchange(false);
|
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();
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,15 +3,14 @@
|
|||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <condition_variable>
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <mutex>
|
|
||||||
#include <thread>
|
|
||||||
|
|
||||||
// El Director és el thread principal que controla la presentació i els inputs.
|
// 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 codi del joc s'executa dins d'un *fiber* cooperatiu (veure fiber.hpp):
|
||||||
// el joc produeix un frame, es bloqueja a JD8_Flip(), i el director el presenta
|
// el joc produeix un frame, crida JD8_Flip() que internament fa yield al
|
||||||
// abans de donar-li via per produir el següent.
|
// 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 {
|
class Director {
|
||||||
public:
|
public:
|
||||||
static void init();
|
static void init();
|
||||||
@@ -21,10 +20,6 @@ class Director {
|
|||||||
// Bucle principal del director. Crida des de main().
|
// Bucle principal del director. Crida des de main().
|
||||||
void run();
|
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)
|
// Demana l'eixida (ex: segona pulsació d'ESC o SDL_QUIT)
|
||||||
void requestQuit();
|
void requestQuit();
|
||||||
|
|
||||||
@@ -34,7 +29,8 @@ class Director {
|
|||||||
// Indica si ESC està bloquejada (el joc no l'ha de veure)
|
// Indica si ESC està bloquejada (el joc no l'ha de veure)
|
||||||
auto isEscBlocked() const -> bool { return esc_blocked_ || esc_swallow_until_release_; }
|
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();
|
void togglePause();
|
||||||
auto isPaused() const -> bool { return paused_; }
|
auto isPaused() const -> bool { return paused_; }
|
||||||
|
|
||||||
@@ -44,20 +40,9 @@ class Director {
|
|||||||
|
|
||||||
static Director* instance_;
|
static Director* instance_;
|
||||||
|
|
||||||
void gameThreadFunc();
|
|
||||||
void handleEvents();
|
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<bool> quit_requested_{false};
|
std::atomic<bool> quit_requested_{false};
|
||||||
std::atomic<bool> game_thread_done_{false};
|
|
||||||
std::atomic<bool> key_pressed_{false};
|
std::atomic<bool> key_pressed_{false};
|
||||||
std::atomic<bool> esc_blocked_{false};
|
std::atomic<bool> esc_blocked_{false};
|
||||||
std::atomic<bool> paused_{false};
|
std::atomic<bool> paused_{false};
|
||||||
|
|||||||
141
source/core/system/fiber.cpp
Normal file
141
source/core/system/fiber.cpp
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
#include "core/system/fiber.hpp"
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
#include <windows.h>
|
||||||
|
#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 <ucontext.h>
|
||||||
|
#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
|
||||||
36
source/core/system/fiber.hpp
Normal file
36
source/core/system/fiber.hpp
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
// 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
|
||||||
Reference in New Issue
Block a user