[Major Feature] WebAssembly/Emscripten build target for browser deployment #158
Labels
No labels
Alpha Release Requirement
Bugfix
Demo Target
Documentation
Major Feature
Minor Feature
priority:tier1-active
priority:tier2-foundation
priority:tier3-future
priority:tier4-deferred
Refactoring & Cleanup
system:animation
system:documentation
system:grid
system:input
system:performance
system:procgen
system:python-binding
system:rendering
system:ui-hierarchy
Tiny Feature
workflow:blocked
workflow:needs-benchmark
workflow:needs-documentation
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Depends on
Reference
john/McRogueFace#158
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Vision
Compile McRogueFace to WebAssembly, enabling:
Dependency Analysis
Major Technical Hurdles
1. SFML Replacement Required
SFML explicitly declined Emscripten support for 3.x (deferred to 4.x with potential Vulkan backend).
Options:
2. Game Loop Architecture Change
Browsers require cooperative multitasking — no blocking
while(true)loops.Requires refactoring
GameEngine::run()to support both models.3. Python-in-WASM Integration
CPython compiles to WASM (PEP 776, build guide), but with limitations:
Key concerns:
McRFPy_API) needs adaptation4. Filesystem Virtualization
All file I/O must go through Emscripten's virtual filesystem:
--preload-fileor asyncfetch()scripts/directory bundled into WASMfopen()becomes async or pre-bundled5. Asset Loading & Binary Size
Requires asset optimization strategy (lazy loading, compression, LOD).
Implementation Strategy
Phase 1: Renderer Abstraction (see #157)
Create
RenderBackendinterface that decouples from SFML:SFMLBackend— current desktop implementationSoftwareBackend— for true headless (#157)VRSFMLBackendorSMKBackend— for WASMThis is shared prerequisite work with #157.
Phase 2: VRSFML Evaluation
Phase 3: Python-in-WASM Prototype
McRFPy_APIbinding approach worksPhase 4: Game Loop Refactor
emscripten_set_main_loopPhase 5: Asset Pipeline
Phase 6: Build System
Relationship to Other Work
Scope Assessment
This is a v2.0+ project requiring:
The renderer abstraction work (#157) is the highest-leverage first step — it unblocks both headless and WASM paths.
References
Research Complete: 2026-01-30
Comprehensive analysis conducted on branch
emscripten-mcrogueface. Full report:docs/EMSCRIPTEN_RESEARCH.mdKey Findings
SFML Coupling Analysis:
sf::types across 78 filessf::RenderTarget*pointer patternHeadlessRendererdemonstrates the backend abstraction patternAbstraction Difficulty by Type:
SFML 3.0 Migration
Estimated 15-25 hours of refactoring work:
setPosition(x, y)→setPosition({x, y}).left→.position.xsf::Keyboard::A→sf::Keyboard::Key::ApollEvent(event)→auto event = pollEvent()VRSFML as Emscripten Path
VRSFML (Vittorio Romeo's fork) provides:
SFML_GAME_LOOPmacro for callback-based main loopKey API differences:
GraphicsContextmanagementBuild-Time Strategy Confirmed
User confirmed this is build-time configuration:
--preload-fileRecommended Phased Approach
mcrf::namespace aliases)GameEngine::doFrame()for callback compatibilityCritical Risk: Python-in-WASM
This remains the biggest unknown. CPython compiles to WASM (PEP 776), but:
McRFPy_APIbinding layer needs testingRecommend early Pyodide evaluation before committing to VRSFML work.
libtcod Architecture Analysis: Simpler Approach Discovered
Examined
modules/libtcod-headlessto understand how libtcod handles renderer abstraction.Key Finding: Context Vtable Pattern
libtcod does NOT wrap every SDL type. Instead, it uses a single C-style vtable for the rendering context:
Each backend (SDL2, xterm) fills in these function pointers.
Conditional Compilation
libtcod uses simple
#ifndef NO_SDLguards in 47 files. WhenLIBTCOD_SDL3=OFF:SDL-dependent code is simply excluded at compile time.
Why This Works
TCOD_Contextvtable handles itcontextdata_holds renderer-specific stateRevised Recommendation for McRogueFace
Don't abstract every
sf::type. Abstract at the RenderContext level.sf::*occurrencessf::*internally#ifndef NO_SFMLProposed McRogueFace Pattern
This dramatically simplifies the implementation path while achieving the same goal.
Phase 1 Progress: MCRF_HEADLESS Compile-Time Build Option
Commit:
7621ae3on branchemscripten-mcroguefaceWhat Was Accomplished
McRogueFace can now compile without any SFML dependencies when built with
-DMCRF_HEADLESS. This is a critical prerequisite for Emscripten/WebAssembly support.Changes Made
src/platform/HeadlessTypes.h(~900 lines)Common.h#ifndef MCRF_HEADLESSdocs/EMSCRIPTEN_RESEARCH.mdBuild Commands
What Still Works in Headless Mode ✅
The headless build uses stub implementations for graphics, but all non-rendering functionality remains fully operational:
What Doesn't Work (Stubs Return Failure) ❌
Texture::loadFromFile()falseImage::saveToFile()falseFont::loadFromFile()falseRenderTarget::draw()Key Architectural Insight
Studied libtcod-headless's abstraction pattern (see Appendix C in research doc). libtcod uses a C-style vtable with ~10 function pointers in
TCOD_Context, not wrapper classes around every type. This informed the approach:Updated Checklist
From the original issue's implementation strategy:
Next Steps
option(MCRF_HEADLESS "Build without graphics" OFF)Effort
~3 hours total for clean headless compilation, significantly less than the 1-2 days estimated in the research phase.
New Use Case: McRogueFace Server (Node.js for Roguelikes)
A chance encounter with inspiration while working on the headless build:
The Node.js Parallel
Node.js took JavaScript—a browser language—and made it run server-side. The
MCRF_HEADLESSbuild does the same for McRogueFace: identical Python game scripts running without a display.Authoritative Game Server
The headless build has everything needed to be the source of truth for a networked roguelike:
What This Enables
Anti-Cheat / Rules Enforcement
Massively Parallel Simulation
Deterministic Replay Verification
CI/CD Integration
Xvfbno longer needed)Architecture Vision
All three targets run identical game logic. The only difference is the rendering backend.
Relationship to Existing Work
This vision strengthens the case for the headless build beyond just Emscripten:
All three use cases benefit from the same renderer abstraction work.
The Roguelike Advantage
Roguelikes are uniquely suited for this architecture:
The
MCRF_HEADLESSbuild isn't just a stepping stone to Emscripten—it's potentially a first-class deployment target for multiplayer roguelikes.Milestone: CMake Headless Build Option
Commit
4c70aeeadds official CMake support for headless builds:Verified Working ✅
lddshows no sfml/openglBinary Size Comparison
What This Enables
Remaining Steps
GameEngine::doFrame()for callback-based main loopMilestone: Main Loop Extraction Complete
Commit
8b6eb1eextractsGameEngine::doFrame()for Emscripten callback support.Architecture
The game loop now has build-time conditional behavior:
The
__EMSCRIPTEN__macro is automatically defined by the Emscripten compiler (emcc), so this is truly build-time selection with zero runtime overhead.Why This Matters
Browsers use cooperative multitasking - JavaScript can't have blocking infinite loops. Instead, the browser calls your code once per frame via
requestAnimationFrame. Theemscripten_set_main_loop_arg()function adapts this pattern to C++.Verified
Progress Summary
First Emscripten Build Attempt - Python is the Blocker
Commit
5081a37documents our firstemccbuild attempt.What Worked ✅
emcmake cmakesuccessfully configures the projectWhat Failed ❌
Python C API headers are incompatible with WASM:
Root Cause
<< 48shifts → overflow on 32-bitLONG_BITcheck fails because WASM'slongis 32 bitsPaths Forward
--host=wasm32-emscriptenMCRF_NO_PYTHONto test pure C++ engine firstRecommendation
Phase 1: Add
MCRF_NO_PYTHONCMake option to validate the rendering/input/timing pipeline works in WASM without Python complexity.Phase 2: Integrate Pyodide for Python scripting. Pyodide has a mature ecosystem and handles the hard parts (asyncio, filesystem virtualization, package management).
Current Branch Progress
First Successful WASM Build! 🎉
Commit
07fd123achieves the first successful Emscripten compilation of McRogueFace:Output:
mcrogueface.wasm(8.9MB) - WebAssembly binarymcrogueface.js(126KB) - JavaScript glue codeWhat's Linked:
Tools/wasm/emscriptenscript)LIBTCOD_SDL3=OFF)Current Status:
When tested with Node.js, the WASM binary initializes but aborts during Python initialization:
This is expected - Python can't find its standard library. The next step is bundling the Python stdlib into the WASM virtual filesystem using Emscripten's
--preload-filemechanism.Next Steps:
Major Milestone: Python 3.14 Running in WebAssembly!
Commit
8c3128eachieves a major milestone - the full game.py now runs in WebAssembly with node.js.What's Working
Test Output
Key Changes
Build Instructions
Next Steps
VRSFML Integration Research
Current Status
Attempted to build VRSFML for Emscripten but encountered compatibility issues:
Error Details
SDL3's Emscripten event handling code expects different callback signatures than what the current Emscripten headers provide.
Options Moving Forward
Option A: Fix SDL3 compatibility
Option B: Use older Emscripten
Option C: Direct WebGL approach
Option D: Wait for upstream fixes
Current WASM State (working)
References
Status Update: Pivoting from VRSFML to Direct WebGL/Canvas
Current State (Commit
8c3128e)Working:
Codebase Health:
[DEBUG]) in McRFPy_API.cpp and GameEngine.cpp are committed but harmlessVRSFML Investigation Results
VRSFML was investigated as the rendering solution but encountered blockers:
Decision: Pivot to direct WebGL/Canvas approach
Next Steps for Rendering
Option 1: Direct Canvas2D (Simplest)
emscripten_set_canvas_element_sizeand Canvas 2D contextEM_ASMJavaScript callsOption 2: Direct WebGL (More complex)
Option 3: SDL2 (Alternative)
Files to Consider Cleaning (Future)
These are currently useful for WASM debugging but could be:
#ifdef MCRF_DEBUGArchitecture for WebGL/Canvas Integration
The existing
HeadlessTypes.hprovides stub implementations. For WASM rendering:WasmRenderer.h/cppwith Emscripten-specific renderingThe game logic already works - only the rendering output is missing.
SDL2/WebGL Backend: Implementation Complete 🎉
The Emscripten/WebGL build is now functional with full rendering support. Rather than waiting for SFML 4.x or evaluating VRSFML/SMK, we implemented a custom SDL2 + OpenGL ES 2 backend that runs alongside the existing SFML backend.
Rendering Features Working
FreeType Integration
Replaced stb_truetype with FreeType (via Emscripten's USE_FREETYPE port) for proper text outline rendering. The key improvement: FT_Stroker strokes vector curves BEFORE rasterization, producing perfect outlines at any thickness (the previous multi-blit approach created gaps at corners with thick outlines).
Current Build Output
Packaging Workflow
Current State: Single Game Bundle
To ship a game today, developers need:
src/scripts/assets/emmake makeinbuild-emscripten/mcrogueface.html,.js,.wasm,.dataFuture: Repackaging Without C++ Rebuild
The
.datafile is created by Emscripten's--preload-fileflags and contains:/lib— Python stdlib (stable, ~3MB)/scripts— Game Python code/assets— Sprites, fonts, audioOption 1: file_packager.py (No C++ rebuild)
Emscripten includes a standalone tool to regenerate just the
.datafile:Include
game_loader.jsbeforemcrogueface.jsin HTML.Option 2: Split Data Files (Recommended for SDK)
Restructure build to create:
mcrogueface_engine.data— Python stdlib (stable, ship once)game.data— Scripts + assets (developer replaces)Option 3: Runtime Loading (Most flexible)
Load game files at runtime instead of preloading:
Proposed SDK Distribution Structure
Developer workflow:
scripts/game.pyand add assets./tools/package_game.sh→ generatesgame.dataindex.html,engine/*, andgame.datato web serverSize Optimization Notes
Python Shared Object (Linux/Desktop)
The debug Python SO is ~34MB. With optimizations:
Result: ~6MB (80% reduction)
Emscripten Python
The WASM Python build uses similar flags. Current
libpython3.14.ais ~20MB static archive. Investigating LTO and dead code elimination for the final WASM binary.Transfer Size Targets
Next Steps
Implemented across multiple commits starting with
c5cc022(SDL2+OpenGL ES 2 renderer backend), through1be2714(Python REPL in browser),67aa413(FreeType text rendering), and3ce7de6(platform fixes). Build targets:make wasm(full game) andmake playground(REPL-focused). Working features: full 2D rendering, Python scripting, text rendering, input handling, playground REPL widget. Remaining WASM polish tracked by #238-#240.