McRogueFace/src/GameEngine.h
John McCardle c5cc022aa2 Add SDL2+OpenGL ES 2 renderer backend for Emscripten/WebGL
Implements Phase 1 of renderer abstraction plan:
- SDL2Types.h: SFML-compatible type stubs in sf:: namespace
- SDL2Renderer.h/cpp: OpenGL ES 2 rendering implementation
- EmscriptenStubs.cpp: Stubs for missing POSIX functions (wait3, wait4, wcsftime)

Build system changes:
- Add MCRF_SDL2 compile-time backend selection
- Add Emscripten SDL2 link options (-sUSE_SDL=2, -sFULL_ES2=1)
- Fix LONG_BIT mismatch for Emscripten in pyport.h

Code changes for SDL2/headless compatibility:
- Guard ImGui includes with !MCRF_HEADLESS && !MCRF_SDL2
- Defer GL shader initialization until after context creation

Current status: Python runs in browser, rendering WIP (canvas sizing issues)

Build commands:
  emcmake cmake -DMCRF_SDL2=ON -B build-emscripten
  emmake make -C build-emscripten

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 11:13:15 -05:00

320 lines
11 KiB
C++

#pragma once
#include "Common.h"
#include "Scene.h"
#include "McRFPy_API.h"
#include "IndexTexture.h"
#include "Timer.h"
#include "PyCallable.h"
#include "McRogueFaceConfig.h"
#include "HeadlessRenderer.h"
#include "SceneTransition.h"
#include "Profiler.h"
// ImGui is only available for SFML builds (not headless, not SDL2)
#if !defined(MCRF_HEADLESS) && !defined(MCRF_SDL2)
#include "ImGuiConsole.h"
#include "ImGuiSceneExplorer.h"
#endif
#include <memory>
#include <sstream>
#include <mutex>
#include <condition_variable>
#include <atomic>
#include <thread>
/**
* @brief Performance profiling metrics structure
*
* Tracks frame timing, render counts, and detailed timing breakdowns.
* Used by GameEngine, ProfilerOverlay (F3), and BenchmarkLogger.
*/
struct ProfilingMetrics {
float frameTime = 0.0f; // Current frame time in milliseconds
float avgFrameTime = 0.0f; // Average frame time over last N frames
int fps = 0; // Frames per second
int drawCalls = 0; // Draw calls per frame
int uiElements = 0; // Number of UI elements rendered
int visibleElements = 0; // Number of visible elements
// Detailed timing breakdowns (added for profiling system)
float gridRenderTime = 0.0f; // Time spent rendering grids (ms)
float entityRenderTime = 0.0f; // Time spent rendering entities (ms)
float fovOverlayTime = 0.0f; // Time spent rendering FOV overlays (ms)
float pythonScriptTime = 0.0f; // Time spent in Python callbacks (ms)
float animationTime = 0.0f; // Time spent updating animations (ms)
float workTime = 0.0f; // Total work time before display/sleep (ms)
// Grid-specific metrics
int gridCellsRendered = 0; // Number of grid cells drawn this frame
int entitiesRendered = 0; // Number of entities drawn this frame
int totalEntities = 0; // Total entities in scene
// Frame time history for averaging
static constexpr int HISTORY_SIZE = 60;
float frameTimeHistory[HISTORY_SIZE] = {0};
int historyIndex = 0;
void updateFrameTime(float deltaMs) {
frameTime = deltaMs;
frameTimeHistory[historyIndex] = deltaMs;
historyIndex = (historyIndex + 1) % HISTORY_SIZE;
// Calculate average
float sum = 0.0f;
for (int i = 0; i < HISTORY_SIZE; ++i) {
sum += frameTimeHistory[i];
}
avgFrameTime = sum / HISTORY_SIZE;
fps = avgFrameTime > 0 ? static_cast<int>(1000.0f / avgFrameTime) : 0;
}
void resetPerFrame() {
drawCalls = 0;
uiElements = 0;
visibleElements = 0;
// Reset per-frame timing metrics
gridRenderTime = 0.0f;
entityRenderTime = 0.0f;
fovOverlayTime = 0.0f;
pythonScriptTime = 0.0f;
animationTime = 0.0f;
// Reset per-frame counters
gridCellsRendered = 0;
entitiesRendered = 0;
totalEntities = 0;
}
};
/**
* @brief Thread synchronization primitive for safe UI updates from background threads (#219)
*
* Allows background Python threads to safely update UI objects by waiting for
* a "safe window" between frames when the render loop is not iterating the scene graph.
*
* Usage from Python:
* with mcrfpy.lock():
* frame.x = new_value # Safe to modify UI here
*/
class FrameLock {
private:
std::mutex mtx;
std::condition_variable cv;
bool safe_window = false;
std::atomic<int> waiting{0};
std::atomic<int> active{0};
public:
/**
* @brief Acquire the lock, blocking until safe window opens
*
* Called by mcrfpy.lock().__enter__. Releases GIL while waiting.
*/
void acquire();
/**
* @brief Release the lock
*
* Called by mcrfpy.lock().__exit__
*/
void release();
/**
* @brief Open the safe window, allowing waiting threads to proceed
*
* Called by render loop between frames
*/
void openWindow();
/**
* @brief Close the safe window, waiting for all active threads to finish
*
* Called by render loop before resuming rendering
*/
void closeWindow();
/**
* @brief Check if any threads are waiting for the lock
*/
bool hasWaiting() const { return waiting.load() > 0; }
};
class GameEngine
{
public:
// Forward declare nested class so private section can use it
class ProfilerOverlay;
// Viewport modes (moved here so private section can use it)
enum class ViewportMode {
Center, // 1:1 pixels, viewport centered in window
Stretch, // viewport size = window size, doesn't respect aspect ratio
Fit // maintains original aspect ratio, leaves black bars
};
private:
std::unique_ptr<sf::RenderWindow> window;
std::unique_ptr<HeadlessRenderer> headless_renderer;
sf::RenderTarget* render_target;
sf::Font font;
std::map<std::string, Scene*> scenes;
bool running = true;
bool paused = false;
int currentFrame = 0;
sf::View visible;
sf::Clock clock;
float frameTime;
std::string window_title;
bool headless = false;
McRogueFaceConfig config;
bool cleaned_up = false;
// #153 - Headless simulation control
int simulation_time = 0; // Simulated time in milliseconds (for headless mode)
bool simulation_clock_paused = false; // True when simulation is paused (waiting for step())
// Window state tracking
bool vsync_enabled = false;
unsigned int framerate_limit = 60;
// Scene transition state
SceneTransition transition;
// Viewport system
sf::Vector2u gameResolution{1024, 768}; // Fixed game resolution
sf::View gameView; // View for the game content
ViewportMode viewportMode = ViewportMode::Fit;
// Shader intermediate texture (#106) - shared texture for shader rendering
static std::unique_ptr<sf::RenderTexture> shaderIntermediate;
static bool shaderIntermediateInitialized;
// Profiling overlay
bool showProfilerOverlay = false; // F3 key toggles this
int overlayUpdateCounter = 0; // Only update overlay every N frames
ProfilerOverlay* profilerOverlay = nullptr; // The actual overlay renderer
// ImGui is only available for SFML builds
#if !defined(MCRF_HEADLESS) && !defined(MCRF_SDL2)
// ImGui console overlay
ImGuiConsole console;
ImGuiSceneExplorer sceneExplorer;
bool imguiInitialized = false;
#endif
// #219 - Thread synchronization for background Python threads
FrameLock frameLock;
std::thread::id main_thread_id; // For detecting if lock() is called from main thread
void updateViewport();
void testTimers();
public:
sf::Clock runtime;
std::map<std::string, std::shared_ptr<Timer>> timers;
std::string scene;
// Profiling metrics (struct defined above class)
ProfilingMetrics metrics;
GameEngine();
GameEngine(const McRogueFaceConfig& cfg);
~GameEngine();
Scene* currentScene();
Scene* getScene(const std::string& name); // #118: Get scene by name
std::vector<std::string> getSceneNames() const; // #136: Get all scene names for explorer
void changeScene(std::string);
void changeScene(std::string sceneName, TransitionType transitionType, float duration);
void createScene(std::string);
void quit();
void setPause(bool);
sf::Font & getFont();
sf::RenderWindow & getWindow();
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)
int getFrame() { return currentFrame; }
float getFrameTime() { return frameTime; }
sf::View getView() { return visible; }
// Note: manageTimer() removed in #173 - use Timer objects directly
std::shared_ptr<Timer> getTimer(const std::string& name);
void setWindowScale(float);
bool isHeadless() const { return headless; }
const McRogueFaceConfig& getConfig() const { return config; }
void setAutoExitAfterExec(bool enabled) { config.auto_exit_after_exec = enabled; }
void processEvent(const sf::Event& event);
// Window property accessors
const std::string& getWindowTitle() const { return window_title; }
void setWindowTitle(const std::string& title);
bool getVSync() const { return vsync_enabled; }
void setVSync(bool enabled);
unsigned int getFramerateLimit() const { return framerate_limit; }
void setFramerateLimit(unsigned int limit);
// Viewport system
void setGameResolution(unsigned int width, unsigned int height);
sf::Vector2u getGameResolution() const { return gameResolution; }
void setViewportMode(ViewportMode mode);
ViewportMode getViewportMode() const { return viewportMode; }
std::string getViewportModeString() const;
sf::Vector2f windowToGameCoords(const sf::Vector2f& windowPos) const;
// Shader system (#106) - shared intermediate texture for shader rendering
static sf::RenderTexture& getShaderIntermediate();
static void initShaderIntermediate(unsigned int width, unsigned int height);
static bool isShaderIntermediateReady() { return shaderIntermediateInitialized; }
// #153 - Headless simulation control
float step(float dt = -1.0f); // Advance simulation; dt<0 means advance to next event
int getSimulationTime() const { return simulation_time; }
void renderScene(); // Force render current scene (for synchronous screenshot)
// #219 - Thread synchronization for background threads
FrameLock& getFrameLock() { return frameLock; }
bool isMainThread() const { return std::this_thread::get_id() == main_thread_id; }
// global textures for scripts to access
std::vector<IndexTexture> textures;
// global audio storage
std::vector<sf::SoundBuffer> sfxbuffers;
sf::Music music;
sf::Sound sfx;
std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> scene_ui(std::string scene);
};
/**
* @brief Visual overlay that displays real-time profiling metrics
*/
class GameEngine::ProfilerOverlay {
private:
sf::Font& font;
sf::Text text;
sf::RectangleShape background;
bool visible;
int updateInterval;
int frameCounter;
sf::Color getPerformanceColor(float frameTimeMs);
std::string formatFloat(float value, int precision = 1);
std::string formatPercentage(float part, float total);
public:
ProfilerOverlay(sf::Font& fontRef);
void toggle();
void setVisible(bool vis);
bool isVisible() const;
void update(const ProfilingMetrics& metrics);
void render(sf::RenderTarget& target);
};