McRogueFace/docs/EMSCRIPTEN_RESEARCH.md

756 lines
24 KiB
Markdown
Raw Normal View History

# 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<Rect<T>>
```
#### 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<sf::Event::Closed>()) ...
}
```
#### 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 <SDL3/SDL.h>
// ... 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 <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 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/ImGui dependencies)
mkdir build-headless && cd build-headless
cmake .. -DMCRF_HEADLESS=ON -DCMAKE_BUILD_TYPE=Release
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
### Completed Milestones
1.**Test Python bindings** - mcrfpy module loads and works in headless mode
- Vector, Color, Scene, Frame, Grid all functional
- libtcod integrations (BSP, pathfinding) available
2.**Add CMake option** - `option(MCRF_HEADLESS "Build without graphics" OFF)`
- Proper conditional compilation and linking
- No SFML symbols in headless binary
3.**Link-time validation** - `ldd` confirms zero SFML/OpenGL dependencies
4.**Binary size reduction** - Headless is 1.6 MB vs 2.5 MB normal build (36% smaller)
### Python Test Results (Headless Mode)
```python
# All these work in headless build:
import mcrfpy
v = mcrfpy.Vector(10, 20) # ✅
c = mcrfpy.Color(255, 128, 64) # ✅
scene = mcrfpy.Scene('test') # ✅
frame = mcrfpy.Frame(pos=(0,0)) # ✅
grid = mcrfpy.Grid(grid_size=(10,10)) # ✅
```
### Remaining Steps for Emscripten
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();
}
```