threads
This commit is contained in:
69
CLAUDE.md
69
CLAUDE.md
@@ -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
|
- **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/`
|
- **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/`)
|
### 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
|
- **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`.
|
All key bindings are configurable via `Options::keys_gui` and stored in `config.yaml`.
|
||||||
|
|
||||||
### Rendering Pipeline
|
### Threading Model (Emulator Architecture)
|
||||||
|
|
||||||
```
|
```
|
||||||
JD8_Flip():
|
Main thread (Director) Game thread (ModuleGame/Sequence::Go())
|
||||||
1. palette→ARGB in pixel_data[320×200] (original engine)
|
──────────────────── ────────────────────────────────────
|
||||||
2. Screen::present(pixel_data):
|
loop at ~60 FPS { loop {
|
||||||
a. FPS count + render info text update
|
SDL_PollEvent() ... game logic ...
|
||||||
b. Overlay::render(pixel_data) (notifications, render info, directly on ARGB)
|
GlobalInputs, Mouse JD8_Flip():
|
||||||
c. IF GPU + shaders enabled:
|
if new_frame_available: palette→ARGB in pixel_data
|
||||||
- uploadPixels → scene_texture (320×200)
|
copy to game_frame publishFrame(pixel_data) ⏸
|
||||||
- [IF 4:3] stretch pass fused with upscale: scene → scaled_texture (W×factor, H×factor×1.2)
|
signal → ────────────────────→ (blocks until Director consumes)
|
||||||
- [IF SS] upscale pass: scene → scaled_texture (W×factor, H×factor)
|
copy game_frame → present_buffer ←──── signal_consumed
|
||||||
- PostFX or CRT-Pi shader → swapchain (with viewport letterboxing)
|
Overlay::render(present_buffer) continue game loop
|
||||||
d. ELSE IF GPU without shaders:
|
Screen::present(present_buffer) }
|
||||||
- uploadPixels → clean render → swapchain
|
SDL_Delay to hit 60fps
|
||||||
e. ELSE (fallback):
|
}
|
||||||
- SDL_UpdateTexture → SDL_RenderPresent
|
```
|
||||||
|
|
||||||
|
**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
|
### Pixel Format
|
||||||
@@ -139,12 +165,15 @@ JD8_Flip produces ABGR byte order: `0xFF000000 + R + (G<<8) + (B<<16)`. SDL text
|
|||||||
|
|
||||||
### Known Issues & Technical Debt
|
### 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.
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ set(APP_SOURCES
|
|||||||
source/core/input/global_inputs.cpp
|
source/core/input/global_inputs.cpp
|
||||||
source/core/input/mouse.cpp
|
source/core/input/mouse.cpp
|
||||||
|
|
||||||
|
# Core - System (nova capa)
|
||||||
|
source/core/system/director.cpp
|
||||||
|
|
||||||
# Game
|
# Game
|
||||||
source/game/options.cpp
|
source/game/options.cpp
|
||||||
source/game/bola.cpp
|
source/game/bola.cpp
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
|
||||||
#include "core/jail/jfile.hpp"
|
#include "core/jail/jfile.hpp"
|
||||||
#include "core/rendering/screen.hpp"
|
#include "core/system/director.hpp"
|
||||||
#include "external/gif.h"
|
#include "external/gif.h"
|
||||||
|
|
||||||
JD8_Surface screen = NULL;
|
JD8_Surface screen = NULL;
|
||||||
@@ -153,7 +153,7 @@ void JD8_Flip() {
|
|||||||
pixel_data[x + (y * 320)] = color;
|
pixel_data[x + (y * 320)] = color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Screen::get()->present(pixel_data);
|
Director::get()->publishFrame(pixel_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
void JD8_FreeSurface(JD8_Surface surface) {
|
void JD8_FreeSurface(JD8_Surface surface) {
|
||||||
|
|||||||
@@ -2,32 +2,13 @@
|
|||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "core/input/global_inputs.hpp"
|
#include "core/system/director.hpp"
|
||||||
#include "core/input/mouse.hpp"
|
|
||||||
#include "core/jail/jgame.hpp"
|
|
||||||
#include "core/rendering/overlay.hpp"
|
|
||||||
#include "game/options.hpp"
|
|
||||||
|
|
||||||
const bool* keystates; // = SDL_GetKeyboardState( NULL );
|
// keystates és actualitzat per SDL internament. Des del joc només fem lectures.
|
||||||
SDL_Event event;
|
const bool* keystates = nullptr;
|
||||||
Uint8 cheat[5];
|
Uint8 cheat[5];
|
||||||
bool key_pressed = false;
|
bool key_pressed = false;
|
||||||
int waitTime = 0;
|
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) {
|
void JI_DisableKeyboard(Uint32 time) {
|
||||||
waitTime = time;
|
waitTime = time;
|
||||||
@@ -42,47 +23,22 @@ void JI_moveCheats(Uint8 new_key) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void JI_Update() {
|
void JI_Update() {
|
||||||
key_pressed = false;
|
// El director ha processat tots els events. Ací només refresquem
|
||||||
keystates = SDL_GetKeyboardState(NULL);
|
// el snapshot del teclat i consumim el flag de tecla polsada.
|
||||||
|
if (keystates == nullptr) {
|
||||||
|
keystates = SDL_GetKeyboardState(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
if (waitTime > 0) waitTime--;
|
if (waitTime > 0) waitTime--;
|
||||||
|
|
||||||
while (SDL_PollEvent(&event)) {
|
// Consumim el flag de "alguna tecla no-GUI polsada" del director
|
||||||
if (event.type == SDL_EVENT_QUIT) JG_QuitSignal();
|
key_pressed = Director::get()->consumeKeyPressed();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool JI_KeyPressed(int key) {
|
bool JI_KeyPressed(int key) {
|
||||||
if (waitTime > 0) return false;
|
if (waitTime > 0 || keystates == nullptr) return false;
|
||||||
// Si ESC està bloquejat per l'overlay, no la passem al joc
|
// ESC bloquejada pel Director (primera pulsació mostra notificació)
|
||||||
if (key == SDL_SCANCODE_ESCAPE && esc_blocked_) return false;
|
if (key == SDL_SCANCODE_ESCAPE && Director::get()->isEscBlocked()) return false;
|
||||||
return keystates[key] != 0;
|
return keystates[key] != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -183,6 +183,10 @@ namespace Overlay {
|
|||||||
render_info_text_ = text;
|
render_info_text_ = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto isEscConsumed() -> bool {
|
||||||
|
return esc_waiting_;
|
||||||
|
}
|
||||||
|
|
||||||
auto handleEscape() -> bool {
|
auto handleEscape() -> bool {
|
||||||
if (!esc_waiting_) {
|
if (!esc_waiting_) {
|
||||||
// Primera pulsació: mostra avís i consumeix
|
// Primera pulsació: mostra avís i consumeix
|
||||||
|
|||||||
@@ -19,4 +19,6 @@ namespace Overlay {
|
|||||||
// Gestió d'eixida amb doble ESC
|
// Gestió d'eixida amb doble ESC
|
||||||
// Retorna true si l'ESC ha sigut consumit (no s'ha de passar al joc)
|
// Retorna true si l'ESC ha sigut consumit (no s'ha de passar al joc)
|
||||||
auto handleEscape() -> bool;
|
auto handleEscape() -> bool;
|
||||||
|
// True mentre s'espera la segona pulsació d'ESC
|
||||||
|
auto isEscConsumed() -> bool;
|
||||||
} // namespace Overlay
|
} // namespace Overlay
|
||||||
|
|||||||
@@ -304,6 +304,7 @@ auto Screen::getActiveShaderName() const -> const char* {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Screen::updateRenderInfo() {
|
void Screen::updateRenderInfo() {
|
||||||
|
static const Uint32 start_ticks = SDL_GetTicks();
|
||||||
std::string driver = gpu_driver_.empty() ? "sdl" : toLower(gpu_driver_);
|
std::string driver = gpu_driver_.empty() ? "sdl" : toLower(gpu_driver_);
|
||||||
std::string info = std::to_string(fps_.last_value) + " fps - " + 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)";
|
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());
|
Overlay::setRenderInfoText(info.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
217
source/core/system/director.cpp
Normal file
217
source/core/system/director.cpp
Normal 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();
|
||||||
|
}
|
||||||
59
source/core/system/director.hpp
Normal file
59
source/core/system/director.hpp
Normal 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};
|
||||||
|
};
|
||||||
@@ -1,20 +1,16 @@
|
|||||||
#include <ctime>
|
#include <ctime>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "core/input/global_inputs.hpp"
|
|
||||||
#include "core/jail/jail_audio.hpp"
|
#include "core/jail/jail_audio.hpp"
|
||||||
#include "core/jail/jdraw8.hpp"
|
#include "core/jail/jdraw8.hpp"
|
||||||
#include "core/jail/jfile.hpp"
|
#include "core/jail/jfile.hpp"
|
||||||
#include "core/jail/jgame.hpp"
|
#include "core/jail/jgame.hpp"
|
||||||
#include "core/rendering/overlay.hpp"
|
#include "core/rendering/overlay.hpp"
|
||||||
#include "core/rendering/screen.hpp"
|
#include "core/rendering/screen.hpp"
|
||||||
#include "game/defines.hpp"
|
#include "core/system/director.hpp"
|
||||||
#include "game/info.hpp"
|
|
||||||
#include "game/modulegame.hpp"
|
|
||||||
#include "game/modulesequence.hpp"
|
|
||||||
#include "game/options.hpp"
|
#include "game/options.hpp"
|
||||||
|
|
||||||
int main(int argc, char* args[]) {
|
int main(int /*argc*/, char* /*args*/[]) {
|
||||||
srand(unsigned(time(NULL)));
|
srand(unsigned(time(NULL)));
|
||||||
|
|
||||||
// Crea la carpeta de configuració i carrega les opcions
|
// Crea la carpeta de configuració i carrega les opcions
|
||||||
@@ -33,43 +29,14 @@ int main(int argc, char* args[]) {
|
|||||||
JD8_Init();
|
JD8_Init();
|
||||||
JA_Init(48000, SDL_AUDIO_S16, 2);
|
JA_Init(48000, SDL_AUDIO_S16, 2);
|
||||||
Overlay::init();
|
Overlay::init();
|
||||||
|
Director::init();
|
||||||
|
|
||||||
info::num_habitacio = Options::game.habitacio_inicial;
|
// Arranca el Director: crea game thread, bucle principal, sincronització de frames
|
||||||
info::num_piramide = Options::game.piramide_inicial;
|
Director::get()->run();
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Options::saveToFile();
|
Options::saveToFile();
|
||||||
|
|
||||||
|
Director::destroy();
|
||||||
Overlay::destroy();
|
Overlay::destroy();
|
||||||
JA_Quit();
|
JA_Quit();
|
||||||
JD8_Quit();
|
JD8_Quit();
|
||||||
|
|||||||
Reference in New Issue
Block a user