step B.2: elimina fiber — Director posseeix l'escena, JD8_Flip sense yield, fiber.hpp/cpp esborrats

This commit is contained in:
2026-04-16 11:14:48 +02:00
parent 4e18f83ec5
commit 96a3cf9ebc
8 changed files with 105 additions and 296 deletions

View File

@@ -39,7 +39,6 @@ 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
# Scenes (cinemàtiques i menús reescrits) # Scenes (cinemàtiques i menús reescrits)
source/scenes/timeline.cpp source/scenes/timeline.cpp

View File

@@ -3,7 +3,6 @@
#include <fstream> #include <fstream>
#include "core/jail/jfile.hpp" #include "core/jail/jfile.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"
@@ -151,16 +150,16 @@ void JD8_BlitCKToSurface(int x, int y, JD8_Surface surface, int sx, int sy, int
} }
void JD8_Flip() { void JD8_Flip() {
// Converteix el framebuffer indexat (paletted) a ARGB (pixel_data).
// El Director crida aquesta funció després del tick de cada escena
// per preparar el frame abans de presentar-lo. Ja no fa yield —
// tot corre en un sol thread sense fibers des de Phase B.2.
for (int x = 0; x < 320; x++) { for (int x = 0; x < 320; x++) {
for (int y = 0; y < 200; y++) { for (int y = 0; y < 200; y++) {
Uint32 color = 0xFF000000 + main_palette[screen[x + (y * 320)]].r + (main_palette[screen[x + (y * 320)]].g << 8) + (main_palette[screen[x + (y * 320)]].b << 16); Uint32 color = 0xFF000000 + main_palette[screen[x + (y * 320)]].r + (main_palette[screen[x + (y * 320)]].g << 8) + (main_palette[screen[x + (y * 320)]].b << 16);
pixel_data[x + (y * 320)] = color; pixel_data[x + (y * 320)] = color;
} }
} }
// 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() { Uint32* JD8_GetFramebuffer() {
@@ -254,20 +253,9 @@ bool JD8_FadeTickStep() {
return false; return false;
} }
void JD8_FadeOut() { // Els shims bloquejants `JD8_FadeOut` i `JD8_FadeToPal` han estat
JD8_FadeStartOut(); // eliminats a Phase B.2: feien un bucle de 32 iteracions amb `JD8_Flip`
while (true) { // entre cada una que només funcionava mentre l'entorn tenia fibers i
const bool done = JD8_FadeTickStep(); // `JD8_Flip` cedia el control al Director. Ara tot fade es fa tick a
JD8_Flip(); // tick via `scenes::PaletteFade` (que encapsula `JD8_FadeStartOut` /
if (done) break; // `JD8_FadeStartToPal` + `JD8_FadeTickStep`).
}
}
void JD8_FadeToPal(JD8_Palette pal) {
JD8_FadeStartToPal(pal);
while (true) {
const bool done = JD8_FadeTickStep();
JD8_Flip();
if (done) break;
}
}

View File

@@ -40,10 +40,9 @@ 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 // Converteix la pantalla indexada a ARGB. El Director crida aquesta
// (GameFiber::yield). El Director llegirà el framebuffer convertit via // funció al final de cada tick i després llegeix el framebuffer via
// JD8_GetFramebuffer() i tornarà a cridar Fiber::resume() quan toque el // JD8_GetFramebuffer() per presentar-lo.
// pròxim frame.
void JD8_Flip(); void JD8_Flip();
// Accés al framebuffer ARGB de 320x200 actualitzat per l'última crida a // Accés al framebuffer ARGB de 320x200 actualitzat per l'última crida a
@@ -58,16 +57,13 @@ void JD8_PutPixel(JD8_Surface surface, int x, int y, Uint8 pixel);
void JD8_SetPaletteColor(Uint8 index, Uint8 r, Uint8 g, Uint8 b); void JD8_SetPaletteColor(Uint8 index, Uint8 r, Uint8 g, Uint8 b);
// Fades legacy bloquejants (shim damunt la màquina d'estats de sota).
void JD8_FadeOut();
void JD8_FadeToPal(JD8_Palette pal);
// API de fade no bloquejant (màquina d'estats). `FadeStart*` inicia el // API de fade no bloquejant (màquina d'estats). `FadeStart*` inicia el
// fade; `FadeTickStep` aplica un pas i retorna `true` quan el fade ha // fade; `FadeTickStep` aplica un pas i retorna `true` quan el fade ha
// acabat. Un pas correspon visualment a una iteració del fade original // acabat. Un pas correspon visualment a una iteració del fade original
// (32 passos en total). El caller és responsable de fer el Flip entre // (32 passos en total). El caller és responsable de fer el Flip entre
// passos si el vol veure animat. `FadeIsActive` permet saber si hi ha // passos si el vol veure animat. `FadeIsActive` permet saber si hi ha
// un fade en curs per a enllaçar-lo amb un altre subsistema. // un fade en curs per a enllaçar-lo amb un altre subsistema.
// L'embolcall `scenes::PaletteFade` ho fa més idiomàtic per a escenes.
void JD8_FadeStartOut(); void JD8_FadeStartOut();
void JD8_FadeStartToPal(JD8_Palette pal); void JD8_FadeStartToPal(JD8_Palette pal);
bool JD8_FadeTickStep(); bool JD8_FadeTickStep();

View File

@@ -1,7 +1,5 @@
#include "core/jail/jgame.hpp" #include "core/jail/jgame.hpp"
#include "core/system/fiber.hpp"
namespace { namespace {
bool quitting = false; bool quitting = false;
@@ -41,12 +39,9 @@ bool JG_ShouldUpdate() {
cycle_counter++; cycle_counter++;
return true; return true;
} }
// Encara no toca update: cedim el control al Director per a que puga // No toca update — retornem false sense més. Des de Phase B.2 ja no
// processar events, animar l'overlay i mantindre l'àudio viu. Sense // hi ha fibers: cap caller fa spin-waits (`while (!JG_ShouldUpdate())`)
// aquest yield, els spin-waits típics de les cinemàtiques // i el Director pren el control del main loop frame a frame.
// (`while (!JG_ShouldUpdate()) { JI_Update(); ... }`) congelarien
// tot el main loop — el fiber no cediria mai.
GameFiber::yield();
return false; return false;
} }

