Build produces mcrogueface.wasm (8.9MB) + mcrogueface.js (126KB): - All 68 C++ source files compile with emcc - Links Python 3.14 (wasm32-emscripten target) - Links libtcod-headless (built for Emscripten) - Uses Emscripten ports: zlib, bzip2, sqlite3 - Includes HACL crypto, expat, mpdec, ffi dependencies CMakeLists.txt updates: - Add HACL .o files (not included in libpython3.14.a) - Add expat, mpdec, ffi static libraries from Python build - Add libtcod WASM build with lodepng and utf8proc - Add Emscripten port link options libtcod-headless submodule updated with Emscripten build. Next: Bundle Python stdlib into WASM filesystem. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
28 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/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
- ✅ Test Python bindings - mcrfpy module loads and works in headless mode
- Vector, Color, Scene, Frame, Grid all functional
- libtcod integrations (BSP, pathfinding) available
- ✅ Add CMake option -
option(MCRF_HEADLESS "Build without graphics" OFF)- Proper conditional compilation and linking
- No SFML symbols in headless binary
- ✅ Link-time validation -
lddconfirms zero SFML/OpenGL dependencies - ✅ Binary size reduction - Headless is 1.6 MB vs 2.5 MB normal build (36% smaller)
Python Test Results (Headless Mode)
# 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
- ✅ Main loop extraction -
GameEngine::doFrame()extracted with Emscripten callback supportrun()now uses#ifdef __EMSCRIPTEN__to choose between callback and blocking loopemscripten_set_main_loop_arg()integration ready
- ✅ Emscripten toolchain -
emcmake cmakeworks with headless mode - ✅ Python-in-WASM - Built CPython 3.14.2 for wasm32-emscripten target
- Uses official
Tools/wasm/emscripten buildscript from CPython repo - Produced libpython3.14.a (47MB static library)
- Also builds: libmpdec, libffi, libexpat for WASM
- Uses official
- ✅ libtcod-in-WASM - Built libtcod-headless for Emscripten
- Uses
LIBTCOD_SDL3=OFFto avoid SDL dependency - Includes lodepng and utf8proc dependencies
- Uses
- ✅ First successful WASM build - mcrogueface.wasm (8.9MB) + mcrogueface.js (126KB)
- All 68 C++ source files compile with emcc
- Links: Python, libtcod, HACL crypto, expat, mpdec, ffi, zlib, bzip2, sqlite3
- 🔲 Python stdlib bundling - Need to package Python stdlib for WASM filesystem
- 🔲 VRSFML integration - Replace stubs with actual WebGL rendering
First Emscripten Build Attempt (2026-01-31)
Command:
source ~/emsdk/emsdk_env.sh
emcmake cmake .. -DMCRF_HEADLESS=ON -DCMAKE_BUILD_TYPE=Release
emmake make -j8
Result: Build failed on Python headers.
Key Errors:
deps/Python/pyport.h:429:2: error: "LONG_BIT definition appears wrong for platform"
warning: shift count >= width of type [-Wshift-count-overflow]
_Py_STATIC_FLAG_BITS << 48 // 48-bit shift on 32-bit WASM!
Root Cause:
- Desktop Python 3.14 headers assume 64-bit Linux with glibc
- Emscripten targets 32-bit WASM with musl-based libc
- Python's immortal reference counting uses
<< 48shifts that overflow on 32-bit LONG_BITcheck fails because WASM'slongis 32 bits
Analysis: The HeadlessTypes.h stubs and game engine code compile fine. The blocker is exclusively the Python C API integration.
Python-in-WASM Options
| Option | Complexity | Description |
|---|---|---|
| Pyodide | Medium | Pre-built Python WASM with package ecosystem |
| CPython WASM | High | Build CPython ourselves with Emscripten |
| No-Python mode | Low | New CMake option to exclude Python entirely |
Pyodide Approach (Recommended):
- Pyodide provides Python 3.12 compiled for WASM
- Would need to replace
deps/Pythonwith Pyodide headers McRFPy_APIbinding layer needs adaptation- Pyodide handles asyncio, file system virtualization
- Active project with good documentation
CPython WASM Build (Successful!)
Date: 2026-01-31
Used the official CPython WASM build process:
# From deps/cpython directory
./Tools/wasm/emscripten build
# This produces:
# - cross-build/wasm32-emscripten/build/python/libpython3.14.a
# - cross-build/wasm32-emscripten/prefix/lib/libmpdec.a
# - cross-build/wasm32-emscripten/prefix/lib/libffi.a
# - cross-build/wasm32-emscripten/build/python/Modules/expat/libexpat.a
CMake Integration:
if(EMSCRIPTEN)
set(PYTHON_WASM_BUILD "${CMAKE_SOURCE_DIR}/deps/cpython/cross-build/wasm32-emscripten/build/python")
set(PYTHON_WASM_PREFIX "${CMAKE_SOURCE_DIR}/deps/cpython/cross-build/wasm32-emscripten/prefix")
# Force WASM-compatible pyconfig.h
add_compile_options(-include ${PYTHON_WASM_BUILD}/pyconfig.h)
# Link all Python dependencies
set(LINK_LIBS
${PYTHON_WASM_BUILD}/libpython3.14.a
${PYTHON_WASM_BUILD}/Modules/_hacl/*.o # HACL crypto not in libpython
${PYTHON_WASM_BUILD}/Modules/expat/libexpat.a
${PYTHON_WASM_PREFIX}/lib/libmpdec.a
${PYTHON_WASM_PREFIX}/lib/libffi.a
)
# Emscripten ports for common libraries
target_link_options(mcrogueface PRIVATE
-sUSE_ZLIB=1
-sUSE_BZIP2=1
-sUSE_SQLITE3=1
)
endif()
No-Python Mode (For Testing):
- Add
MCRF_NO_PYTHONCMake option - Allows testing WASM build without Python complexity
- Game engine would be pure C++ (no scripting)
- Useful for validating rendering, input, timing first
Main Loop Architecture
The game loop now supports both desktop (blocking) and browser (callback) modes:
// 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();
}