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:
parent
4c70aee020
commit
8b6eb1e7ae
3 changed files with 193 additions and 137 deletions
|
|
@ -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();
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -295,149 +295,37 @@ 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)
|
||||
{
|
||||
// Reset per-frame metrics
|
||||
metrics.resetPerFrame();
|
||||
|
||||
currentScene()->update();
|
||||
testTimers();
|
||||
|
||||
// Update Python scenes
|
||||
{
|
||||
ScopedTimer pyTimer(metrics.pythonScriptTime);
|
||||
McRFPy_API::updatePythonScenes(frameTime);
|
||||
}
|
||||
|
||||
// Update animations (only if frameTime is valid)
|
||||
if (frameTime > 0.0f && frameTime < 1.0f) {
|
||||
ScopedTimer animTimer(metrics.animationTime);
|
||||
AnimationManager::getInstance().update(frameTime);
|
||||
}
|
||||
|
||||
if (!headless) {
|
||||
sUserInput();
|
||||
|
||||
#ifndef MCRF_HEADLESS
|
||||
// Update ImGui
|
||||
if (imguiInitialized) {
|
||||
ImGui::SFML::Update(*window, clock.getElapsedTime());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
if (!paused)
|
||||
{
|
||||
}
|
||||
|
||||
// Handle scene transitions
|
||||
if (transition.type != TransitionType::None)
|
||||
{
|
||||
transition.update(frameTime);
|
||||
|
||||
if (transition.isComplete())
|
||||
{
|
||||
// Transition complete - finalize scene change
|
||||
scene = transition.toScene;
|
||||
transition.type = TransitionType::None;
|
||||
|
||||
// Trigger Python scene lifecycle events
|
||||
McRFPy_API::triggerSceneChange(transition.fromScene, transition.toScene);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Render transition
|
||||
render_target->clear();
|
||||
transition.render(*render_target);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Normal scene rendering
|
||||
currentScene()->render();
|
||||
}
|
||||
|
||||
// Update and render profiler overlay (if enabled)
|
||||
if (profilerOverlay && !headless) {
|
||||
profilerOverlay->update(metrics);
|
||||
profilerOverlay->render(*render_target);
|
||||
}
|
||||
|
||||
#ifndef MCRF_HEADLESS
|
||||
// Render ImGui overlays (console and scene explorer)
|
||||
if (imguiInitialized && !headless) {
|
||||
console.render();
|
||||
sceneExplorer.render(*this);
|
||||
ImGui::SFML::Render(*window);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Record work time before display (which may block for vsync/framerate limit)
|
||||
metrics.workTime = clock.getElapsedTime().asSeconds() * 1000.0f;
|
||||
|
||||
// Display the frame
|
||||
// #219 - Release GIL during display() to allow background threads to run
|
||||
if (headless) {
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
headless_renderer->display();
|
||||
Py_END_ALLOW_THREADS
|
||||
// Take screenshot if requested
|
||||
if (config.take_screenshot) {
|
||||
headless_renderer->saveScreenshot(config.screenshot_path.empty() ? "screenshot.png" : config.screenshot_path);
|
||||
config.take_screenshot = false; // Only take one screenshot
|
||||
}
|
||||
} else {
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
window->display();
|
||||
Py_END_ALLOW_THREADS
|
||||
}
|
||||
|
||||
// #219 - Safe window for background threads to modify UI
|
||||
// This runs AFTER display() but BEFORE the next frame's processing
|
||||
if (frameLock.hasWaiting()) {
|
||||
frameLock.openWindow();
|
||||
// Release GIL so waiting threads can proceed with their mcrfpy.lock() blocks
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
frameLock.closeWindow(); // Wait for all lock holders to complete
|
||||
Py_END_ALLOW_THREADS
|
||||
}
|
||||
|
||||
currentFrame++;
|
||||
frameTime = clock.restart().asSeconds();
|
||||
fps = 1 / frameTime;
|
||||
|
||||
// Update profiling metrics
|
||||
metrics.updateFrameTime(frameTime * 1000.0f); // Convert to milliseconds
|
||||
|
||||
// Record frame data for benchmark logging (if running)
|
||||
g_benchmarkLogger.recordFrame(metrics);
|
||||
|
||||
int whole_fps = metrics.fps;
|
||||
int tenth_fps = (metrics.fps * 10) % 10;
|
||||
|
||||
if (!headless && window) {
|
||||
window->setTitle(window_title);
|
||||
}
|
||||
|
||||
// In windowed mode, check if window was closed
|
||||
if (!headless && window && !window->isOpen()) {
|
||||
running = false;
|
||||
}
|
||||
|
||||
// In headless exec mode, auto-exit when no timers remain
|
||||
if (config.auto_exit_after_exec && timers.empty()) {
|
||||
running = false;
|
||||
}
|
||||
|
||||
// Check if a Python exception has signaled exit
|
||||
if (McRFPy_API::shouldExit()) {
|
||||
running = false;
|
||||
}
|
||||
doFrame();
|
||||
}
|
||||
|
||||
// Clean up before exiting the run loop
|
||||
|
|
@ -447,6 +335,147 @@ void GameEngine::run()
|
|||
// 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();
|
||||
|
||||
currentScene()->update();
|
||||
testTimers();
|
||||
|
||||
// Update Python scenes
|
||||
{
|
||||
ScopedTimer pyTimer(metrics.pythonScriptTime);
|
||||
McRFPy_API::updatePythonScenes(frameTime);
|
||||
}
|
||||
|
||||
// Update animations (only if frameTime is valid)
|
||||
if (frameTime > 0.0f && frameTime < 1.0f) {
|
||||
ScopedTimer animTimer(metrics.animationTime);
|
||||
AnimationManager::getInstance().update(frameTime);
|
||||
}
|
||||
|
||||
if (!headless) {
|
||||
sUserInput();
|
||||
|
||||
#ifndef MCRF_HEADLESS
|
||||
// Update ImGui
|
||||
if (imguiInitialized) {
|
||||
ImGui::SFML::Update(*window, clock.getElapsedTime());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
if (!paused)
|
||||
{
|
||||
}
|
||||
|
||||
// Handle scene transitions
|
||||
if (transition.type != TransitionType::None)
|
||||
{
|
||||
transition.update(frameTime);
|
||||
|
||||
if (transition.isComplete())
|
||||
{
|
||||
// Transition complete - finalize scene change
|
||||
scene = transition.toScene;
|
||||
transition.type = TransitionType::None;
|
||||
|
||||
// Trigger Python scene lifecycle events
|
||||
McRFPy_API::triggerSceneChange(transition.fromScene, transition.toScene);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Render transition
|
||||
render_target->clear();
|
||||
transition.render(*render_target);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Normal scene rendering
|
||||
currentScene()->render();
|
||||
}
|
||||
|
||||
// Update and render profiler overlay (if enabled)
|
||||
if (profilerOverlay && !headless) {
|
||||
profilerOverlay->update(metrics);
|
||||
profilerOverlay->render(*render_target);
|
||||
}
|
||||
|
||||
#ifndef MCRF_HEADLESS
|
||||
// Render ImGui overlays (console and scene explorer)
|
||||
if (imguiInitialized && !headless) {
|
||||
console.render();
|
||||
sceneExplorer.render(*this);
|
||||
ImGui::SFML::Render(*window);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Record work time before display (which may block for vsync/framerate limit)
|
||||
metrics.workTime = clock.getElapsedTime().asSeconds() * 1000.0f;
|
||||
|
||||
// Display the frame
|
||||
// #219 - Release GIL during display() to allow background threads to run
|
||||
if (headless) {
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
headless_renderer->display();
|
||||
Py_END_ALLOW_THREADS
|
||||
// Take screenshot if requested
|
||||
if (config.take_screenshot) {
|
||||
headless_renderer->saveScreenshot(config.screenshot_path.empty() ? "screenshot.png" : config.screenshot_path);
|
||||
config.take_screenshot = false; // Only take one screenshot
|
||||
}
|
||||
} else {
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
window->display();
|
||||
Py_END_ALLOW_THREADS
|
||||
}
|
||||
|
||||
// #219 - Safe window for background threads to modify UI
|
||||
// This runs AFTER display() but BEFORE the next frame's processing
|
||||
if (frameLock.hasWaiting()) {
|
||||
frameLock.openWindow();
|
||||
// Release GIL so waiting threads can proceed with their mcrfpy.lock() blocks
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
frameLock.closeWindow(); // Wait for all lock holders to complete
|
||||
Py_END_ALLOW_THREADS
|
||||
}
|
||||
|
||||
currentFrame++;
|
||||
frameTime = clock.restart().asSeconds();
|
||||
float fps = 1 / frameTime;
|
||||
|
||||
// Update profiling metrics
|
||||
metrics.updateFrameTime(frameTime * 1000.0f); // Convert to milliseconds
|
||||
|
||||
// Record frame data for benchmark logging (if running)
|
||||
g_benchmarkLogger.recordFrame(metrics);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// In windowed mode, check if window was closed
|
||||
if (!headless && window && !window->isOpen()) {
|
||||
running = false;
|
||||
}
|
||||
|
||||
// In headless exec mode, auto-exit when no timers remain
|
||||
if (config.auto_exit_after_exec && timers.empty()) {
|
||||
running = false;
|
||||
}
|
||||
|
||||
// Check if a Python exception has signaled exit
|
||||
if (McRFPy_API::shouldExit()) {
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<Timer> GameEngine::getTimer(const std::string& name)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue