diff --git a/CMakeLists.txt b/CMakeLists.txt index 514dd82..b4bc3fd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,7 +39,6 @@ set(APP_SOURCES # Core - System (nova capa) source/core/system/director.cpp - source/core/system/fiber.cpp # Scenes (cinemàtiques i menús reescrits) source/scenes/timeline.cpp diff --git a/source/core/jail/jdraw8.cpp b/source/core/jail/jdraw8.cpp index e36170a..873528f 100644 --- a/source/core/jail/jdraw8.cpp +++ b/source/core/jail/jdraw8.cpp @@ -3,7 +3,6 @@ #include #include "core/jail/jfile.hpp" -#include "core/system/fiber.hpp" #if defined(__clang__) #pragma clang diagnostic push #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() { + // 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 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); 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() { @@ -254,20 +253,9 @@ bool JD8_FadeTickStep() { return false; } -void JD8_FadeOut() { - JD8_FadeStartOut(); - while (true) { - const bool done = JD8_FadeTickStep(); - JD8_Flip(); - if (done) break; - } -} - -void JD8_FadeToPal(JD8_Palette pal) { - JD8_FadeStartToPal(pal); - while (true) { - const bool done = JD8_FadeTickStep(); - JD8_Flip(); - if (done) break; - } -} +// Els shims bloquejants `JD8_FadeOut` i `JD8_FadeToPal` han estat +// eliminats a Phase B.2: feien un bucle de 32 iteracions amb `JD8_Flip` +// entre cada una que només funcionava mentre l'entorn tenia fibers i +// `JD8_Flip` cedia el control al Director. Ara tot fade es fa tick a +// tick via `scenes::PaletteFade` (que encapsula `JD8_FadeStartOut` / +// `JD8_FadeStartToPal` + `JD8_FadeTickStep`). diff --git a/source/core/jail/jdraw8.hpp b/source/core/jail/jdraw8.hpp index 1d5aa07..23d979f 100644 --- a/source/core/jail/jdraw8.hpp +++ b/source/core/jail/jdraw8.hpp @@ -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); -// 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. +// Converteix la pantalla indexada a ARGB. El Director crida aquesta +// funció al final de cada tick i després llegeix el framebuffer via +// JD8_GetFramebuffer() per presentar-lo. void JD8_Flip(); // 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); -// 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 // fade; `FadeTickStep` aplica un pas i retorna `true` quan el fade ha // acabat. Un pas correspon visualment a una iteració del fade original // (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 // 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_FadeStartToPal(JD8_Palette pal); bool JD8_FadeTickStep(); diff --git a/source/core/jail/jgame.cpp b/source/core/jail/jgame.cpp index 6e8673d..cf84bad 100644 --- a/source/core/jail/jgame.cpp +++ b/source/core/jail/jgame.cpp @@ -1,7 +1,5 @@ #include "core/jail/jgame.hpp" -#include "core/system/fiber.hpp" - namespace { bool quitting = false; @@ -41,12 +39,9 @@ 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(); + // No toca update — retornem false sense més. Des de Phase B.2 ja no + // hi ha fibers: cap caller fa spin-waits (`while (!JG_ShouldUpdate())`) + // i el Director pren el control del main loop frame a frame. return false; } diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index 7ee9109..3e76cb6 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -15,7 +15,6 @@ #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/options.hpp" @@ -35,14 +34,9 @@ extern void JI_moveCheats(Uint8 new_key); Director* Director::instance_ = nullptr; -namespace { +Director::~Director() = default; -// Entry del fiber del joc. Dispatcha a una escena segons l'estat actual: -// `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() { +void Director::initGameContext() { info::ctx.num_habitacio = Options::game.habitacio_inicial; info::ctx.num_piramide = Options::game.piramide_inicial; info::ctx.diners = Options::game.diners_inicial; @@ -57,55 +51,31 @@ void gameFiberEntry() { info::ctx.nou_personatge = true; fclose(ini); } - - int gameState = 1; - while (gameState != -1 && !JG_Quitting()) { - std::unique_ptr 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(); - } 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(now - last)); - last = now; - JD8_Flip(); // presenta i cedix al Director - } - gameState = scene->nextState(); - } } -} // namespace +std::unique_ptr Director::createNextScene() { + if (game_state_ == 0) { + // Gameplay. ModuleGame és una scenes::Scene des de la Phase A. + return std::make_unique(); + } + // 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() { instance_ = new Director(); Gamepad::init(); // Registre d'escenes. Cada entrada = un state_key (`num_piramide`) - // amb una factory de `scenes::Scene`. El gameFiberEntry consulta - // aquest registry per a tots els states de seqüència; si una clau - // no apareix ací, el fiber eixirà del loop. + // amb una factory de `scenes::Scene`. iterate() consulta aquest + // registry per a tots els states de seqüència (game_state_ == 1); si + // una clau no apareix ací, Director surt ordenadament. auto& registry = scenes::SceneRegistry::instance(); registry.registerScene(0, [] { return std::make_unique(); }); registry.registerScene(100, [] { return std::make_unique(); }); @@ -132,12 +102,9 @@ void Director::init() { } return std::make_unique(); }); - - GameFiber::init(gameFiberEntry); } void Director::destroy() { - GameFiber::destroy(); Gamepad::destroy(); delete instance_; instance_ = nullptr; @@ -164,17 +131,17 @@ void Director::setup() { } bool Director::iterate() { - if (GameFiber::is_done() || 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. + if (quit_requested_) { JG_QuitSignal(); - while (!GameFiber::is_done()) { - GameFiber::resume(); - } + current_scene_.reset(); // destrueix l'escena actual ordenadament return false; } + if (!context_initialized_) { + initGameContext(); + context_initialized_ = true; + } + constexpr Uint32 FRAME_MS_VSYNC = 16; // ~60 FPS amb VSync constexpr Uint32 FRAME_MS_NO_VSYNC = 4; // ~250 FPS sense VSync (límit superior) @@ -205,15 +172,41 @@ bool Director::iterate() { esc_blocked_ = 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. + // Avança l'escena (si no estem pausats). En pausa, es manté l'escena + // congelada i re-presentem l'últim frame amb l'overlay fresc per + // damunt. if (!paused_) { - GameFiber::resume(); - if (GameFiber::is_done()) { - return false; + // Transicions: si l'escena actual ha acabat (o s'ha senyalat + // quit), llegim el seu next state i la destruïm per crear la + // 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(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_)); has_frame_ = true; } @@ -238,12 +231,11 @@ bool Director::iterate() { } void Director::teardown() { - // Si el joc encara no ha acabat (p.ex. eixida per SDL_QUIT des del - // sistema), li donem l'oportunitat de tornar net. + // Senyal de quit i descàrrega ordenada de l'escena en curs. Els + // destructors de cada escena són no-bloquejants — ja no fan fades + // bloquejants. La resta de cleanup la gestiona `destroy()`. JG_QuitSignal(); - while (!GameFiber::is_done()) { - GameFiber::resume(); - } + current_scene_.reset(); } void Director::run() { diff --git a/source/core/system/director.hpp b/source/core/system/director.hpp index 579e6e8..a0a08e9 100644 --- a/source/core/system/director.hpp +++ b/source/core/system/director.hpp @@ -4,13 +4,15 @@ #include #include +#include -// El Director és el thread principal que controla la presentació i els inputs. -// 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. +#include "scenes/scene.hpp" + +// El Director és l'únic thread del runtime. Cada iterate() fa input → +// tick de l'escena actual → JD8_Flip → overlay → present → sleep al frame +// target. Totes les escenes (`scenes::Scene` i `ModuleGame`) són +// 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 { public: static void init(); @@ -40,19 +42,26 @@ 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: mentre està activa, Director no fa resume() del fiber del joc, - // així que el joc queda congelat al seu últim JD8_Flip. + // Pausa: mentre està activa, iterate() no avança l'escena — es + // continua presentant el darrer frame amb overlay fresc. void togglePause(); auto isPaused() const -> bool { return paused_; } private: Director() = default; - ~Director() = default; + ~Director(); static Director* instance_; 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 createNextScene(); + // Buffers persistents entre iteracions. Abans eren locals a run(), // ara són membres perquè iterate() els pot reutilitzar sense tornar-los // a reservar en cada crida del callback. @@ -60,6 +69,13 @@ class Director { Uint32 presentation_buffer_[320 * 200]{}; 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 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 quit_requested_{false}; std::atomic key_pressed_{false}; std::atomic esc_blocked_{false}; diff --git a/source/core/system/fiber.cpp b/source/core/system/fiber.cpp deleted file mode 100644 index 2eb3e48..0000000 --- a/source/core/system/fiber.cpp +++ /dev/null @@ -1,141 +0,0 @@ -#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 deleted file mode 100644 index 8692676..0000000 --- a/source/core/system/fiber.hpp +++ /dev/null @@ -1,36 +0,0 @@ -#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