View File

@@ -15,7 +15,6 @@
#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/options.hpp" #include "game/options.hpp"
@@ -35,14 +34,9 @@ extern void JI_moveCheats(Uint8 new_key);
Director* Director::instance_ = nullptr; Director* Director::instance_ = nullptr;
namespace { Director::~Director() = default;
// Entry del fiber del joc. Dispatcha a una escena segons l'estat actual: void Director::initGameContext() {
// `gameState == 0` → ModuleGame (gameplay), `gameState == 1` → una
// `scenes::Scene` del registry triada per `info::ctx.num_piramide`. Cada
// escena és tick-based; el JD8_Flip() entre ticks cedeix al Director
// via `GameFiber::yield()`.
void gameFiberEntry() {
info::ctx.num_habitacio = Options::game.habitacio_inicial; info::ctx.num_habitacio = Options::game.habitacio_inicial;
info::ctx.num_piramide = Options::game.piramide_inicial; info::ctx.num_piramide = Options::game.piramide_inicial;
info::ctx.diners = Options::game.diners_inicial; info::ctx.diners = Options::game.diners_inicial;
@@ -57,55 +51,31 @@ void gameFiberEntry() {
info::ctx.nou_personatge = true; info::ctx.nou_personatge = true;
fclose(ini); fclose(ini);
} }
int gameState = 1;
while (gameState != -1 && !JG_Quitting()) {
std::unique_ptr<scenes::Scene> scene;
if (gameState == 0) {
// Gameplay. ModuleGame és una scenes::Scene des de Phase A de
// la migració — mateix mini-loop tick+flip que la resta.
scene = std::make_unique<ModuleGame>();
} else {
// gameState == 1: dispatch al registry per num_piramide. El
// vell ModuleSequence::Go() feia aquest redirect al principi:
// si el jugador arriba a la Secreta (6) sense prou diners,
// salta als slides de fracàs (7) abans de buscar l'escena.
if (info::ctx.num_piramide == 6 && info::ctx.diners < 200) {
info::ctx.num_piramide = 7;
}
scene = scenes::SceneRegistry::instance().tryCreate(info::ctx.num_piramide);
}
if (!scene) {
// State no registrat — indica un bug del dispatcher o del
// registre d'escenes. Eixim ordenadament en lloc de cremar CPU.
break;
}
scene->onEnter();
Uint32 last = SDL_GetTicks();
while (!scene->done() && !JG_Quitting()) {
JI_Update(); // refresca key_pressed/any_key per a les escenes
const Uint32 now = SDL_GetTicks();
scene->tick(static_cast<int>(now - last));
last = now;
JD8_Flip(); // presenta i cedix al Director
}
gameState = scene->nextState();
}
} }
} // namespace std::unique_ptr<scenes::Scene> Director::createNextScene() {
if (game_state_ == 0) {
// Gameplay. ModuleGame és una scenes::Scene des de la Phase A.
return std::make_unique<ModuleGame>();
}
// game_state_ == 1: dispatch al registry per num_piramide. Replica
// del redirect que el vell ModuleSequence::Go() feia: si el jugador
// arriba a la Secreta (6) sense prou diners, salta als slides de
// fracàs (7) abans de buscar l'escena al registry.
if (info::ctx.num_piramide == 6 && info::ctx.diners < 200) {
info::ctx.num_piramide = 7;
}
return scenes::SceneRegistry::instance().tryCreate(info::ctx.num_piramide);
}
void Director::init() { void Director::init() {
instance_ = new Director(); instance_ = new Director();
Gamepad::init(); Gamepad::init();
// Registre d'escenes. Cada entrada = un state_key (`num_piramide`) // Registre d'escenes. Cada entrada = un state_key (`num_piramide`)
// amb una factory de `scenes::Scene`. El gameFiberEntry consulta // amb una factory de `scenes::Scene`. iterate() consulta aquest
// aquest registry per a tots els states de seqüència; si una clau // registry per a tots els states de seqüència (game_state_ == 1); si
// no apareix ací, el fiber eixirà del loop. // una clau no apareix ací, Director surt ordenadament.
auto& registry = scenes::SceneRegistry::instance(); auto& registry = scenes::SceneRegistry::instance();
registry.registerScene(0, [] { return std::make_unique<scenes::MenuScene>(); }); registry.registerScene(0, [] { return std::make_unique<scenes::MenuScene>(); });
registry.registerScene(100, [] { return std::make_unique<scenes::MortScene>(); }); registry.registerScene(100, [] { return std::make_unique<scenes::MortScene>(); });
@@ -132,12 +102,9 @@ void Director::init() {
} }
return std::make_unique<scenes::IntroScene>(); return std::make_unique<scenes::IntroScene>();
}); });
GameFiber::init(gameFiberEntry);
} }
void Director::destroy() { void Director::destroy() {
GameFiber::destroy();
Gamepad::destroy(); Gamepad::destroy();
delete instance_; delete instance_;
instance_ = nullptr; instance_ = nullptr;
@@ -164,17 +131,17 @@ void Director::setup() {
} }
bool Director::iterate() { bool Director::iterate() {
if (GameFiber::is_done() || quit_requested_) { if (quit_requested_) {
// 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(); JG_QuitSignal();
while (!GameFiber::is_done()) { current_scene_.reset(); // destrueix l'escena actual ordenadament
GameFiber::resume();
}
return false; return false;
} }
if (!context_initialized_) {
initGameContext();
context_initialized_ = true;
}
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)
@@ -205,15 +172,41 @@ bool Director::iterate() {
esc_blocked_ = false; esc_blocked_ = false;
} }
// Cedeix el control al fiber del joc. Quan retorne (per un JD8_Flip() // Avança l'escena (si no estem pausats). En pausa, es manté l'escena
// dins del joc) tindrem un nou frame a pixel_data. Si estem en pausa, // congelada i re-presentem l'últim frame amb l'overlay fresc per
// no executem el fiber: es queda congelat al seu últim yield i // damunt.
// continuem presentant l'últim frame conegut.
if (!paused_) { if (!paused_) {
GameFiber::resume(); // Transicions: si l'escena actual ha acabat (o s'ha senyalat
if (GameFiber::is_done()) { // quit), llegim el seu next state i la destruïm per crear la
return false; // següent a continuació.
if (current_scene_ && (current_scene_->done() || JG_Quitting())) {
game_state_ = current_scene_->nextState();
current_scene_.reset();
} }
// Si no hi ha escena activa, construeix la pròxima segons
// game_state_ i info::ctx. Si és impossible (game_state_ == -1,
// quit, o state no registrat), eixim del loop.
if (!current_scene_) {
if (game_state_ == -1 || JG_Quitting()) return false;
current_scene_ = createNextScene();
if (!current_scene_) return false;
current_scene_->onEnter();
last_tick_ms_ = SDL_GetTicks();
}
// Tick de l'escena. JI_Update refresca key_pressed/any_key; el
// delta_ms és el temps real transcorregut des de l'últim tick.
JI_Update();
const Uint32 now = SDL_GetTicks();
const int delta_ms = static_cast<int>(now - last_tick_ms_);
last_tick_ms_ = now;
current_scene_->tick(delta_ms);
// Converteix `screen` indexat → `pixel_data` ARGB amb la paleta
// actual. JD8_Flip ja no fa yield (Phase B.2 eliminà els fibers);
// ara només omple el framebuffer perquè el Director l'aprofite.
JD8_Flip();
std::memcpy(game_frame_, JD8_GetFramebuffer(), sizeof(game_frame_)); std::memcpy(game_frame_, JD8_GetFramebuffer(), sizeof(game_frame_));
has_frame_ = true; has_frame_ = true;
} }
@@ -238,12 +231,11 @@ bool Director::iterate() {
} }
void Director::teardown() { void Director::teardown() {
// Si el joc encara no ha acabat (p.ex. eixida per SDL_QUIT des del // Senyal de quit i descàrrega ordenada de l'escena en curs. Els
// sistema), li donem l'oportunitat de tornar net. // destructors de cada escena són no-bloquejants — ja no fan fades
// bloquejants. La resta de cleanup la gestiona `destroy()`.
JG_QuitSignal(); JG_QuitSignal();
while (!GameFiber::is_done()) { current_scene_.reset();
GameFiber::resume();
}
} }
void Director::run() { void Director::run() {

View File

@@ -4,13 +4,15 @@
#include <atomic> #include <atomic>
#include <cstdint> #include <cstdint>
#include <memory>
// El Director és el thread principal que controla la presentació i els inputs. #include "scenes/scene.hpp"
// 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 // El Director és l'únic thread del runtime. Cada iterate() fa input →
// Director, i el Director el presenta abans de tornar-lo a reprendre amb // tick de l'escena actual → JD8_Flip → overlay → present → sleep al frame
// GameFiber::resume(). Tot ocorre en un únic thread — sense mutex, sense // target. Totes les escenes (`scenes::Scene` i `ModuleGame`) són
// condition_variable, compatible amb el futur port a SDL_AppIterate. // tick-based i no bloquegen — no hi ha fibers, mutex ni condition_variable.
// Compatible amb SDL_AppIterate i amb el futur port a emscripten.
class Director { class Director {
public: public:
static void init(); static void init();
@@ -40,19 +42,26 @@ 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: mentre està activa, Director no fa resume() del fiber del joc, // Pausa: mentre està activa, iterate() no avança l'escena — es
// així que el joc queda congelat al seu últim JD8_Flip. // continua presentant el darrer frame amb overlay fresc.
void togglePause(); void togglePause();
auto isPaused() const -> bool { return paused_; } auto isPaused() const -> bool { return paused_; }
private: private:
Director() = default; Director() = default;
~Director() = default; ~Director();
static Director* instance_; static Director* instance_;
void pollAllEvents(); // drenatge amb SDL_PollEvent, només per al bucle natiu void pollAllEvents(); // drenatge amb SDL_PollEvent, només per al bucle natiu
// Inicialitza info::ctx a partir de Options::game.* i comprova trick.ini.
// Es crida una sola vegada des d'iterate() a la primera invocació.
void initGameContext();
// Construeix l'escena apropiada segons game_state_ i info::ctx.
// Retorna nullptr si l'state actual no té escena registrada (bug).
std::unique_ptr<scenes::Scene> createNextScene();
// Buffers persistents entre iteracions. Abans eren locals a run(), // Buffers persistents entre iteracions. Abans eren locals a run(),
// ara són membres perquè iterate() els pot reutilitzar sense tornar-los // ara són membres perquè iterate() els pot reutilitzar sense tornar-los
// a reservar en cada crida del callback. // a reservar en cada crida del callback.
@@ -60,6 +69,13 @@ class Director {
Uint32 presentation_buffer_[320 * 200]{}; Uint32 presentation_buffer_[320 * 200]{};
bool has_frame_{false}; bool has_frame_{false};
// Estat de l'escena actual. Abans vivia al stack del GameFiber; des
// de la Phase B.2 de la migració viu directament al Director.
std::unique_ptr<scenes::Scene> current_scene_;
int game_state_{1}; // 0 = gameplay (ModuleGame), 1 = via SceneRegistry, -1 = quit
Uint32 last_tick_ms_{0};
bool context_initialized_{false};
std::atomic<bool> quit_requested_{false}; std::atomic<bool> quit_requested_{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};

View File

@@ -1,141 +0,0 @@
#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

View File

@@ -1,36 +0,0 @@
#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