Extract GameEngine::doFrame() for Emscripten callback support

Refactors the main game loop to support both:
- Desktop: traditional blocking while(running) loop
- Browser: emscripten_set_main_loop_arg() callback (build-time conditional)

Changes:
- Add doFrame() method containing single-frame update logic
- Add isRunning() accessor for Emscripten callback
- run() now conditionally uses #ifdef __EMSCRIPTEN__ for loop selection
- Add emscriptenMainLoopCallback() static function

This is a prerequisite for Emscripten builds - browsers require
cooperative multitasking with callback-based frame updates.

Both normal and headless builds verified working.

Contributes to #158

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
John McCardle 2026-01-30 23:51:35 -05:00
commit 8b6eb1e7ae
3 changed files with 193 additions and 137 deletions

View file

@ -725,7 +725,32 @@ grid = mcrfpy.Grid(grid_size=(10,10)) # ✅
### Remaining Steps for Emscripten
1. **Main loop extraction** - Extract `GameEngine::doFrame()` for callback-based loop
1. ✅ **Main loop extraction** - `GameEngine::doFrame()` extracted with Emscripten callback support
- `run()` now uses `#ifdef __EMSCRIPTEN__` to choose between callback and blocking loop
- `emscripten_set_main_loop_arg()` integration ready
2. **Emscripten toolchain** - Add CMake toolchain file for emcc
3. **VRSFML integration** - Replace stubs with actual WebGL rendering
4. **Python-in-WASM** - Test CPython/Pyodide integration (highest risk)
### Main Loop Architecture
The game loop now supports both desktop (blocking) and browser (callback) modes:
```cpp
// GameEngine::run() - build-time conditional
#ifdef __EMSCRIPTEN__
emscripten_set_main_loop_arg(emscriptenMainLoopCallback, this, 0, 1);
#else
while (running) { doFrame(); }
#endif
// GameEngine::doFrame() - same code runs in both modes
void GameEngine::doFrame() {
metrics.resetPerFrame();
currentScene()->update();
testTimers();
// ... animations, input, rendering ...
currentFrame++;
frameTime = clock.restart().asSeconds();
}
```

View file

@ -295,13 +295,50 @@ void GameEngine::setWindowScale(float multiplier)
}
}
// Emscripten callback support
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
// Static callback for emscripten_set_main_loop_arg
static void emscriptenMainLoopCallback(void* arg) {
GameEngine* engine = static_cast<GameEngine*>(arg);
if (!engine->isRunning()) {
emscripten_cancel_main_loop();
engine->cleanup();
return;
}
engine->doFrame();
}
#endif
void GameEngine::run()
{
//std::cout << "GameEngine::run() starting main loop..." << std::endl;
float fps = 0.0;
frameTime = 0.016f; // Initialize to ~60 FPS
clock.restart();
#ifdef __EMSCRIPTEN__
// Browser: use callback-based loop (non-blocking)
// 0 = use requestAnimationFrame, 1 = simulate infinite loop
emscripten_set_main_loop_arg(emscriptenMainLoopCallback, this, 0, 1);
#else
// Desktop: traditional blocking loop
while (running)
{
doFrame();
}
// Clean up before exiting the run loop
cleanup();
// #144: Quick exit to avoid cleanup segfaults in Python/C++ destructor ordering
// This is a pragmatic workaround - proper cleanup would require careful
// attention to shared_ptr cycles and Python GC interaction
std::_Exit(0);
#endif
}
void GameEngine::doFrame()
{
// Reset per-frame metrics
metrics.resetPerFrame();
@ -409,7 +446,7 @@ void GameEngine::run()
currentFrame++;
frameTime = clock.restart().asSeconds();
fps = 1 / frameTime;
float fps = 1 / frameTime;
// Update profiling metrics
metrics.updateFrameTime(frameTime * 1000.0f); // Convert to milliseconds
@ -419,6 +456,7 @@ void GameEngine::run()
int whole_fps = metrics.fps;
int tenth_fps = (metrics.fps * 10) % 10;
(void)whole_fps; (void)tenth_fps; (void)fps; // Suppress unused variable warnings
if (!headless && window) {
window->setTitle(window_title);
@ -440,15 +478,6 @@ void GameEngine::run()
}
}
// Clean up before exiting the run loop
cleanup();
// #144: Quick exit to avoid cleanup segfaults in Python/C++ destructor ordering
// This is a pragmatic workaround - proper cleanup would require careful
// attention to shared_ptr cycles and Python GC interaction
std::_Exit(0);
}
std::shared_ptr<Timer> GameEngine::getTimer(const std::string& name)
{
auto it = timers.find(name);

View file

@ -235,6 +235,8 @@ public:
sf::RenderTarget & getRenderTarget();
sf::RenderTarget* getRenderTargetPtr() { return render_target; }
void run();
void doFrame(); // Single frame update - extracted for Emscripten callback support
bool isRunning() const { return running; } // Check if engine should continue running
void sUserInput();
void cleanup(); // Clean up Python references before destruction
void executeStartupScripts(); // Execute --exec scripts (called once after final engine setup)