This commit enables McRogueFace to compile without SFML dependencies when built with -DMCRF_HEADLESS, a prerequisite for Emscripten/WebAssembly support. Changes: - Add src/platform/HeadlessTypes.h (~900 lines of SFML type stubs) - Consolidate all SFML includes through src/Common.h (15 files fixed) - Wrap ImGui-SFML with #ifndef MCRF_HEADLESS guards - Disable debug console/explorer in headless builds - Add comprehensive research document: docs/EMSCRIPTEN_RESEARCH.md The headless build compiles successfully but uses stub implementations that return failure/no-op. This proves the abstraction boundary is clean and enables future work on alternative backends (VRSFML, Emscripten). What still works in headless mode: - Python interpreter and script execution - libtcod integrations (pathfinding, FOV, noise, BSP, heightmaps) - Timer system and scene management - All game logic and data structures Build commands: Normal: make Headless: cmake .. -DCMAKE_CXX_FLAGS="-DMCRF_HEADLESS" && make Addresses #158 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
23 KiB
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:
- SFML 2.6 → 3.0 migration (modernization)
- 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
-
RenderTarget Pointer Pattern (
GameEngine.h:156)sf::RenderTarget* render_target; // Points to either window.get() or headless_renderer->getRenderTarget()This already decouples rendering logic from the specific backend.
-
HeadlessRenderer (
src/HeadlessRenderer.h)- Uses
sf::RenderTextureinternally - Provides unified interface:
getRenderTarget(),display(),saveScreenshot() - Demonstrates the pattern for additional backends
- Uses
-
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
- Virtual
-
Asset Wrappers
PyTexture,PyFont,PyShaderwrap 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)
// 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)
// 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<Rect<T>>
3. Resource Constructors (Low Impact)
// 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)
// 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)
// 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<sf::Event::Closed>()) ...
}
6. CMake Target Changes
# 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?
- Available Now: VRSFML is working today with browser demos
- Modern OpenGL: Removes legacy calls, targets OpenGL ES 3.0+ (WebGL 2)
- SFML_GAME_LOOP Macro: Handles blocking vs callback loop abstraction
- Performance: 500k sprites @ 60FPS vs 3 FPS upstream (batching)
- 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:
void GameEngine::run() {
while (running) {
processEvents();
update();
render();
display();
}
}
Emscripten-compatible pattern:
// 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
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
// 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::withmcrf::namespace - Update Common.h to provide both namespaces during transition
Phase 2: Main Loop Extraction
Goal: Make game loop callback-compatible
- Extract
GameEngine::doFrame()fromrun() - Add
#ifdef __EMSCRIPTEN__conditional inrun() - Test that desktop behavior is unchanged
Phase 3: Render Backend Interface
Goal: Abstract RenderTarget operations
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_APIbinding 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
VRSFML/Emscripten
Python WASM
Related Issues
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)
-
Extract
GameEngine::doFrame()- Move loop body to separate method
- No API changes, just internal refactoring
- Enables future Emscripten callback integration
-
Create type aliases in Common.h
namespace mcrf { using Vector2f = sf::Vector2f; using Vector2i = sf::Vector2i; using Color = sf::Color; using FloatRect = sf::FloatRect; }- Allows gradual migration from
sf::tomcrf:: - No functional changes
- Allows gradual migration from
-
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)
-
Audit vector parameter calls
- Find all
setPosition(x, y)style calls - Prepare regex patterns for migration
- Find all
-
Audit rect member access
- Find all
.left,.top,.width,.heightuses - Prepare for
.position.x,.size.xstyle
- Find all
-
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:
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:
// 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:
// 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 <SDL3/SDL.h>
// ... SDL-dependent code ...
#endif
47 files use this pattern. When building headless, SDL code is simply excluded.
Why This Pattern Works
- Core functionality is SDL-independent: Console manipulation, pathfinding, FOV, noise, BSP, etc. don't need SDL
- Only rendering needs abstraction: The
TCOD_Contextis the single point of abstraction - Minimal API surface: Just ~10 function pointers instead of wrapping every primitive
- 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:
- Keep SFML types internally -
sf::Vector2f,sf::Color,sf::FloatRectare fine - Abstract at the RenderContext level - One vtable for window/rendering operations
- Use
#ifndef NO_SFMLguards - Compile-time backend selection - Create alternative backend for Emscripten - WebGL + canvas implementation
Proposed McRogueFace Context Pattern
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
-
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
-
Modified
src/Common.h- Conditional include:#ifdef MCRF_HEADLESS #include "platform/HeadlessTypes.h" #else #include <SFML/Graphics.hpp> #include <SFML/Audio.hpp> #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 |
<SFML/Graphics.hpp> |
✓ |
Animation.h |
<SFML/Graphics.hpp> |
✓ |
GridChunk.h |
<SFML/Graphics.hpp> |
✓ |
GridLayers.h |
<SFML/Graphics.hpp> |
✓ |
HeadlessRenderer.h |
<SFML/Graphics.hpp> |
✓ |
SceneTransition.h |
<SFML/Graphics.hpp> |
✓ |
McRFPy_Automation.h |
<SFML/Graphics.hpp>, <SFML/Window.hpp> |
✓ |
PyWindow.cpp |
<SFML/Graphics.hpp> |
✓ |
ActionCode.h |
<SFML/Window/Keyboard.hpp> |
✓ |
PyKey.h |
<SFML/Window/Keyboard.hpp> |
✓ |
PyMouseButton.h |
<SFML/Window/Mouse.hpp> |
✓ |
PyBSP.h |
<SFML/System/Vector2.hpp> |
✓ |
UIGridPathfinding.h |
<SFML/System/Vector2.hpp> |
✓ |
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 functionalitysf::Glsl::Vec3,sf::Glsl::Vec4- For shader uniformssf::BlendMode- For rendering statessf::CurrentTextureType- For shader texture binding
Methods Added:
Font::Infostruct andFont::getInfo()Texture::update()overloadsTexture::copyToImage()Transform::getInverse()RenderStatesconstructors from Transform, BlendMode, Shader*Music::getDuration(),getPlayingOffset(),setPlayingOffset()SoundBuffer::getDuration()RenderWindow::setMouseCursorGrabbed()sf::err()stream function- Keyboard aliases:
BackSpace,BackSlash,SemiColon,Dash
Build Commands
# 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
- Test Python bindings - Ensure mcrfpy module loads in headless mode
- Add CMake option -
option(MCRF_HEADLESS "Build without graphics" OFF) - Link-time validation - Verify no SFML symbols are referenced
- Emscripten testing - Try building with emcc