This commit is contained in:
2026-04-04 23:03:34 +02:00
parent a4ee304a79
commit 63424429ca
10 changed files with 407 additions and 160 deletions

View File

@@ -51,6 +51,10 @@ Flat C-style APIs (no classes), prefixed by subsystem. **Do not touch gameplay l
- **JI** (`jinput`) — Input: keyboard state polling, key debouncing, cheat code detection. Filters GUI keys from game, calls `GlobalInputs::handle()` and `Mouse::updateCursorVisibility()` each update
- **JF** (`jfile`) — File I/O: filesystem folder mode (`data/`) or packed resource file (`.jrf`). Config folder at `~/.config/jailgames/aee/`
### System Layer (`source/core/system/`)
- **Director** (`director.hpp/cpp`) — **Orchestrator singleton**. Owns main thread. Launches game thread that runs `ModuleGame`/`ModuleSequence::Go()`. Emulator-style architecture: Director runs at ~60 FPS independently, polls SDL events, updates overlay, presents frames. Game thread blocks at `JD8_Flip()``Director::publishFrame()` until Director consumes the frame. Director is **non-blocking**: if no new frame is available, it re-presents the last known game frame with fresh overlay on top
### Presentation Layer (`source/core/rendering/`)
- **Screen** (`screen.hpp/cpp`) — Singleton. Manages SDL_Window, SDL_Renderer, SDL_Texture. Dual rendering path: SDL3GPU with shaders (primary) or SDL_Renderer fallback. Handles fullscreen, zoom, aspect ratio 4:3, integer scaling, VSync. Counts FPS and updates render info text
@@ -93,23 +97,45 @@ Follows the pattern from `jaildoctors_dilemma`, persists to YAML:
All key bindings are configurable via `Options::keys_gui` and stored in `config.yaml`.
### Rendering Pipeline
### Threading Model (Emulator Architecture)
```
JD8_Flip():
1. palette→ARGB in pixel_data[320×200] (original engine)
2. Screen::present(pixel_data):
a. FPS count + render info text update
b. Overlay::render(pixel_data) (notifications, render info, directly on ARGB)
c. IF GPU + shaders enabled:
- uploadPixels → scene_texture (320×200)
- [IF 4:3] stretch pass fused with upscale: scene → scaled_texture (W×factor, H×factor×1.2)
- [IF SS] upscale pass: scene → scaled_texture (W×factor, H×factor)
- PostFX or CRT-Pi shader → swapchain (with viewport letterboxing)
d. ELSE IF GPU without shaders:
- uploadPixels → clean render → swapchain
e. ELSE (fallback):
- SDL_UpdateTexture → SDL_RenderPresent
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) }
SDL_Delay to hit 60fps
}
```
**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
### Rendering Pipeline (inside Screen::present)
```
Screen::present(pixel_data):
1. FPS count + render info text update
2. IF GPU + shaders enabled:
- uploadPixels → scene_texture (320×200)
- [IF 4:3] stretch pass fused with upscale: scene → scaled_texture (W×factor, H×factor×1.2)
- [IF SS] upscale pass: scene → scaled_texture (W×factor, H×factor)
- PostFX or CRT-Pi shader → swapchain (with viewport letterboxing)
3. ELSE IF GPU without shaders:
- uploadPixels → clean render → swapchain
4. ELSE (fallback):
- SDL_UpdateTexture → SDL_RenderPresent
```
### Pixel Format
@@ -139,12 +165,15 @@ JD8_Flip produces ABGR byte order: `0xFF000000 + R + (G<<8) + (B<<16)`. SDL text
### Known Issues & Technical Debt
1. **ESC double-press does not work in ModuleGame**: `modulegame.cpp:136` polls `JI_KeyPressed(ESCAPE)` each frame and calls `JG_QuitSignal()` immediately. The overlay intercepts the KEY_UP event and blocks polling via `esc_blocked_`, but there is a race condition — the game's polling can fire before the block takes effect. Needs deeper integration (possibly intercepting at `JI_KeyPressed` level with state tracking across frames, or modifying the game module to use the overlay's quit flow)
1. **gif.h cannot be included twice**: Functions are not `static` or `inline`, causing multiple definition errors. Text class uses `extern` forward declarations as workaround
2. **Overlay freezes during intro sequences**: ModuleSequence does blocking loops with delays that don't call `JI_Update()`, so `Overlay::render()` doesn't execute and notifications appear frozen. Would require refactoring the original modules to use non-blocking animation — conflicts with golden rule
### Previously Fixed (kept for reference)
3. **gif.h cannot be included twice**: Functions are not `static` or `inline`, causing multiple definition errors. Text class uses `extern` forward declarations as workaround
- **ESC double-press**: Fixed by intercepting KEY_DOWN in Director, setting atomic `esc_blocked_` immediately. `JI_KeyPressed(ESCAPE)` consults this flag. No race condition possible because Director's flag wins before game polls
- **Overlay freeze during intros**: Fixed by threading model. Director runs independently at 60 FPS regardless of game delays. Double buffer avoids overlay smearing on re-presented frames
### Main Loop (`source/main.cpp`)
### Main Entry (`source/main.cpp`)
Init order: `file_setconfigfolder``Options::load``Options::loadPostFX/CrtPi``JG_Init``Screen::init``JD8_Init``JA_Init``Overlay::init`. Shutdown reverse. A state machine alternates between `ModuleSequence` (state 1) and `ModuleGame` (state 0). Each module's `Go()` returns the next state (-1 to quit).
Init order: `file_setconfigfolder``Options::load``Options::loadPostFX/CrtPi``JG_Init``Screen::init``JD8_Init``JA_Init``Overlay::init``Director::init``Director::run()` (blocks until quit). Shutdown: `Options::save``Director::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.

View File

@@ -31,6 +31,9 @@ set(APP_SOURCES
source/core/input/global_inputs.cpp
source/core/input/mouse.cpp
# Core - System (nova capa)
source/core/system/director.cpp
# Game
source/game/options.cpp
source/game/bola.cpp

View File

@@ -3,7 +3,7 @@
#include <fstream>
#include "core/jail/jfile.hpp"
#include "core/rendering/screen.hpp"
#include "core/system/director.hpp"
#include "external/gif.h"
JD8_Surface screen = NULL;
@@ -153,7 +153,7 @@ void JD8_Flip() {
pixel_data[x + (y * 320)] = color;
}
}
Screen::get()->present(pixel_data);
Director::get()->publishFrame(pixel_data);
}
void JD8_FreeSurface(JD8_Surface surface) {

View File

@@ -2,32 +2,13 @@
#include <string>
#include "core/input/global_inputs.hpp"
#include "core/input/mouse.hpp"
#include "core/jail/jgame.hpp"
#include "core/rendering/overlay.hpp"
#include "game/options.hpp"
#include "core/system/director.hpp"
const bool* keystates; // = SDL_GetKeyboardState( NULL );
SDL_Event event;
// keystates és actualitzat per SDL internament. Des del joc només fem lectures.
const bool* keystates = nullptr;
Uint8 cheat[5];
bool key_pressed = false;
int waitTime = 0;
static bool esc_blocked_ = false; // Bloqueja ESC per polling quan l'overlay l'ha consumit
// Comprova si un scancode pertany a les tecles reservades per a la GUI
static bool isGuiKey(SDL_Scancode sc) {
return sc == Options::keys_gui.dec_zoom ||
sc == Options::keys_gui.inc_zoom ||
sc == Options::keys_gui.fullscreen ||
sc == Options::keys_gui.toggle_shader ||
sc == Options::keys_gui.toggle_aspect_ratio ||
sc == Options::keys_gui.toggle_supersampling ||
sc == Options::keys_gui.next_shader ||
sc == Options::keys_gui.next_shader_preset ||
sc == Options::keys_gui.toggle_stretch_filter ||
sc == Options::keys_gui.toggle_render_info;
}
void JI_DisableKeyboard(Uint32 time) {
waitTime = time;
@@ -42,47 +23,22 @@ void JI_moveCheats(Uint8 new_key) {
}
void JI_Update() {
key_pressed = false;
keystates = SDL_GetKeyboardState(NULL);
// El director ha processat tots els events. Ací només refresquem
// el snapshot del teclat i consumim el flag de tecla polsada.
if (keystates == nullptr) {
keystates = SDL_GetKeyboardState(NULL);
}
if (waitTime > 0) waitTime--;
while (SDL_PollEvent(&event)) {
if (event.type == SDL_EVENT_QUIT) JG_QuitSignal();
if (event.type == SDL_EVENT_KEY_UP) {
// ESC interceptat per l'overlay (doble pulsació per eixir)
if (event.key.scancode == SDL_SCANCODE_ESCAPE) {
if (Overlay::handleEscape()) {
// Consumit: primera pulsació, bloqueja ESC per polling
esc_blocked_ = true;
} else {
// Segona pulsació: desbloqueja i passa al joc
esc_blocked_ = false;
key_pressed = true;
JG_QuitSignal();
}
} else if (!isGuiKey(event.key.scancode)) {
// Tecles normals del joc
key_pressed = true;
JI_moveCheats(event.key.scancode);
}
}
Mouse::handleEvent(event);
}
// Desbloqueja ESC quan la tecla ja no està polsada i l'overlay ha fet timeout
if (esc_blocked_ && !keystates[SDL_SCANCODE_ESCAPE]) {
esc_blocked_ = false;
}
Mouse::updateCursorVisibility();
GlobalInputs::handle();
// Consumim el flag de "alguna tecla no-GUI polsada" del director
key_pressed = Director::get()->consumeKeyPressed();
}
bool JI_KeyPressed(int key) {
if (waitTime > 0) return false;
// Si ESC està bloquejat per l'overlay, no la passem al joc
if (key == SDL_SCANCODE_ESCAPE && esc_blocked_) return false;
if (waitTime > 0 || keystates == nullptr) return false;
// ESC bloquejada pel Director (primera pulsació mostra notificació)
if (key == SDL_SCANCODE_ESCAPE && Director::get()->isEscBlocked()) return false;
return keystates[key] != 0;
}

View File

@@ -183,6 +183,10 @@ namespace Overlay {
render_info_text_ = text;
}
auto isEscConsumed() -> bool {
return esc_waiting_;
}
auto handleEscape() -> bool {
if (!esc_waiting_) {
// Primera pulsació: mostra avís i consumeix

View File

@@ -19,4 +19,6 @@ namespace Overlay {
// Gestió d'eixida amb doble ESC
// Retorna true si l'ESC ha sigut consumit (no s'ha de passar al joc)
auto handleEscape() -> bool;
// True mentre s'espera la segona pulsació d'ESC
auto isEscConsumed() -> bool;
} // namespace Overlay

View File

@@ -304,6 +304,7 @@ auto Screen::getActiveShaderName() const -> const char* {
}
void Screen::updateRenderInfo() {
static const Uint32 start_ticks = SDL_GetTicks();
std::string driver = gpu_driver_.empty() ? "sdl" : toLower(gpu_driver_);
std::string info = std::to_string(fps_.last_value) + " fps - " + driver;
@@ -314,6 +315,15 @@ void Screen::updateRenderInfo() {
if (Options::video.supersampling) info += " (ss)";
}
// Temps de joc: m:ss.cc
Uint32 elapsed = SDL_GetTicks() - start_ticks;
int minutes = elapsed / 60000;
int seconds = (elapsed / 1000) % 60;
int centis = (elapsed / 10) % 100;
char time_buf[32];
snprintf(time_buf, sizeof(time_buf), " - %d:%02d.%02d", minutes, seconds, centis);
info += time_buf;
Overlay::setRenderInfoText(info.c_str());
}

View File

@@ -0,0 +1,217 @@
#include "core/system/director.hpp"
#include <cstring>
#include <iostream>
#include "core/input/global_inputs.hpp"
#include "core/input/mouse.hpp"
#include "core/jail/jgame.hpp"
#include "core/rendering/overlay.hpp"
#include "core/rendering/screen.hpp"
#include "game/info.hpp"
#include "game/modulegame.hpp"
#include "game/modulesequence.hpp"
#include "game/options.hpp"
// Cheats del joc original — declarats a jinput.cpp
extern void JI_moveCheats(Uint8 new_key);
Director* Director::instance_ = nullptr;
void Director::init() {
instance_ = new Director();
}
void Director::destroy() {
delete instance_;
instance_ = nullptr;
}
auto Director::get() -> Director* {
return instance_;
}
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)
Uint32 game_frame[320 * 200]{};
Uint32 presentation_buffer[320 * 200]{};
bool has_frame = false;
constexpr Uint32 TARGET_FRAME_MS = 16; // ~60 FPS
// Bucle principal del director (no-bloquejant)
while (!game_thread_done_ && !quit_requested_) {
Uint32 frame_start = SDL_GetTicks();
handleEvents();
GlobalInputs::handle();
Mouse::updateCursorVisibility();
// Si l'overlay ja no bloqueja ESC (timeout), desbloquegem
if (esc_blocked_ && !Overlay::isEscConsumed()) {
esc_blocked_ = false;
}
// Consumeix un frame nou si n'hi ha un disponible (no bloqueja)
bool new_frame = false;
{
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
}
// Presenta sempre: parteix del frame net del joc, afegeix overlay i envia
if (has_frame) {
memcpy(presentation_buffer, game_frame, sizeof(presentation_buffer));
Screen::get()->present(presentation_buffer);
}
// Limita a ~60 FPS
Uint32 elapsed = SDL_GetTicks() - frame_start;
if (elapsed < TARGET_FRAME_MS) {
SDL_Delay(TARGET_FRAME_MS - elapsed);
}
}
// Assegura que el game thread ix (despertar-lo per si està esperant)
quit_requested_ = true;
JG_QuitSignal();
{
std::lock_guard lock(mutex_);
frame_consumed_ = true;
}
frame_consumed_cv_.notify_all();
if (game_thread_.joinable()) {
game_thread_.join();
}
}
void Director::handleEvents() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
if (event.type == SDL_EVENT_QUIT) {
JG_QuitSignal();
requestQuit();
}
// ESC: interceptem KEY_DOWN per bloquejar-la ABANS que el joc la veja per polling
if (event.type == SDL_EVENT_KEY_DOWN && event.key.scancode == SDL_SCANCODE_ESCAPE && !event.key.repeat) {
esc_blocked_ = true; // Bloqueja ESC per polling immediatament
if (!Overlay::isEscConsumed()) {
// Primera pulsació: mostra notificació
Overlay::handleEscape();
} else {
// Segona pulsació: senyal d'eixida al joc
esc_blocked_ = false;
key_pressed_ = true;
JG_QuitSignal();
}
continue; // no processa més aquest event
}
if (event.type == SDL_EVENT_KEY_UP) {
if (event.key.scancode == SDL_SCANCODE_ESCAPE) {
// Ja processat a KEY_DOWN, només deixem netejar el bloqueig
// quan l'overlay faça timeout
continue;
} else {
// Comprova si és una tecla GUI (no passa al joc)
const auto sc = event.key.scancode;
const bool is_gui_key = (sc == Options::keys_gui.dec_zoom ||
sc == Options::keys_gui.inc_zoom ||
sc == Options::keys_gui.fullscreen ||
sc == Options::keys_gui.toggle_shader ||
sc == Options::keys_gui.toggle_aspect_ratio ||
sc == Options::keys_gui.toggle_supersampling ||
sc == Options::keys_gui.next_shader ||
sc == Options::keys_gui.next_shader_preset ||
sc == Options::keys_gui.toggle_stretch_filter ||
sc == Options::keys_gui.toggle_render_info);
if (!is_gui_key) {
key_pressed_ = true;
JI_moveCheats(sc);
}
}
}
Mouse::handleEvent(event);
}
}
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::num_habitacio = Options::game.habitacio_inicial;
info::num_piramide = Options::game.piramide_inicial;
info::diners = 0;
info::diamants = 0;
info::vida = Options::game.vides;
info::momies = 0;
info::nou_personatge = false;
info::pepe_activat = false;
FILE* ini = fopen("trick.ini", "rb");
if (ini != nullptr) {
info::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

@@ -0,0 +1,59 @@
#pragma once
#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.
class Director {
public:
static void init();
static void destroy();
static auto get() -> 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();
// Consumeix el flag de "tecla polsada" (com l'antic JI_AnyKey)
auto consumeKeyPressed() -> bool;
// Indica si ESC està bloquejada (el joc no l'ha de veure)
auto isEscBlocked() const -> bool { return esc_blocked_; }
private:
Director() = default;
~Director() = default;
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};
};

View File

@@ -1,20 +1,16 @@
#include <ctime>
#include <string>
#include "core/input/global_inputs.hpp"
#include "core/jail/jail_audio.hpp"
#include "core/jail/jdraw8.hpp"
#include "core/jail/jfile.hpp"
#include "core/jail/jgame.hpp"
#include "core/rendering/overlay.hpp"
#include "core/rendering/screen.hpp"
#include "game/defines.hpp"
#include "game/info.hpp"
#include "game/modulegame.hpp"
#include "game/modulesequence.hpp"
#include "core/system/director.hpp"
#include "game/options.hpp"
int main(int argc, char* args[]) {
int main(int /*argc*/, char* /*args*/[]) {
srand(unsigned(time(NULL)));
// Crea la carpeta de configuració i carrega les opcions
@@ -33,43 +29,14 @@ int main(int argc, char* args[]) {
JD8_Init();
JA_Init(48000, SDL_AUDIO_S16, 2);
Overlay::init();
Director::init();
info::num_habitacio = Options::game.habitacio_inicial;
info::num_piramide = Options::game.piramide_inicial;
info::diners = 0;
info::diamants = 0;
info::vida = Options::game.vides;
info::momies = 0;
info::nou_personatge = false;
info::pepe_activat = false;
FILE* ini = fopen("trick.ini", "rb");
if (ini != NULL) {
info::nou_personatge = true;
fclose(ini);
}
int gameState = 1;
while (gameState != -1) {
switch (gameState) {
case 0:
ModuleGame* moduleGame;
moduleGame = new ModuleGame();
gameState = moduleGame->Go();
delete moduleGame;
break;
case 1:
ModuleSequence* moduleSequence;
moduleSequence = new ModuleSequence();
gameState = moduleSequence->Go();
delete moduleSequence;
break;
}
}
// Arranca el Director: crea game thread, bucle principal, sincronització de frames
Director::get()->run();
Options::saveToFile();
Director::destroy();
Overlay::destroy();
JA_Quit();
JD8_Quit();