fase 4+5: fibers cooperatius substitueixen el game thread, sense mutex ni cv

This commit is contained in:
2026-04-15 18:50:43 +02:00
parent 801a8ad1bd
commit 1507a1c740
9 changed files with 313 additions and 141 deletions

View File

@@ -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();
}

View File

@@ -3,15 +3,14 @@
#include <SDL3/SDL.h>
#include <atomic>
#include <condition_variable>
#include <cstdint>
#include <mutex>
#include <thread>
// 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<bool> quit_requested_{false};
std::atomic<bool> game_thread_done_{false};
std::atomic<bool> key_pressed_{false};
std::atomic<bool> esc_blocked_{false};
std::atomic<bool> paused_{false};

View 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

View 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