# McRogueFace Emscripten & Renderer Abstraction Research **Date**: 2026-01-30 **Branch**: `emscripten-mcrogueface` **Related Issues**: #157 (True Headless), #158 (Emscripten/WASM) ## Executive Summary This document analyzes the technical requirements for: 1. **SFML 2.6 → 3.0 migration** (modernization) 2. **Emscripten/WebAssembly compilation** (browser deployment) Both goals share a common prerequisite: **renderer abstraction**. The codebase already has a partial abstraction via `sf::RenderTarget*` pointer, but SFML types are pervasive (1276 occurrences across 78 files). **Key Insight**: This is a **build-time configuration**, not runtime switching. The standard McRogueFace binary remains a dynamic environment; Emscripten builds bundle assets and scripts at compile time. --- ## Current Architecture Analysis ### Existing Abstraction Strengths 1. **RenderTarget Pointer Pattern** (`GameEngine.h:156`) ```cpp sf::RenderTarget* render_target; // Points to either window.get() or headless_renderer->getRenderTarget() ``` This already decouples rendering logic from the specific backend. 2. **HeadlessRenderer** (`src/HeadlessRenderer.h`) - Uses `sf::RenderTexture` internally - Provides unified interface: `getRenderTarget()`, `display()`, `saveScreenshot()` - Demonstrates the pattern for additional backends 3. **UIDrawable Hierarchy** - Virtual `render(sf::Vector2f, sf::RenderTarget&)` method - 7 drawable types: Frame, Caption, Sprite, Entity, Grid, Line, Circle, Arc - Each manages its own SFML primitives internally 4. **Asset Wrappers** - `PyTexture`, `PyFont`, `PyShader` wrap SFML types - Python reference counting integrated - Single point of change for asset loading APIs ### Current SFML Coupling Points | Area | Count | Difficulty | Notes | |------|-------|------------|-------| | `sf::Vector2f` | ~200+ | Medium | Used everywhere for positions, sizes | | `sf::Color` | ~100+ | Easy | Simple 4-byte struct replacement | | `sf::FloatRect` | ~50+ | Medium | Bounds, intersection testing | | `sf::RenderTexture` | ~20 | Hard | Shader effects, caching | | `sf::Sprite/Text` | ~30 | Hard | Core rendering primitives | | `sf::Event` | ~15 | Medium | Input system coupling | | `sf::Keyboard/Mouse` | ~50+ | Easy | Enum mappings | Total: **1276 occurrences across 78 files** --- ## SFML 3.0 Migration Analysis ### Breaking Changes Requiring Code Updates #### 1. Vector Parameters (High Impact) ```cpp // SFML 2.6 setPosition(10, 20); sf::VideoMode(1024, 768, 32); sf::FloatRect(x, y, w, h); // SFML 3.0 setPosition({10, 20}); sf::VideoMode({1024, 768}, 32); sf::FloatRect({x, y}, {w, h}); ``` **Strategy**: Regex-based search/replace with manual verification. #### 2. Rect Member Changes (Medium Impact) ```cpp // SFML 2.6 rect.left, rect.top, rect.width, rect.height rect.getPosition(), rect.getSize() // SFML 3.0 rect.position.x, rect.position.y, rect.size.x, rect.size.y rect.position, rect.size // direct access rect.findIntersection() -> std::optional> ``` #### 3. Resource Constructors (Low Impact) ```cpp // SFML 2.6 sf::Sound sound; // default constructible sound.setBuffer(buffer); // SFML 3.0 sf::Sound sound(buffer); // requires buffer at construction ``` #### 4. Keyboard/Mouse Enum Scoping (Medium Impact) ```cpp // SFML 2.6 sf::Keyboard::A sf::Mouse::Left // SFML 3.0 sf::Keyboard::Key::A sf::Mouse::Button::Left ``` #### 5. Event Handling (Medium Impact) ```cpp // SFML 2.6 sf::Event event; while (window.pollEvent(event)) { if (event.type == sf::Event::Closed) ... } // SFML 3.0 while (auto event = window.pollEvent()) { if (event->is()) ... } ``` #### 6. CMake Target Changes ```cmake # SFML 2.6 find_package(SFML 2 REQUIRED COMPONENTS graphics audio) target_link_libraries(app sfml-graphics sfml-audio) # SFML 3.0 find_package(SFML 3 REQUIRED COMPONENTS Graphics Audio) target_link_libraries(app SFML::Graphics SFML::Audio) ``` ### Migration Effort Estimate | Phase | Files | Changes | Effort | |-------|-------|---------|--------| | CMakeLists.txt | 1 | Target names | 1 hour | | Vector parameters | 30+ | ~200 calls | 4-8 hours | | Rect refactoring | 20+ | ~50 usages | 2-4 hours | | Event handling | 5 | ~15 sites | 2 hours | | Keyboard/Mouse | 10 | ~50 enums | 2 hours | | Resource constructors | 10 | ~30 sites | 2 hours | | **Total** | - | - | **~15-25 hours** | --- ## Emscripten/VRSFML Analysis ### Why VRSFML Over Waiting for SFML 4.x? 1. **Available Now**: VRSFML is working today with browser demos 2. **Modern OpenGL**: Removes legacy calls, targets OpenGL ES 3.0+ (WebGL 2) 3. **SFML_GAME_LOOP Macro**: Handles blocking vs callback loop abstraction 4. **Performance**: 500k sprites @ 60FPS vs 3 FPS upstream (batching) 5. **SFML 4.x Timeline**: Unknown, potentially years away ### VRSFML API Differences from SFML | Feature | SFML 2.6/3.0 | VRSFML | |---------|--------------|--------| | Default constructors | Allowed | Not allowed for resources | | Texture ownership | Pointer in Sprite | Passed at draw time | | Context management | Hidden global | Explicit `GraphicsContext` | | Drawable base class | Polymorphic | Removed | | Loading methods | `loadFromFile()` returns bool | Returns `std::optional` | | Main loop | `while(running)` | `SFML_GAME_LOOP { }` | ### Main Loop Refactoring Current blocking loop: ```cpp void GameEngine::run() { while (running) { processEvents(); update(); render(); display(); } } ``` Emscripten-compatible pattern: ```cpp // Option A: VRSFML macro SFML_GAME_LOOP { processEvents(); update(); render(); display(); } // Option B: Manual Emscripten integration #ifdef __EMSCRIPTEN__ void mainLoopCallback() { if (!game.running) { emscripten_cancel_main_loop(); return; } game.doFrame(); } emscripten_set_main_loop(mainLoopCallback, 0, 1); #else while (running) { doFrame(); } #endif ``` **Recommendation**: Use preprocessor-based approach with `doFrame()` extraction for cleaner separation. --- ## Build-Time Configuration Strategy ### Normal Build (Desktop) - Dynamic loading of assets from `assets/` directory - Python scripts loaded from `scripts/` directory at runtime - Full McRogueFace environment with dynamic game loading ### Emscripten Build (Web) - Assets bundled via `--preload-file assets` - Scripts bundled via `--preload-file scripts` - Virtual filesystem (MEMFS/IDBFS) - Optional: Script linting with Pyodide before bundling - Single-purpose deployment (one game per build) ### CMake Configuration ```cmake option(MCRF_BUILD_EMSCRIPTEN "Build for Emscripten/WebAssembly" OFF) if(MCRF_BUILD_EMSCRIPTEN) set(CMAKE_TOOLCHAIN_FILE ${CMAKE_SOURCE_DIR}/cmake/toolchains/emscripten.cmake) add_definitions(-DMCRF_EMSCRIPTEN) # Bundle assets set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} \ --preload-file ${CMAKE_SOURCE_DIR}/assets@/assets \ --preload-file ${CMAKE_SOURCE_DIR}/scripts@/scripts") endif() ``` --- ## Phased Implementation Plan ### Phase 0: Preparation (This PR) - [ ] Create `docs/EMSCRIPTEN_RESEARCH.md` (this document) - [ ] Update Gitea issues #157, #158 with findings - [ ] Identify specific files requiring changes - [ ] Create test matrix for rendering features ### Phase 1: Type Abstraction Layer **Goal**: Isolate SFML types behind McRogueFace wrappers ```cpp // src/types/McrfTypes.h namespace mcrf { using Vector2f = sf::Vector2f; // Alias initially, replace later using Color = sf::Color; using FloatRect = sf::FloatRect; } ``` Changes: - [ ] Create `src/types/` directory with wrapper types - [ ] Gradually replace `sf::` with `mcrf::` namespace - [ ] Update Common.h to provide both namespaces during transition ### Phase 2: Main Loop Extraction **Goal**: Make game loop callback-compatible - [ ] Extract `GameEngine::doFrame()` from `run()` - [ ] Add `#ifdef __EMSCRIPTEN__` conditional in `run()` - [ ] Test that desktop behavior is unchanged ### Phase 3: Render Backend Interface **Goal**: Abstract RenderTarget operations ```cpp class RenderBackend { public: virtual ~RenderBackend() = default; virtual void clear(const Color& color) = 0; virtual void draw(const Sprite& sprite) = 0; virtual void draw(const Text& text) = 0; virtual void display() = 0; virtual bool isOpen() const = 0; virtual Vector2u getSize() const = 0; }; class SFMLBackend : public RenderBackend { ... }; class VRSFMLBackend : public RenderBackend { ... }; // Future ``` ### Phase 4: SFML 3.0 Migration **Goal**: Update to SFML 3.0 API - [ ] Update CMakeLists.txt targets - [ ] Fix vector parameter calls - [ ] Fix rect member access - [ ] Fix event handling - [ ] Fix keyboard/mouse enums - [ ] Test thoroughly ### Phase 5: VRSFML Integration (Experimental) **Goal**: Add VRSFML as alternative backend - [ ] Add VRSFML as submodule/dependency - [ ] Implement VRSFMLBackend - [ ] Add Emscripten CMake configuration - [ ] Test in browser ### Phase 6: Python-in-WASM **Goal**: Get Python scripting working in browser **High Risk** - This is the major unknown: - [ ] Build CPython for Emscripten - [ ] Test `McRFPy_API` binding compatibility - [ ] Evaluate Pyodide vs raw CPython - [ ] Handle filesystem virtualization - [ ] Test threading limitations --- ## Risk Assessment | Risk | Probability | Impact | Mitigation | |------|-------------|--------|------------| | SFML 3.0 breaks unexpected code | Medium | Medium | Comprehensive test suite | | VRSFML API too different | Low | High | Can fork/patch VRSFML | | Python-in-WASM fails | Medium | Critical | Evaluate Pyodide early | | Performance regression | Low | Medium | Benchmark before/after | | Binary size too large | Medium | Medium | Lazy loading, stdlib trimming | --- ## References ### SFML 3.0 - [Migration Guide](https://www.sfml-dev.org/tutorials/3.0/getting-started/migrate/) - [Changelog](https://www.sfml-dev.org/development/changelog/) - [Release Notes](https://github.com/SFML/SFML/releases/tag/3.0.0) ### VRSFML/Emscripten - [VRSFML Blog Post](https://vittorioromeo.com/index/blog/vrsfml.html) - [VRSFML GitHub](https://github.com/vittorioromeo/VRSFML) - [Browser Demos](https://vittorioromeo.github.io/VRSFML_HTML5_Examples/) ### Python WASM - [PEP 776 - Python Emscripten Support](https://peps.python.org/pep-0776/) - [CPython WASM Build Guide](https://github.com/python/cpython/blob/main/Tools/wasm/README.md) - [Pyodide](https://github.com/pyodide/pyodide) ### Related Issues - [SFML Emscripten Discussion #1494](https://github.com/SFML/SFML/issues/1494) - [libtcod Emscripten #41](https://github.com/libtcod/libtcod/issues/41) --- ## Appendix A: File-by-File SFML Usage Inventory ### Critical Files (Must Abstract for Emscripten) | File | SFML Types Used | Role | Abstraction Difficulty | |------|-----------------|------|------------------------| | `GameEngine.h/cpp` | RenderWindow, Clock, Font, Event | Main loop, window | **CRITICAL** | | `HeadlessRenderer.h/cpp` | RenderTexture | Headless backend | **CRITICAL** | | `UIDrawable.h/cpp` | Vector2f, RenderTarget, FloatRect | Base render interface | **HARD** | | `UIFrame.h/cpp` | RectangleShape, Vector2f, Color | Container rendering | **HARD** | | `UISprite.h/cpp` | Sprite, Texture, Vector2f | Texture display | **HARD** | | `UICaption.h/cpp` | Text, Font, Vector2f, Color | Text rendering | **HARD** | | `UIGrid.h/cpp` | RenderTexture, Sprite, Vector2f | Tile grid system | **HARD** | | `UIEntity.h/cpp` | Sprite, Vector2f | Game entities | **HARD** | | `UICircle.h/cpp` | CircleShape, Vector2f, Color | Circle shape | **MEDIUM** | | `UILine.h/cpp` | VertexArray, Vector2f, Color | Line rendering | **MEDIUM** | | `UIArc.h/cpp` | CircleShape segments, Vector2f | Arc shape | **MEDIUM** | | `Scene.h/cpp` | Vector2f, RenderTarget | Scene management | **MEDIUM** | | `SceneTransition.h/cpp` | RenderTexture, Sprite | Transitions | **MEDIUM** | ### Wrapper Files (Already Partially Abstracted) | File | SFML Types Wrapped | Python API | Notes | |------|-------------------|------------|-------| | `PyVector.h/cpp` | sf::Vector2f | Vector | Ready for backend swap | | `PyColor.h/cpp` | sf::Color | Color | Ready for backend swap | | `PyTexture.h/cpp` | sf::Texture | Texture | Asset loading needs work | | `PyFont.h/cpp` | sf::Font | Font | Asset loading needs work | | `PyShader.h/cpp` | sf::Shader | Shader | Optional feature | ### Input System Files | File | SFML Types Used | Notes | |------|-----------------|-------| | `ActionCode.h` | Keyboard::Key, Mouse::Button | Enum encoding only | | `PyKey.h/cpp` | Keyboard::Key enum | 140+ key mappings | | `PyMouseButton.h/cpp` | Mouse::Button enum | Simple enum | | `PyKeyboard.h/cpp` | Keyboard::isKeyPressed | State queries | | `PyMouse.h/cpp` | Mouse::getPosition | Position queries | | `PyInputState.h/cpp` | None (pure enum) | No SFML dependency | ### Support Files (Low Priority) | File | SFML Types Used | Notes | |------|-----------------|-------| | `Animation.h/cpp` | Vector2f, Color (as values) | Pure data animation | | `GridLayers.h/cpp` | RenderTexture, Color | Layer caching | | `IndexTexture.h/cpp` | Texture, IntRect | Legacy texture format | | `Resources.h/cpp` | Font | Global font storage | | `ProfilerOverlay.cpp` | Text, RectangleShape | Debug overlay | | `McRFPy_Automation.h/cpp` | Various | Testing only | --- ## Appendix B: Recommended First Steps ### Immediate (Non-Breaking Changes) 1. **Extract `GameEngine::doFrame()`** - Move loop body to separate method - No API changes, just internal refactoring - Enables future Emscripten callback integration 2. **Create type aliases in Common.h** ```cpp namespace mcrf { using Vector2f = sf::Vector2f; using Vector2i = sf::Vector2i; using Color = sf::Color; using FloatRect = sf::FloatRect; } ``` - Allows gradual migration from `sf::` to `mcrf::` - No functional changes 3. **Document current render path** - Add comments to key rendering functions - Identify all `target.draw()` call sites - Create rendering flow diagram ### Short-Term (Preparation for SFML 3.0) 1. **Audit vector parameter calls** - Find all `setPosition(x, y)` style calls - Prepare regex patterns for migration 2. **Audit rect member access** - Find all `.left`, `.top`, `.width`, `.height` uses - Prepare for `.position.x`, `.size.x` style 3. **Test suite expansion** - Add rendering validation tests - Screenshot comparison tests - Animation correctness tests --- ## Appendix C: libtcod Architecture Analysis **Key Finding**: libtcod uses a much simpler abstraction pattern than initially proposed. ### libtcod's Context Vtable Pattern libtcod doesn't wrap every SDL type. Instead, it abstracts at the **context level** using a C-style vtable: ```c struct TCOD_Context { int type; void* contextdata_; // Backend-specific data (opaque pointer) // Function pointers - the "vtable" void (*c_destructor_)(struct TCOD_Context* self); TCOD_Error (*c_present_)(struct TCOD_Context* self, const TCOD_Console* console, const TCOD_ViewportOptions* viewport); void (*c_pixel_to_tile_)(struct TCOD_Context* self, double* x, double* y); TCOD_Error (*c_save_screenshot_)(struct TCOD_Context* self, const char* filename); struct SDL_Window* (*c_get_sdl_window_)(struct TCOD_Context* self); TCOD_Error (*c_set_tileset_)(struct TCOD_Context* self, TCOD_Tileset* tileset); TCOD_Error (*c_screen_capture_)(struct TCOD_Context* self, ...); // ... more operations }; ``` ### How Backends Implement It Each renderer fills in the function pointers: ```c // In renderer_sdl2.c context->c_destructor_ = sdl2_destructor; context->c_present_ = sdl2_present; context->c_get_sdl_window_ = sdl2_get_window; // ... // In renderer_xterm.c context->c_destructor_ = xterm_destructor; context->c_present_ = xterm_present; // ... ``` ### Conditional Compilation with NO_SDL libtcod uses simple preprocessor guards: ```c // In CMakeLists.txt if(LIBTCOD_SDL3) target_link_libraries(${PROJECT_NAME} PUBLIC SDL3::SDL3) else() target_compile_definitions(${PROJECT_NAME} PUBLIC NO_SDL) endif() // In source files #ifndef NO_SDL #include // ... SDL-dependent code ... #endif ``` **47 files** use this pattern. When building headless, SDL code is simply excluded. ### Why This Pattern Works 1. **Core functionality is SDL-independent**: Console manipulation, pathfinding, FOV, noise, BSP, etc. don't need SDL 2. **Only rendering needs abstraction**: The `TCOD_Context` is the single point of abstraction 3. **Minimal API surface**: Just ~10 function pointers instead of wrapping every primitive 4. **Backend-specific data is opaque**: `contextdata_` holds renderer-specific state ### Implications for McRogueFace **libtcod's approach suggests we should NOT try to abstract every `sf::` type.** Instead, consider: 1. **Keep SFML types internally** - `sf::Vector2f`, `sf::Color`, `sf::FloatRect` are fine 2. **Abstract at the RenderContext level** - One vtable for window/rendering operations 3. **Use `#ifndef NO_SFML` guards** - Compile-time backend selection 4. **Create alternative backend for Emscripten** - WebGL + canvas implementation ### Proposed McRogueFace Context Pattern ```cpp struct McRF_RenderContext { void* backend_data; // SFML or WebGL specific data // Function pointers void (*destroy)(McRF_RenderContext* self); void (*clear)(McRF_RenderContext* self, uint32_t color); void (*present)(McRF_RenderContext* self); void (*draw_sprite)(McRF_RenderContext* self, const Sprite* sprite); void (*draw_text)(McRF_RenderContext* self, const Text* text); void (*draw_rect)(McRF_RenderContext* self, const Rect* rect); bool (*poll_event)(McRF_RenderContext* self, Event* event); void (*screenshot)(McRF_RenderContext* self, const char* path); // ... }; // SFML backend McRF_RenderContext* mcrf_sfml_context_new(int width, int height, const char* title); // Emscripten backend (future) McRF_RenderContext* mcrf_webgl_context_new(const char* canvas_id); ``` ### Comparison: Original Plan vs libtcod-Inspired Plan | Aspect | Original Plan | libtcod-Inspired Plan | |--------|---------------|----------------------| | Type abstraction | Replace all `sf::*` with `mcrf::*` | Keep `sf::*` internally | | Abstraction point | Every primitive type | Single Context object | | Files affected | 78+ files | ~10 core files | | Compile-time switching | Complex namespace aliasing | Simple `#ifndef NO_SFML` | | Backend complexity | Full reimplementation | Focused vtable | **Recommendation**: Adopt libtcod's simpler pattern. Focus abstraction on the rendering context, not on data types. --- ## Appendix D: Headless Build Experiment Results **Experiment Date**: 2026-01-30 **Branch**: `emscripten-mcrogueface` ### Objective Attempt to compile McRogueFace without SFML dependencies to identify true coupling points. ### What We Created 1. **`src/platform/HeadlessTypes.h`** - Complete SFML type stubs (~600 lines): - Vector2f, Vector2i, Vector2u - Color with standard color constants - FloatRect, IntRect - Time, Clock (with chrono-based implementation) - Transform, Vertex, View - Shape hierarchy (RectangleShape, CircleShape, etc.) - Texture, Sprite, Font, Text stubs - RenderTarget, RenderTexture, RenderWindow stubs - Audio stubs (Sound, Music, SoundBuffer) - Input stubs (Keyboard, Mouse, Event) - Shader stub 2. **Modified `src/Common.h`** - Conditional include: ```cpp #ifdef MCRF_HEADLESS #include "platform/HeadlessTypes.h" #else #include #include #endif ``` ### Build Attempt Result **SUCCESS** - Headless build compiles after consolidating includes and adding stubs. ### Work Completed #### 1. Consolidated SFML Includes **15 files** had direct SFML includes that bypassed Common.h. All were modified to use `#include "Common.h"` instead: | File | Original Include | Fixed | |------|------------------|-------| | `main.cpp` | `` | ✓ | | `Animation.h` | `` | ✓ | | `GridChunk.h` | `` | ✓ | | `GridLayers.h` | `` | ✓ | | `HeadlessRenderer.h` | `` | ✓ | | `SceneTransition.h` | `` | ✓ | | `McRFPy_Automation.h` | ``, `` | ✓ | | `PyWindow.cpp` | `` | ✓ | | `ActionCode.h` | `` | ✓ | | `PyKey.h` | `` | ✓ | | `PyMouseButton.h` | `` | ✓ | | `PyBSP.h` | `` | ✓ | | `UIGridPathfinding.h` | `` | ✓ | #### 2. Wrapped ImGui-SFML with Guards ImGui-SFML is disabled entirely in headless builds since debug tools can't be accessed through the API: | File | Changes | |------|---------| | `GameEngine.h` | Guarded includes and member variables | | `GameEngine.cpp` | Guarded all ImGui::SFML calls | | `ImGuiConsole.h/cpp` | Entire file wrapped with `#ifndef MCRF_HEADLESS` | | `ImGuiSceneExplorer.h/cpp` | Entire file wrapped with `#ifndef MCRF_HEADLESS` | | `McRFPy_API.cpp` | Guarded ImGuiConsole include and setEnabled call | #### 3. Extended HeadlessTypes.h The stub file grew from ~700 lines to ~900 lines with additional types and methods: **Types Added:** - `sf::Image` - For screenshot functionality - `sf::Glsl::Vec3`, `sf::Glsl::Vec4` - For shader uniforms - `sf::BlendMode` - For rendering states - `sf::CurrentTextureType` - For shader texture binding **Methods Added:** - `Font::Info` struct and `Font::getInfo()` - `Texture::update()` overloads - `Texture::copyToImage()` - `Transform::getInverse()` - `RenderStates` constructors from Transform, BlendMode, Shader* - `Music::getDuration()`, `getPlayingOffset()`, `setPlayingOffset()` - `SoundBuffer::getDuration()` - `RenderWindow::setMouseCursorGrabbed()` - `sf::err()` stream function - Keyboard aliases: `BackSpace`, `BackSlash`, `SemiColon`, `Dash` ### Build Commands ```bash # Normal SFML build (default) make # Headless build (no SFML dependency) mkdir build-headless && cd build-headless cmake .. -DCMAKE_CXX_FLAGS="-DMCRF_HEADLESS" -DCMAKE_BUILD_TYPE=Debug make ``` ### Key Insight The libtcod approach of `#ifndef NO_SDL` guards works when **all platform includes go through a single point**. The consolidation of 15+ bypass points into Common.h was the prerequisite that made this work. ### Actual Effort | Task | Files | Time | |------|-------|------| | Replace direct SFML includes with Common.h | 15 | ~30 min | | Wrap ImGui-SFML in guards | 5 | ~20 min | | Extend HeadlessTypes.h with missing stubs | 1 | ~1 hour | | Fix compilation errors iteratively | - | ~1 hour | **Total**: ~3 hours for clean headless compilation ### Next Steps 1. **Test Python bindings** - Ensure mcrfpy module loads in headless mode 2. **Add CMake option** - `option(MCRF_HEADLESS "Build without graphics" OFF)` 3. **Link-time validation** - Verify no SFML symbols are referenced 4. **Emscripten testing** - Try building with emcc