[Major Feature] WebAssembly/Emscripten build target for browser deployment #158

Closed
opened 2025-12-07 04:50:41 +00:00 by john · 13 comments
Owner

Vision

Compile McRogueFace to WebAssembly, enabling:

  • No-install web application — run games directly in browsers
  • Code portability — Python scripts transfer directly between web and desktop
  • Broader reach — accessible on any device with a modern browser
  • Future console support — abstract renderer also enables Nintendo Switch, etc.

Dependency Analysis

Dependency WASM Support Status Notes
libtcod-headless Ready Our fork No SDL deps, should cross-compile cleanly
SFML None Blocker Explicitly out-of-scope for SFML 3.x
VRSFML (fork) Yes Alternative Modern OpenGL ES 3.0+, Emscripten-ready
SMK Yes Alternative SFML-like API built for WASM
CPython Tier 3 PEP 776 Official support in 3.11+, formalized in 3.14
ImGui Mature Works Native Emscripten backend available

Major Technical Hurdles

1. SFML Replacement Required

SFML explicitly declined Emscripten support for 3.x (deferred to 4.x with potential Vulkan backend).

Options:

  • VRSFML — Emscripten-ready fork, removes legacy OpenGL, targets OpenGL ES 3.0+
  • SMK — Ground-up SFML alternative for WASM
  • Wait for SFML 4.x — Unknown timeline

2. Game Loop Architecture Change

Browsers require cooperative multitasking — no blocking while(true) loops.

// Current (blocking)
while (running) {
    processEvents();
    update();
    render();
}

// Emscripten (callback-based)
void mainLoop() { /* single frame */ }
emscripten_set_main_loop(mainLoop, 0, 1);

// VRSFML provides SFML_GAME_LOOP macro to abstract this

Requires refactoring GameEngine::run() to support both models.

3. Python-in-WASM Integration

CPython compiles to WASM (PEP 776, build guide), but with limitations:

Feature Browser Node Pyodide
subprocess/fork
threads WIP
file system MEMFS only IDB/Node
shared extensions WIP WIP
PyPI packages
sockets WebSocket

Key concerns:

  • Threading + dynamic loading together is experimental (Pyodide uses no-pthreads)
  • No subprocess/fork/popen (ENOSYS)
  • C++↔Python binding layer (McRFPy_API) needs adaptation
  • Binary size: ~4.5 MB compressed for minimal stdlib

4. Filesystem Virtualization

All file I/O must go through Emscripten's virtual filesystem:

  • Assets preloaded via --preload-file or async fetch()
  • scripts/ directory bundled into WASM
  • Save/load requires IndexedDB or cloud storage
  • Synchronous fopen() becomes async or pre-bundled

5. Asset Loading & Binary Size

Estimated bundle size:
- CPython WASM: ~10-15 MB
- Graphics library: ~2-5 MB
- Game code + assets: varies
- Total: 20-50+ MB potential download

Requires asset optimization strategy (lazy loading, compression, LOD).

Implementation Strategy

Phase 1: Renderer Abstraction (see #157)

Create RenderBackend interface that decouples from SFML:

  • SFMLBackend — current desktop implementation
  • SoftwareBackend — for true headless (#157)
  • VRSFMLBackend or SMKBackend — for WASM

This is shared prerequisite work with #157.

Phase 2: VRSFML Evaluation

  • Build VRSFML and test browser demos
  • Assess API compatibility with current McRogueFace code
  • Identify required changes to rendering code

Phase 3: Python-in-WASM Prototype

  • Build minimal CPython for Emscripten
  • Test if McRFPy_API binding approach works
  • Evaluate Pyodide vs raw CPython Emscripten
  • This is the highest-risk unknown

Phase 4: Game Loop Refactor

  • Abstract main loop to support both blocking and callback styles
  • Test with Emscripten's emscripten_set_main_loop

Phase 5: Asset Pipeline

  • Implement Emscripten virtual FS integration
  • Bundle scripts and assets
  • Add async loading where needed

Phase 6: Build System

  • CMake configuration for Emscripten toolchain
  • CI/CD for WASM builds
  • Hosting/deployment strategy

Relationship to Other Work

  • Depends on #157 — Renderer abstraction is prerequisite
  • Benefits from libtcod-headless — Already SDL-free, should cross-compile
  • Enables future console ports — Same abstraction supports Switch, etc.

Scope Assessment

This is a v2.0+ project requiring:

  • 150+ files touched
  • New build system configuration
  • Potentially months of focused effort
  • Significant unknowns (especially Python integration)

The renderer abstraction work (#157) is the highest-leverage first step — it unblocks both headless and WASM paths.

References

## Vision Compile McRogueFace to WebAssembly, enabling: - **No-install web application** — run games directly in browsers - **Code portability** — Python scripts transfer directly between web and desktop - **Broader reach** — accessible on any device with a modern browser - **Future console support** — abstract renderer also enables Nintendo Switch, etc. ## Dependency Analysis | Dependency | WASM Support | Status | Notes | |------------|--------------|--------|-------| | **libtcod-headless** | ✅ Ready | Our fork | No SDL deps, should cross-compile cleanly | | **SFML** | ❌ None | Blocker | Explicitly out-of-scope for SFML 3.x | | **VRSFML** (fork) | ✅ Yes | Alternative | Modern OpenGL ES 3.0+, Emscripten-ready | | **SMK** | ✅ Yes | Alternative | SFML-like API built for WASM | | **CPython** | ✅ Tier 3 | PEP 776 | Official support in 3.11+, formalized in 3.14 | | **ImGui** | ✅ Mature | Works | Native Emscripten backend available | ## Major Technical Hurdles ### 1. SFML Replacement Required SFML [explicitly declined Emscripten support](https://github.com/SFML/SFML/issues/1494) for 3.x (deferred to 4.x with potential Vulkan backend). **Options:** - **[VRSFML](https://vittorioromeo.com/index/blog/vrsfml.html)** — Emscripten-ready fork, removes legacy OpenGL, targets OpenGL ES 3.0+ - **[SMK](https://github.com/ArthurSonzogni/smk)** — Ground-up SFML alternative for WASM - **Wait for SFML 4.x** — Unknown timeline ### 2. Game Loop Architecture Change Browsers require cooperative multitasking — no blocking `while(true)` loops. ```cpp // Current (blocking) while (running) { processEvents(); update(); render(); } // Emscripten (callback-based) void mainLoop() { /* single frame */ } emscripten_set_main_loop(mainLoop, 0, 1); // VRSFML provides SFML_GAME_LOOP macro to abstract this ``` Requires refactoring `GameEngine::run()` to support both models. ### 3. Python-in-WASM Integration CPython compiles to WASM ([PEP 776](https://peps.python.org/pep-0776/), [build guide](https://github.com/python/cpython/blob/main/Tools/wasm/README.md)), but with limitations: | Feature | Browser | Node | Pyodide | |---------|---------|------|---------| | subprocess/fork | ❌ | ❌ | ❌ | | threads | ❌ | ✅ | WIP | | file system | MEMFS only | ✅ | IDB/Node | | shared extensions | WIP | WIP | ✅ | | PyPI packages | ❌ | ❌ | ✅ | | sockets | ❌ | ❌ | WebSocket | **Key concerns:** - Threading + dynamic loading together is experimental (Pyodide uses no-pthreads) - No subprocess/fork/popen (ENOSYS) - C++↔Python binding layer (`McRFPy_API`) needs adaptation - Binary size: ~4.5 MB compressed for minimal stdlib ### 4. Filesystem Virtualization All file I/O must go through Emscripten's virtual filesystem: - Assets preloaded via `--preload-file` or async `fetch()` - `scripts/` directory bundled into WASM - Save/load requires IndexedDB or cloud storage - Synchronous `fopen()` becomes async or pre-bundled ### 5. Asset Loading & Binary Size ``` Estimated bundle size: - CPython WASM: ~10-15 MB - Graphics library: ~2-5 MB - Game code + assets: varies - Total: 20-50+ MB potential download ``` Requires asset optimization strategy (lazy loading, compression, LOD). ## Implementation Strategy ### Phase 1: Renderer Abstraction (see #157) Create `RenderBackend` interface that decouples from SFML: - `SFMLBackend` — current desktop implementation - `SoftwareBackend` — for true headless (#157) - `VRSFMLBackend` or `SMKBackend` — for WASM This is shared prerequisite work with #157. ### Phase 2: VRSFML Evaluation - Build VRSFML and test [browser demos](https://vittorioromeo.com/index/blog/vrsfml.html) - Assess API compatibility with current McRogueFace code - Identify required changes to rendering code ### Phase 3: Python-in-WASM Prototype - Build minimal CPython for Emscripten - Test if `McRFPy_API` binding approach works - Evaluate Pyodide vs raw CPython Emscripten - **This is the highest-risk unknown** ### Phase 4: Game Loop Refactor - Abstract main loop to support both blocking and callback styles - Test with Emscripten's `emscripten_set_main_loop` ### Phase 5: Asset Pipeline - Implement Emscripten virtual FS integration - Bundle scripts and assets - Add async loading where needed ### Phase 6: Build System - CMake configuration for Emscripten toolchain - CI/CD for WASM builds - Hosting/deployment strategy ## Relationship to Other Work - **Depends on #157** — Renderer abstraction is prerequisite - **Benefits from libtcod-headless** — Already SDL-free, should cross-compile - **Enables future console ports** — Same abstraction supports Switch, etc. ## Scope Assessment This is a **v2.0+ project** requiring: - 150+ files touched - New build system configuration - Potentially months of focused effort - Significant unknowns (especially Python integration) The renderer abstraction work (#157) is the highest-leverage first step — it unblocks both headless and WASM paths. ## References - [SFML WASM Issue #1494](https://github.com/SFML/SFML/issues/1494) - [VRSFML Emscripten Fork](https://vittorioromeo.com/index/blog/vrsfml.html) - [SMK - WASM Multimedia Library](https://github.com/ArthurSonzogni/smk) - [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) - [Python WASM Notes](https://pythondev.readthedocs.io/wasm.html) - [Pyodide](https://github.com/pyodide/pyodide) - [libtcod Emscripten Issue #41](https://github.com/libtcod/libtcod/issues/41) - [libtcod-headless fork](https://github.com/jmccardle/libtcod-headless)
Author
Owner

Research Complete: 2026-01-30

Comprehensive analysis conducted on branch emscripten-mcrogueface. Full report: docs/EMSCRIPTEN_RESEARCH.md

Key Findings

SFML Coupling Analysis:

  • 1,276 occurrences of sf:: types across 78 files
  • Already have partial abstraction via sf::RenderTarget* pointer pattern
  • HeadlessRenderer demonstrates the backend abstraction pattern

Abstraction Difficulty by Type:

Type Count Difficulty
Vector2f ~200+ Easy (pure data)
Color ~100+ Easy (pure data)
RenderTarget ~50 HARD (central)
Shapes/Sprites ~60 HARD (platform-specific)
Events/Input ~80 Medium

SFML 3.0 Migration

Estimated 15-25 hours of refactoring work:

  • Vector parameter changes: setPosition(x, y)setPosition({x, y})
  • Rect member changes: .left.position.x
  • Scoped enums: sf::Keyboard::Asf::Keyboard::Key::A
  • Event handling: pollEvent(event)auto event = pollEvent()

VRSFML as Emscripten Path

VRSFML (Vittorio Romeo's fork) provides:

  • SFML_GAME_LOOP macro for callback-based main loop
  • Modern OpenGL ES 3.0+ (WebGL 2 compatible)
  • 500k sprite @ 60FPS (vs 3 FPS upstream) via batching

Key API differences:

  • No default constructors for resources
  • Texture passed at draw time, not stored in Sprite
  • Explicit GraphicsContext management

Build-Time Strategy Confirmed

User confirmed this is build-time configuration:

  • Desktop: Dynamic asset/script loading (current behavior)
  • Emscripten: Bundled assets via --preload-file
  • Optional script linting with Pyodide before bundling
  1. Phase 0 (This branch): Research documentation
  2. Phase 1: Type abstraction layer (mcrf:: namespace aliases)
  3. Phase 2: Extract GameEngine::doFrame() for callback compatibility
  4. Phase 3: Render backend interface
  5. Phase 4: SFML 3.0 migration
  6. Phase 5: VRSFML integration
  7. Phase 6: Python-in-WASM (highest risk)

Critical Risk: Python-in-WASM

This remains the biggest unknown. CPython compiles to WASM (PEP 776), but:

  • No threading + dynamic loading together
  • No subprocess/fork
  • McRFPy_API binding layer needs testing
  • Binary size: ~10-15 MB for minimal stdlib

Recommend early Pyodide evaluation before committing to VRSFML work.

## Research Complete: 2026-01-30 Comprehensive analysis conducted on branch `emscripten-mcrogueface`. Full report: `docs/EMSCRIPTEN_RESEARCH.md` ### Key Findings **SFML Coupling Analysis:** - **1,276 occurrences** of `sf::` types across **78 files** - Already have partial abstraction via `sf::RenderTarget*` pointer pattern - `HeadlessRenderer` demonstrates the backend abstraction pattern **Abstraction Difficulty by Type:** | Type | Count | Difficulty | |------|-------|------------| | Vector2f | ~200+ | Easy (pure data) | | Color | ~100+ | Easy (pure data) | | RenderTarget | ~50 | **HARD** (central) | | Shapes/Sprites | ~60 | **HARD** (platform-specific) | | Events/Input | ~80 | Medium | ### SFML 3.0 Migration Estimated **15-25 hours** of refactoring work: - Vector parameter changes: `setPosition(x, y)` → `setPosition({x, y})` - Rect member changes: `.left` → `.position.x` - Scoped enums: `sf::Keyboard::A` → `sf::Keyboard::Key::A` - Event handling: `pollEvent(event)` → `auto event = pollEvent()` ### VRSFML as Emscripten Path VRSFML (Vittorio Romeo's fork) provides: - `SFML_GAME_LOOP` macro for callback-based main loop - Modern OpenGL ES 3.0+ (WebGL 2 compatible) - 500k sprite @ 60FPS (vs 3 FPS upstream) via batching Key API differences: - No default constructors for resources - Texture passed at draw time, not stored in Sprite - Explicit `GraphicsContext` management ### Build-Time Strategy Confirmed User confirmed this is **build-time configuration**: - Desktop: Dynamic asset/script loading (current behavior) - Emscripten: Bundled assets via `--preload-file` - Optional script linting with Pyodide before bundling ### Recommended Phased Approach 1. **Phase 0** (This branch): Research documentation ✅ 2. **Phase 1**: Type abstraction layer (`mcrf::` namespace aliases) 3. **Phase 2**: Extract `GameEngine::doFrame()` for callback compatibility 4. **Phase 3**: Render backend interface 5. **Phase 4**: SFML 3.0 migration 6. **Phase 5**: VRSFML integration 7. **Phase 6**: Python-in-WASM (highest risk) ### Critical Risk: Python-in-WASM This remains the biggest unknown. CPython compiles to WASM (PEP 776), but: - No threading + dynamic loading together - No subprocess/fork - `McRFPy_API` binding layer needs testing - Binary size: ~10-15 MB for minimal stdlib Recommend early Pyodide evaluation before committing to VRSFML work.
Author
Owner

libtcod Architecture Analysis: Simpler Approach Discovered

Examined modules/libtcod-headless to 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:

struct TCOD_Context {
    void* contextdata_;  // Backend-specific data (opaque)
    
    // Function pointers (vtable)
    void (*c_destructor_)(struct TCOD_Context* self);
    TCOD_Error (*c_present_)(struct TCOD_Context* self, const TCOD_Console* console, ...);
    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);
    // ... ~10 total operations
};

Each backend (SDL2, xterm) fills in these function pointers.

Conditional Compilation

libtcod uses simple #ifndef NO_SDL guards in 47 files. When LIBTCOD_SDL3=OFF:

target_compile_definitions(${PROJECT_NAME} PUBLIC NO_SDL)

SDL-dependent code is simply excluded at compile time.

Why This Works

  1. Core functionality is platform-independent: Console, pathfinding, FOV, noise, BSP work without SDL
  2. Only rendering needs abstraction: Single TCOD_Context vtable handles it
  3. Minimal surface area: ~10 function pointers vs wrapping every primitive
  4. Backend data is opaque: contextdata_ holds renderer-specific state

Revised Recommendation for McRogueFace

Don't abstract every sf:: type. Abstract at the RenderContext level.

Original Plan libtcod-Inspired Plan
Replace 1276 sf::* occurrences Keep sf::* internally
Wrapper types for Vector2f, Color Keep SFML types; they're just data
78+ files touched ~10 core files for context abstraction
Complex namespace aliasing Simple #ifndef NO_SFML

Proposed McRogueFace Pattern

struct McRF_RenderContext {
    void* backend_data;
    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);
    bool (*poll_event)(McRF_RenderContext* self, Event* event);
    // ...
};

This dramatically simplifies the implementation path while achieving the same goal.

## libtcod Architecture Analysis: Simpler Approach Discovered Examined `modules/libtcod-headless` to 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: ```c struct TCOD_Context { void* contextdata_; // Backend-specific data (opaque) // Function pointers (vtable) void (*c_destructor_)(struct TCOD_Context* self); TCOD_Error (*c_present_)(struct TCOD_Context* self, const TCOD_Console* console, ...); 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); // ... ~10 total operations }; ``` Each backend (SDL2, xterm) fills in these function pointers. ### Conditional Compilation libtcod uses simple `#ifndef NO_SDL` guards in **47 files**. When `LIBTCOD_SDL3=OFF`: ```cmake target_compile_definitions(${PROJECT_NAME} PUBLIC NO_SDL) ``` SDL-dependent code is simply excluded at compile time. ### Why This Works 1. **Core functionality is platform-independent**: Console, pathfinding, FOV, noise, BSP work without SDL 2. **Only rendering needs abstraction**: Single `TCOD_Context` vtable handles it 3. **Minimal surface area**: ~10 function pointers vs wrapping every primitive 4. **Backend data is opaque**: `contextdata_` holds renderer-specific state ### Revised Recommendation for McRogueFace **Don't abstract every `sf::` type. Abstract at the RenderContext level.** | Original Plan | libtcod-Inspired Plan | |---------------|----------------------| | Replace 1276 `sf::*` occurrences | Keep `sf::*` internally | | Wrapper types for Vector2f, Color | Keep SFML types; they're just data | | 78+ files touched | ~10 core files for context abstraction | | Complex namespace aliasing | Simple `#ifndef NO_SFML` | ### Proposed McRogueFace Pattern ```cpp struct McRF_RenderContext { void* backend_data; 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); bool (*poll_event)(McRF_RenderContext* self, Event* event); // ... }; ``` This dramatically simplifies the implementation path while achieving the same goal.
Author
Owner

Phase 1 Progress: MCRF_HEADLESS Compile-Time Build Option

Commit: 7621ae3 on branch emscripten-mcrogueface

What 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

Category Files Description
New stub layer src/platform/HeadlessTypes.h (~900 lines) Complete SFML type stubs for headless compilation
Include consolidation 15 source files Routed all scattered SFML includes through Common.h
ImGui-SFML guards 5 files Wrapped with #ifndef MCRF_HEADLESS
Research doc docs/EMSCRIPTEN_RESEARCH.md Comprehensive analysis including libtcod architecture study

Build Commands

# Normal SFML build (unchanged)
make

# Headless build (new capability)
mkdir build-headless && cd build-headless
cmake .. -DCMAKE_CXX_FLAGS="-DMCRF_HEADLESS" -DCMAKE_BUILD_TYPE=Debug
make

What Still Works in Headless Mode

The headless build uses stub implementations for graphics, but all non-rendering functionality remains fully operational:

Subsystem Status Notes
Python interpreter Works Full script execution
libtcod pathfinding Works A*, Dijkstra algorithms
libtcod FOV Works Field of view calculations
libtcod noise Works Perlin, simplex, wavelet noise
libtcod BSP Works Binary space partition dungeon generation
libtcod heightmaps Works Terrain generation
Timer system Works Event scheduling
Scene management Works Scene graph, transitions (data only)
Entity/Grid data Works All game logic and data structures
Animation system Works Property interpolation (values computed, not rendered)
Input enums Works Key/mouse constants available

What Doesn't Work (Stubs Return Failure)

Operation Stub Behavior
Texture::loadFromFile() Returns false
Image::saveToFile() Returns false
Font::loadFromFile() Returns false
RenderTarget::draw() No-op
Screenshots Fail silently

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:

  • Type stubs for compilation (what we built)
  • Runtime switching via context/backend pattern (future work)

Updated Checklist

From the original issue's implementation strategy:

  • Phase 1: Renderer Abstraction — SFML dependency isolated via conditional compilation
  • Phase 2: VRSFML Evaluation — Can now be tested against headless build
  • Phase 3: Python-in-WASM Prototype
  • Phase 4: Game Loop Refactor
  • Phase 5: Asset Pipeline
  • Phase 6: Build System (Emscripten toolchain)

Next Steps

  1. Add CMake optionoption(MCRF_HEADLESS "Build without graphics" OFF)
  2. Link-time validation — Verify zero SFML symbols in headless binary
  3. VRSFML integration — Replace stubs with real Emscripten-compatible implementations
  4. Test Python-in-WASM — Highest-risk unknown

Effort

~3 hours total for clean headless compilation, significantly less than the 1-2 days estimated in the research phase.

## Phase 1 Progress: MCRF_HEADLESS Compile-Time Build Option **Commit:** [`7621ae3`](https://dev.ffwf.net/forgejo/john/McRogueFace/commit/7621ae3) on branch `emscripten-mcrogueface` ### What 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 | Category | Files | Description | |----------|-------|-------------| | **New stub layer** | `src/platform/HeadlessTypes.h` (~900 lines) | Complete SFML type stubs for headless compilation | | **Include consolidation** | 15 source files | Routed all scattered SFML includes through `Common.h` | | **ImGui-SFML guards** | 5 files | Wrapped with `#ifndef MCRF_HEADLESS` | | **Research doc** | `docs/EMSCRIPTEN_RESEARCH.md` | Comprehensive analysis including libtcod architecture study | #### Build Commands ```bash # Normal SFML build (unchanged) make # Headless build (new capability) mkdir build-headless && cd build-headless cmake .. -DCMAKE_CXX_FLAGS="-DMCRF_HEADLESS" -DCMAKE_BUILD_TYPE=Debug make ``` ### What Still Works in Headless Mode ✅ The headless build uses stub implementations for graphics, but **all non-rendering functionality remains fully operational**: | Subsystem | Status | Notes | |-----------|--------|-------| | **Python interpreter** | ✅ Works | Full script execution | | **libtcod pathfinding** | ✅ Works | A*, Dijkstra algorithms | | **libtcod FOV** | ✅ Works | Field of view calculations | | **libtcod noise** | ✅ Works | Perlin, simplex, wavelet noise | | **libtcod BSP** | ✅ Works | Binary space partition dungeon generation | | **libtcod heightmaps** | ✅ Works | Terrain generation | | **Timer system** | ✅ Works | Event scheduling | | **Scene management** | ✅ Works | Scene graph, transitions (data only) | | **Entity/Grid data** | ✅ Works | All game logic and data structures | | **Animation system** | ✅ Works | Property interpolation (values computed, not rendered) | | **Input enums** | ✅ Works | Key/mouse constants available | ### What Doesn't Work (Stubs Return Failure) ❌ | Operation | Stub Behavior | |-----------|---------------| | `Texture::loadFromFile()` | Returns `false` | | `Image::saveToFile()` | Returns `false` | | `Font::loadFromFile()` | Returns `false` | | `RenderTarget::draw()` | No-op | | Screenshots | Fail silently | ### 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: - **Type stubs** for compilation (what we built) - **Runtime switching** via context/backend pattern (future work) ### Updated Checklist From the original issue's implementation strategy: - [x] **Phase 1: Renderer Abstraction** — SFML dependency isolated via conditional compilation - [ ] Phase 2: VRSFML Evaluation — Can now be tested against headless build - [ ] Phase 3: Python-in-WASM Prototype - [ ] Phase 4: Game Loop Refactor - [ ] Phase 5: Asset Pipeline - [ ] Phase 6: Build System (Emscripten toolchain) ### Next Steps 1. **Add CMake option** — `option(MCRF_HEADLESS "Build without graphics" OFF)` 2. **Link-time validation** — Verify zero SFML symbols in headless binary 3. **VRSFML integration** — Replace stubs with real Emscripten-compatible implementations 4. **Test Python-in-WASM** — Highest-risk unknown ### Effort ~3 hours total for clean headless compilation, significantly less than the 1-2 days estimated in the research phase.
Author
Owner

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 Headless McRogueFace
V8 JavaScript engine CPython interpreter
Browser APIs stubbed/replaced SFML graphics stubbed
Same language, server context Same game scripts, server context
Event loop architecture Timer/scene system

Node.js took JavaScript—a browser language—and made it run server-side. The MCRF_HEADLESS build 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:

# Same game.py runs on both client AND server
player.move(direction)  
# Client: animates sprite, sends action to server
# Server: validates move, updates authoritative state, broadcasts result

What This Enables

Anti-Cheat / Rules Enforcement

  • Server runs identical pathfinding → "Can this entity actually reach that tile in one turn?"
  • Server runs identical FOV calculations → "Could this player actually see that enemy?"
  • Server runs identical collision logic → "Is this move legal given the grid state?"

Massively Parallel Simulation

  • Spin up 1000 headless instances on a server farm
  • Run Monte Carlo simulations for game balance testing
  • No GPU, no display—pure CPU game logic at maximum speed
  • Test millions of procedurally generated dungeons overnight

Deterministic Replay Verification

  • Record player inputs on client
  • Replay on headless server with identical random seeds
  • Detect desync between client recording and server truth
  • Prove or disprove claims of bugs/exploits

CI/CD Integration

  • Headless tests run in GitHub Actions / CI pipelines
  • No display server required (Xvfb no longer needed)
  • Game logic unit tests at full speed
  • Procedural generation validation without rendering overhead

Architecture Vision

┌─────────────────────────────────────────────────────────────┐
│                    McRogueFace Ecosystem                    │
├─────────────────────┬─────────────────────┬─────────────────┤
│   Desktop Client    │   Browser Client    │  Headless Server│
│   (SFML backend)    │   (VRSFML/WASM)     │  (stub backend) │
├─────────────────────┴─────────────────────┴─────────────────┤
│                 Shared Python Game Scripts                  │
│              (entities, levels, game rules)                 │
├─────────────────────────────────────────────────────────────┤
│                    libtcod integrations                     │
│         (pathfinding, FOV, noise, BSP, heightmaps)          │
└─────────────────────────────────────────────────────────────┘

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:

  • #158 (this issue): Browser deployment via WASM
  • #157: True headless execution for testing
  • NEW: Authoritative multiplayer server

All three use cases benefit from the same renderer abstraction work.

The Roguelike Advantage

Roguelikes are uniquely suited for this architecture:

  • Turn-based → No frame-perfect sync required
  • Discrete state → Grid positions, not floats
  • Procedural → Server generates dungeons, clients render them
  • Text-friendly → State can be serialized compactly

The MCRF_HEADLESS build isn't just a stepping stone to Emscripten—it's potentially a first-class deployment target for multiplayer roguelikes.

## 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 | Headless McRogueFace | |---------|---------------------| | V8 JavaScript engine | CPython interpreter | | Browser APIs stubbed/replaced | SFML graphics stubbed | | Same language, server context | Same game scripts, server context | | Event loop architecture | Timer/scene system | Node.js took JavaScript—a browser language—and made it run server-side. The `MCRF_HEADLESS` build 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: ```python # Same game.py runs on both client AND server player.move(direction) # Client: animates sprite, sends action to server # Server: validates move, updates authoritative state, broadcasts result ``` ### What This Enables **Anti-Cheat / Rules Enforcement** - Server runs identical **pathfinding** → "Can this entity actually reach that tile in one turn?" - Server runs identical **FOV calculations** → "Could this player actually see that enemy?" - Server runs identical **collision logic** → "Is this move legal given the grid state?" **Massively Parallel Simulation** - Spin up 1000 headless instances on a server farm - Run Monte Carlo simulations for game balance testing - No GPU, no display—pure CPU game logic at maximum speed - Test millions of procedurally generated dungeons overnight **Deterministic Replay Verification** - Record player inputs on client - Replay on headless server with identical random seeds - Detect desync between client recording and server truth - Prove or disprove claims of bugs/exploits **CI/CD Integration** - Headless tests run in GitHub Actions / CI pipelines - No display server required (`Xvfb` no longer needed) - Game logic unit tests at full speed - Procedural generation validation without rendering overhead ### Architecture Vision ``` ┌─────────────────────────────────────────────────────────────┐ │ McRogueFace Ecosystem │ ├─────────────────────┬─────────────────────┬─────────────────┤ │ Desktop Client │ Browser Client │ Headless Server│ │ (SFML backend) │ (VRSFML/WASM) │ (stub backend) │ ├─────────────────────┴─────────────────────┴─────────────────┤ │ Shared Python Game Scripts │ │ (entities, levels, game rules) │ ├─────────────────────────────────────────────────────────────┤ │ libtcod integrations │ │ (pathfinding, FOV, noise, BSP, heightmaps) │ └─────────────────────────────────────────────────────────────┘ ``` 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: - **#158 (this issue)**: Browser deployment via WASM - **#157**: True headless execution for testing - **NEW**: Authoritative multiplayer server All three use cases benefit from the same renderer abstraction work. ### The Roguelike Advantage Roguelikes are uniquely suited for this architecture: - **Turn-based** → No frame-perfect sync required - **Discrete state** → Grid positions, not floats - **Procedural** → Server generates dungeons, clients render them - **Text-friendly** → State can be serialized compactly The `MCRF_HEADLESS` build isn't just a stepping stone to Emscripten—it's potentially a first-class deployment target for multiplayer roguelikes.
Author
Owner

Milestone: CMake Headless Build Option

Commit 4c70aee adds official CMake support for headless builds:

cmake .. -DMCRF_HEADLESS=ON -DCMAKE_BUILD_TYPE=Release
make -j$(nproc)

Verified Working

Feature Status
Zero SFML dependencies ldd shows no sfml/opengl
Python interpreter Fully functional
mcrfpy.Vector Works
mcrfpy.Color Works
mcrfpy.Scene Works
mcrfpy.Frame Works
mcrfpy.Grid Works
libtcod integrations Available

Binary Size Comparison

Build Size Notes
Normal (SFML) 2.5 MB Full graphics
Headless 1.6 MB 36% reduction

What This Enables

  1. McRogueFace Server - Run game logic without graphics for multiplayer/validation
  2. Faster CI - Test Python scripts without X11/display requirements
  3. Emscripten prep - Foundation for replacing stubs with WebGL

Remaining Steps

  1. Extract GameEngine::doFrame() for callback-based main loop
  2. Add Emscripten CMake toolchain
  3. Integrate VRSFML for actual WebGL rendering
  4. Test Python-in-WASM (highest risk unknown)
## Milestone: CMake Headless Build Option Commit `4c70aee` adds official CMake support for headless builds: ```bash cmake .. -DMCRF_HEADLESS=ON -DCMAKE_BUILD_TYPE=Release make -j$(nproc) ``` ### Verified Working ✅ | Feature | Status | |---------|--------| | Zero SFML dependencies | ✅ `ldd` shows no sfml/opengl | | Python interpreter | ✅ Fully functional | | mcrfpy.Vector | ✅ Works | | mcrfpy.Color | ✅ Works | | mcrfpy.Scene | ✅ Works | | mcrfpy.Frame | ✅ Works | | mcrfpy.Grid | ✅ Works | | libtcod integrations | ✅ Available | ### Binary Size Comparison | Build | Size | Notes | |-------|------|-------| | Normal (SFML) | 2.5 MB | Full graphics | | Headless | 1.6 MB | 36% reduction | ### What This Enables 1. **McRogueFace Server** - Run game logic without graphics for multiplayer/validation 2. **Faster CI** - Test Python scripts without X11/display requirements 3. **Emscripten prep** - Foundation for replacing stubs with WebGL ### Remaining Steps 1. Extract `GameEngine::doFrame()` for callback-based main loop 2. Add Emscripten CMake toolchain 3. Integrate VRSFML for actual WebGL rendering 4. Test Python-in-WASM (highest risk unknown)
Author
Owner

Milestone: Main Loop Extraction Complete

Commit 8b6eb1e extracts GameEngine::doFrame() for Emscripten callback support.

Architecture

The game loop now has build-time conditional behavior:

void GameEngine::run() {
#ifdef __EMSCRIPTEN__
    // Browser: callback-based (non-blocking)
    emscripten_set_main_loop_arg(emscriptenMainLoopCallback, this, 0, 1);
#else
    // Desktop: traditional blocking loop
    while (running) {
        doFrame();
    }
#endif
}

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. The emscripten_set_main_loop_arg() function adapts this pattern to C++.

Verified

  • Normal SFML build compiles and runs
  • Headless build compiles and runs
  • Python interpreter functional in both modes

Progress Summary

Task Status
HeadlessTypes.h stubs Complete
CMake MCRF_HEADLESS option Complete
Python bindings in headless Working
Main loop extraction Complete
Emscripten CMake toolchain 🔲 Next
VRSFML integration 🔲 Future
Python-in-WASM 🔲 Future (highest risk)
## Milestone: Main Loop Extraction Complete Commit `8b6eb1e` extracts `GameEngine::doFrame()` for Emscripten callback support. ### Architecture The game loop now has build-time conditional behavior: ```cpp void GameEngine::run() { #ifdef __EMSCRIPTEN__ // Browser: callback-based (non-blocking) emscripten_set_main_loop_arg(emscriptenMainLoopCallback, this, 0, 1); #else // Desktop: traditional blocking loop while (running) { doFrame(); } #endif } ``` 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`. The `emscripten_set_main_loop_arg()` function adapts this pattern to C++. ### Verified - ✅ Normal SFML build compiles and runs - ✅ Headless build compiles and runs - ✅ Python interpreter functional in both modes ### Progress Summary | Task | Status | |------|--------| | HeadlessTypes.h stubs | ✅ Complete | | CMake MCRF_HEADLESS option | ✅ Complete | | Python bindings in headless | ✅ Working | | Main loop extraction | ✅ Complete | | Emscripten CMake toolchain | 🔲 Next | | VRSFML integration | 🔲 Future | | Python-in-WASM | 🔲 Future (highest risk) |
Author
Owner

First Emscripten Build Attempt - Python is the Blocker

Commit 5081a37 documents our first emcc build attempt.

What Worked

  • Emscripten SDK 5.0.0 installed and configured
  • emcmake cmake successfully configures the project
  • HeadlessTypes.h stubs compile fine with Emscripten's clang
  • Game engine code (GameEngine, Scene, etc.) compiles fine

What Failed

Python C API headers are incompatible with WASM:

deps/Python/pyport.h:429:2: error: 
"LONG_BIT definition appears wrong for platform (bad gcc/glibc config?)."
warning: shift count >= width of type [-Wshift-count-overflow]
_Py_STATIC_FLAG_BITS << 48  // 48-bit shift on 32-bit WASM!

Root Cause

  1. Desktop Python 3.14 headers assume 64-bit Linux with glibc
  2. Emscripten targets 32-bit WASM with musl-based libc
  3. Python's immortal reference counting uses << 48 shifts → overflow on 32-bit
  4. LONG_BIT check fails because WASM's long is 32 bits

Paths Forward

Option Effort Description
Pyodide Medium Use pre-built Python 3.12 WASM from Pyodide project
CPython WASM High Build CPython ourselves with --host=wasm32-emscripten
No-Python mode Low Add MCRF_NO_PYTHON to test pure C++ engine first

Recommendation

Phase 1: Add MCRF_NO_PYTHON CMake 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

Task Status
HeadlessTypes.h stubs Works with emcc
CMake MCRF_HEADLESS option Works with emcmake
Main loop extraction Ready for browser callback
Python integration BLOCKER - needs WASM-specific approach
VRSFML integration 🔲 After Python resolved
## First Emscripten Build Attempt - Python is the Blocker Commit `5081a37` documents our first `emcc` build attempt. ### What Worked ✅ - Emscripten SDK 5.0.0 installed and configured - `emcmake cmake` successfully configures the project - HeadlessTypes.h stubs compile fine with Emscripten's clang - Game engine code (GameEngine, Scene, etc.) compiles fine ### What Failed ❌ **Python C API headers are incompatible with WASM:** ``` deps/Python/pyport.h:429:2: error: "LONG_BIT definition appears wrong for platform (bad gcc/glibc config?)." ``` ``` warning: shift count >= width of type [-Wshift-count-overflow] _Py_STATIC_FLAG_BITS << 48 // 48-bit shift on 32-bit WASM! ``` ### Root Cause 1. Desktop Python 3.14 headers assume 64-bit Linux with glibc 2. Emscripten targets **32-bit WASM** with musl-based libc 3. Python's immortal reference counting uses `<< 48` shifts → overflow on 32-bit 4. `LONG_BIT` check fails because WASM's `long` is 32 bits ### Paths Forward | Option | Effort | Description | |--------|--------|-------------| | **Pyodide** | Medium | Use pre-built Python 3.12 WASM from Pyodide project | | **CPython WASM** | High | Build CPython ourselves with `--host=wasm32-emscripten` | | **No-Python mode** | Low | Add `MCRF_NO_PYTHON` to test pure C++ engine first | ### Recommendation **Phase 1:** Add `MCRF_NO_PYTHON` CMake 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 | Task | Status | |------|--------| | HeadlessTypes.h stubs | ✅ Works with emcc | | CMake MCRF_HEADLESS option | ✅ Works with emcmake | | Main loop extraction | ✅ Ready for browser callback | | Python integration | ❌ **BLOCKER** - needs WASM-specific approach | | VRSFML integration | 🔲 After Python resolved |
Author
Owner

First Successful WASM Build! 🎉

Commit 07fd123 achieves the first successful Emscripten compilation of McRogueFace:

Output:

  • mcrogueface.wasm (8.9MB) - WebAssembly binary
  • mcrogueface.js (126KB) - JavaScript glue code

What's Linked:

  • Python 3.14 (built for wasm32-emscripten target using CPython's official Tools/wasm/emscripten script)
  • libtcod-headless (built for Emscripten with LIBTCOD_SDL3=OFF)
  • HACL crypto (MD5, SHA1, SHA2, SHA3, Blake2)
  • expat, mpdec, ffi from Python build
  • zlib, bzip2, sqlite3 from Emscripten ports

Current Status:
When tested with Node.js, the WASM binary initializes but aborts during Python initialization:

[DEBUG] GameEngine: loading default game.py
[DEBUG] GameEngine: initializing Python API
Aborted()

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-file mechanism.

Next Steps:

  1. Bundle Python stdlib into WASM filesystem
  2. Test Python initialization in browser environment
  3. Eventually integrate VRSFML for actual WebGL rendering
## First Successful WASM Build! 🎉 Commit `07fd123` achieves the first successful Emscripten compilation of McRogueFace: **Output:** - `mcrogueface.wasm` (8.9MB) - WebAssembly binary - `mcrogueface.js` (126KB) - JavaScript glue code **What's Linked:** - Python 3.14 (built for wasm32-emscripten target using CPython's official `Tools/wasm/emscripten` script) - libtcod-headless (built for Emscripten with `LIBTCOD_SDL3=OFF`) - HACL crypto (MD5, SHA1, SHA2, SHA3, Blake2) - expat, mpdec, ffi from Python build - zlib, bzip2, sqlite3 from Emscripten ports **Current Status:** When tested with Node.js, the WASM binary initializes but aborts during Python initialization: ``` [DEBUG] GameEngine: loading default game.py [DEBUG] GameEngine: initializing Python API Aborted() ``` 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-file` mechanism. **Next Steps:** 1. Bundle Python stdlib into WASM filesystem 2. Test Python initialization in browser environment 3. Eventually integrate VRSFML for actual WebGL rendering
Author
Owner

Major Milestone: Python 3.14 Running in WebAssembly!

Commit 8c3128e achieves a major milestone - the full game.py now runs in WebAssembly with node.js.

What's Working

  • Python 3.14 initializes correctly in WASM
  • mcrfpy module loads successfully
  • All game scripts execute (game.py, cos_entities.py, cos_level.py, etc.)
  • Level generation works - entities are created and placed correctly
  • dataclasses, random, and other stdlib modules work

Test Output

[DEBUG] GameEngine: executing scripts/game.py
[('boulder', 'boulder', 'rat', 'cyclops', 'boulder'), 'spawn', ('rat', 'big rat'), ('button', 'boulder', 'exit')]
('boulder', (1, 5))
('boulder', (1, 7))
('rat', (1, 6))
('cyclops', (1, 1))
...
[DEBUG] GameEngine: game.py execution complete

Key Changes

  1. CMakeLists.txt: 2MB stack, Emscripten link options, preload files
  2. platform.h: WASM-specific implementations for executable paths
  3. HeadlessTypes.h: Make Texture/Font/Sound stubs return success
  4. wasm_stdlib/: Minimal Python stdlib (~4MB) for WASM

Build Instructions

cd build-emscripten
source ~/emsdk/emsdk_env.sh
emmake make
node mcrogueface.js  # Test in node

Next Steps

  • Integrate VRSFML for actual WebGL rendering
  • Create HTML page to host WASM build
  • Test in browsers (Chrome, Firefox)
  • Optimize bundle size
## Major Milestone: Python 3.14 Running in WebAssembly! Commit 8c3128e achieves a major milestone - the full game.py now runs in WebAssembly with node.js. ### What's Working - Python 3.14 initializes correctly in WASM - mcrfpy module loads successfully - All game scripts execute (game.py, cos_entities.py, cos_level.py, etc.) - Level generation works - entities are created and placed correctly - dataclasses, random, and other stdlib modules work ### Test Output ``` [DEBUG] GameEngine: executing scripts/game.py [('boulder', 'boulder', 'rat', 'cyclops', 'boulder'), 'spawn', ('rat', 'big rat'), ('button', 'boulder', 'exit')] ('boulder', (1, 5)) ('boulder', (1, 7)) ('rat', (1, 6)) ('cyclops', (1, 1)) ... [DEBUG] GameEngine: game.py execution complete ``` ### Key Changes 1. **CMakeLists.txt**: 2MB stack, Emscripten link options, preload files 2. **platform.h**: WASM-specific implementations for executable paths 3. **HeadlessTypes.h**: Make Texture/Font/Sound stubs return success 4. **wasm_stdlib/**: Minimal Python stdlib (~4MB) for WASM ### Build Instructions ```bash cd build-emscripten source ~/emsdk/emsdk_env.sh emmake make node mcrogueface.js # Test in node ``` ### Next Steps - [ ] Integrate VRSFML for actual WebGL rendering - [ ] Create HTML page to host WASM build - [ ] Test in browsers (Chrome, Firefox) - [ ] Optimize bundle size
Author
Owner

VRSFML Integration Research

Current Status

Attempted to build VRSFML for Emscripten but encountered compatibility issues:

  1. CMake 3.28+ requirement - Resolved by downloading CMake 3.31
  2. SDL3/Emscripten API mismatch - SDL3's Emscripten event handlers have incompatible function signatures with Emscripten 5.0.0

Error Details

error: incompatible function pointer types passing 
'void (SDL_WindowData *, const Emscripten_PointerEvent *, bool)' 
to parameter of type 'em_mouse_callback_func'

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

  • Patch SDL3's Emscripten code to match current API
  • Could be fragile and require ongoing maintenance

Option B: Use older Emscripten

  • Try Emscripten 3.x or early 4.x versions
  • May work but limits access to newer features/fixes

Option C: Direct WebGL approach

  • Skip VRSFML entirely for WASM
  • Implement simple WebGL/Canvas renderer using Emscripten APIs directly
  • More work but more control

Option D: Wait for upstream fixes

  • SDL3 and VRSFML are actively developed
  • These compatibility issues may be resolved soon

Current WASM State (working)

  • Python 3.14 runs in WASM ✓
  • mcrfpy module works ✓
  • Game scripts execute ✓
  • Headless mode (no graphics) ✓

References

## VRSFML Integration Research ### Current Status Attempted to build VRSFML for Emscripten but encountered compatibility issues: 1. **CMake 3.28+ requirement** - Resolved by downloading CMake 3.31 2. **SDL3/Emscripten API mismatch** - SDL3's Emscripten event handlers have incompatible function signatures with Emscripten 5.0.0 ### Error Details ``` error: incompatible function pointer types passing 'void (SDL_WindowData *, const Emscripten_PointerEvent *, bool)' to parameter of type 'em_mouse_callback_func' ``` 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** - Patch SDL3's Emscripten code to match current API - Could be fragile and require ongoing maintenance **Option B: Use older Emscripten** - Try Emscripten 3.x or early 4.x versions - May work but limits access to newer features/fixes **Option C: Direct WebGL approach** - Skip VRSFML entirely for WASM - Implement simple WebGL/Canvas renderer using Emscripten APIs directly - More work but more control **Option D: Wait for upstream fixes** - SDL3 and VRSFML are actively developed - These compatibility issues may be resolved soon ### Current WASM State (working) - Python 3.14 runs in WASM ✓ - mcrfpy module works ✓ - Game scripts execute ✓ - Headless mode (no graphics) ✓ ### References - [VRSFML GitHub](https://github.com/vittorioromeo/VRSFML) - [VRSFML Blog Post](https://vittorioromeo.com/index/blog/vrsfml.html)
Author
Owner

Status Update: Pivoting from VRSFML to Direct WebGL/Canvas

Current State (Commit 8c3128e)

Working:

  • Python 3.14 runs in WebAssembly
  • mcrfpy module loads and functions correctly
  • Game scripts execute (game.py, cos_entities.py, cos_level.py, etc.)
  • Level generation works - entities placed correctly
  • Headless mode operational (no graphics output yet)

Codebase Health:

  • Clean commit state - no uncommitted changes to tracked files
  • Debug statements ([DEBUG]) in McRFPy_API.cpp and GameEngine.cpp are committed but harmless
  • VRSFML module removed (was never integrated, just cloned for testing)

VRSFML Investigation Results

VRSFML was investigated as the rendering solution but encountered blockers:

  1. CMake 3.28+ requirement - Solved by downloading CMake 3.31
  2. SDL3/Emscripten API mismatch - SDL3's Emscripten event handlers have incompatible callback signatures
    • Tested with Emscripten 5.0.0 and 3.1.51 - same issue
    • This is an upstream SDL3 bug, not version-dependent

Decision: Pivot to direct WebGL/Canvas approach

Next Steps for Rendering

Option 1: Direct Canvas2D (Simplest)

  • Use Emscripten's emscripten_set_canvas_element_size and Canvas 2D context
  • Draw tiles/sprites via EM_ASM JavaScript calls
  • Best for roguelike's tile-based nature

Option 2: Direct WebGL (More complex)

  • Use Emscripten's OpenGL ES subset
  • More setup but better performance for many sprites

Option 3: SDL2 (Alternative)

  • SDL2 has better Emscripten support than SDL3
  • Could provide a middle ground

Files to Consider Cleaning (Future)

// Debug statements in McRFPy_API.cpp (lines 701-733, 816-825)
std::cerr << "[DEBUG] api_init: ..." << std::endl;

// Debug statements in GameEngine.cpp (lines 125-138)
std::cerr << "[DEBUG] GameEngine: ..." << std::endl;

These are currently useful for WASM debugging but could be:

  • Removed for cleaner output
  • Made conditional on #ifdef MCRF_DEBUG
  • Converted to a proper logging system

Architecture for WebGL/Canvas Integration

The existing HeadlessTypes.h provides stub implementations. For WASM rendering:

  1. Create WasmRenderer.h/cpp with Emscripten-specific rendering
  2. Either:
    • Replace headless stubs with real WebGL/Canvas calls, OR
    • Add a new rendering path alongside headless

The game logic already works - only the rendering output is missing.

## Status Update: Pivoting from VRSFML to Direct WebGL/Canvas ### Current State (Commit 8c3128e) **Working:** - ✅ Python 3.14 runs in WebAssembly - ✅ mcrfpy module loads and functions correctly - ✅ Game scripts execute (game.py, cos_entities.py, cos_level.py, etc.) - ✅ Level generation works - entities placed correctly - ✅ Headless mode operational (no graphics output yet) **Codebase Health:** - Clean commit state - no uncommitted changes to tracked files - Debug statements (`[DEBUG]`) in McRFPy_API.cpp and GameEngine.cpp are committed but harmless - VRSFML module removed (was never integrated, just cloned for testing) ### VRSFML Investigation Results VRSFML was investigated as the rendering solution but encountered blockers: 1. **CMake 3.28+ requirement** - Solved by downloading CMake 3.31 2. **SDL3/Emscripten API mismatch** - SDL3's Emscripten event handlers have incompatible callback signatures - Tested with Emscripten 5.0.0 and 3.1.51 - same issue - This is an upstream SDL3 bug, not version-dependent **Decision: Pivot to direct WebGL/Canvas approach** ### Next Steps for Rendering **Option 1: Direct Canvas2D (Simplest)** - Use Emscripten's `emscripten_set_canvas_element_size` and Canvas 2D context - Draw tiles/sprites via `EM_ASM` JavaScript calls - Best for roguelike's tile-based nature **Option 2: Direct WebGL (More complex)** - Use Emscripten's OpenGL ES subset - More setup but better performance for many sprites **Option 3: SDL2 (Alternative)** - SDL2 has better Emscripten support than SDL3 - Could provide a middle ground ### Files to Consider Cleaning (Future) ```cpp // Debug statements in McRFPy_API.cpp (lines 701-733, 816-825) std::cerr << "[DEBUG] api_init: ..." << std::endl; // Debug statements in GameEngine.cpp (lines 125-138) std::cerr << "[DEBUG] GameEngine: ..." << std::endl; ``` These are currently useful for WASM debugging but could be: - Removed for cleaner output - Made conditional on `#ifdef MCRF_DEBUG` - Converted to a proper logging system ### Architecture for WebGL/Canvas Integration The existing `HeadlessTypes.h` provides stub implementations. For WASM rendering: 1. Create `WasmRenderer.h/cpp` with Emscripten-specific rendering 2. Either: - Replace headless stubs with real WebGL/Canvas calls, OR - Add a new rendering path alongside headless The game logic already works - only the rendering output is missing.
Author
Owner

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

Feature Status Implementation
Window/GL context SDL2 + WebGL 2
Shape rendering Custom GLSL shaders
Sprite/texture stb_image + GL textures
Text rendering FreeType (replaced stb_truetype)
Text outlines FT_Stroker for vector-based outlines
RenderTexture (FBO) glFramebuffer with Y-flip handling
Grid rendering Batched tile rendering
Input (keyboard/mouse) SDL2 event translation to sf::Event
Python REPL widget In-browser Python console

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

mcrogueface.wasm   ~14 MB   (C++ engine + Python)
mcrogueface.js     ~636 KB  (Emscripten glue)
mcrogueface.data   ~4.6 MB  (preloaded: assets, scripts, Python stdlib)
mcrogueface.html   ~16 KB   (shell with REPL UI)

Packaging Workflow

Current State: Single Game Bundle

To ship a game today, developers need:

  1. Full source build with Emscripten SDK
  2. Game scripts in src/scripts/
  3. Assets in assets/
  4. Run emmake make in build-emscripten/
  5. Deploy: mcrogueface.html, .js, .wasm, .data

Future: Repackaging Without C++ Rebuild

The .data file is created by Emscripten's --preload-file flags and contains:

  • /lib — Python stdlib (stable, ~3MB)
  • /scripts — Game Python code
  • /assets — Sprites, fonts, audio

Option 1: file_packager.py (No C++ rebuild)

Emscripten includes a standalone tool to regenerate just the .data file:

python3 $EMSDK/upstream/emscripten/tools/file_packager.py \
    game.data \
    --preload my_scripts@/scripts \
    --preload my_assets@/assets \
    --js-output=game_loader.js

Include game_loader.js before mcrogueface.js in HTML.

Option 2: Split Data Files (Recommended for SDK)

Restructure build to create:

  1. mcrogueface_engine.data — Python stdlib (stable, ship once)
  2. game.data — Scripts + assets (developer replaces)

Option 3: Runtime Loading (Most flexible)

Load game files at runtime instead of preloading:

Module.preRun.push(function() {
    FS.createPreloadedFile('/scripts', 'game.py', 'game.py', true, false);
});

Proposed SDK Distribution Structure

mcrogueface-web-sdk/
├── engine/
│   ├── mcrogueface.wasm      # Pre-built engine
│   ├── mcrogueface.js        # Loader (modified for split data)
│   └── stdlib.data           # Python standard library
├── template/
│   ├── index.html            # HTML shell template
│   ├── scripts/
│   │   └── game.py           # Starter game script
│   └── assets/
│       └── (add sprites/fonts here)
├── tools/
│   └── package_game.sh       # Wrapper around file_packager.py
└── README.md

Developer workflow:

  1. Edit scripts/game.py and add assets
  2. Run ./tools/package_game.sh → generates game.data
  3. Upload index.html, engine/*, and game.data to web server

Size Optimization Notes

Python Shared Object (Linux/Desktop)

The debug Python SO is ~34MB. With optimizations:

./configure --enable-optimizations --with-lto --enable-shared --disable-test-modules
make -j$(nproc)
strip --strip-unneeded libpython3.14.so.1.0

Result: ~6MB (80% reduction)

Emscripten Python

The WASM Python build uses similar flags. Current libpython3.14.a is ~20MB static archive. Investigating LTO and dead code elimination for the final WASM binary.

Transfer Size Targets

Component Current Target
WASM binary ~14 MB ~8 MB (with LTO)
Python stdlib ~3 MB ~1.5 MB (filtered)
Gzip'd total TBD < 5 MB

Next Steps

  1. FreeType integration (done)
  2. Optimized Python builds (desktop + WASM)
  3. SDK packaging scripts
  4. Split data file support
  5. 🔮 Future: Hot-reload game scripts without page refresh
## 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 | Feature | Status | Implementation | |---------|--------|----------------| | Window/GL context | ✅ | SDL2 + WebGL 2 | | Shape rendering | ✅ | Custom GLSL shaders | | Sprite/texture | ✅ | stb_image + GL textures | | Text rendering | ✅ | **FreeType** (replaced stb_truetype) | | Text outlines | ✅ | FT_Stroker for vector-based outlines | | RenderTexture (FBO) | ✅ | glFramebuffer with Y-flip handling | | Grid rendering | ✅ | Batched tile rendering | | Input (keyboard/mouse) | ✅ | SDL2 event translation to sf::Event | | Python REPL widget | ✅ | In-browser Python console | ### 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 ``` mcrogueface.wasm ~14 MB (C++ engine + Python) mcrogueface.js ~636 KB (Emscripten glue) mcrogueface.data ~4.6 MB (preloaded: assets, scripts, Python stdlib) mcrogueface.html ~16 KB (shell with REPL UI) ``` --- ## Packaging Workflow ### Current State: Single Game Bundle To ship a game today, developers need: 1. **Full source build** with Emscripten SDK 2. Game scripts in `src/scripts/` 3. Assets in `assets/` 4. Run `emmake make` in `build-emscripten/` 5. Deploy: `mcrogueface.html`, `.js`, `.wasm`, `.data` ### Future: Repackaging Without C++ Rebuild The `.data` file is created by Emscripten's `--preload-file` flags and contains: - `/lib` — Python stdlib (stable, ~3MB) - `/scripts` — Game Python code - `/assets` — Sprites, fonts, audio **Option 1: file_packager.py (No C++ rebuild)** Emscripten includes a standalone tool to regenerate just the `.data` file: ```bash python3 $EMSDK/upstream/emscripten/tools/file_packager.py \ game.data \ --preload my_scripts@/scripts \ --preload my_assets@/assets \ --js-output=game_loader.js ``` Include `game_loader.js` before `mcrogueface.js` in HTML. **Option 2: Split Data Files (Recommended for SDK)** Restructure build to create: 1. `mcrogueface_engine.data` — Python stdlib (stable, ship once) 2. `game.data` — Scripts + assets (developer replaces) **Option 3: Runtime Loading (Most flexible)** Load game files at runtime instead of preloading: ```javascript Module.preRun.push(function() { FS.createPreloadedFile('/scripts', 'game.py', 'game.py', true, false); }); ``` ### Proposed SDK Distribution Structure ``` mcrogueface-web-sdk/ ├── engine/ │ ├── mcrogueface.wasm # Pre-built engine │ ├── mcrogueface.js # Loader (modified for split data) │ └── stdlib.data # Python standard library ├── template/ │ ├── index.html # HTML shell template │ ├── scripts/ │ │ └── game.py # Starter game script │ └── assets/ │ └── (add sprites/fonts here) ├── tools/ │ └── package_game.sh # Wrapper around file_packager.py └── README.md ``` Developer workflow: 1. Edit `scripts/game.py` and add assets 2. Run `./tools/package_game.sh` → generates `game.data` 3. Upload `index.html`, `engine/*`, and `game.data` to web server --- ## Size Optimization Notes ### Python Shared Object (Linux/Desktop) The debug Python SO is ~34MB. With optimizations: ```bash ./configure --enable-optimizations --with-lto --enable-shared --disable-test-modules make -j$(nproc) strip --strip-unneeded libpython3.14.so.1.0 ``` Result: ~6MB (80% reduction) ### Emscripten Python The WASM Python build uses similar flags. Current `libpython3.14.a` is ~20MB static archive. Investigating LTO and dead code elimination for the final WASM binary. ### Transfer Size Targets | Component | Current | Target | |-----------|---------|--------| | WASM binary | ~14 MB | ~8 MB (with LTO) | | Python stdlib | ~3 MB | ~1.5 MB (filtered) | | Gzip'd total | TBD | < 5 MB | --- ## Next Steps 1. ✅ FreeType integration (done) 2. ⏳ Optimized Python builds (desktop + WASM) 3. ⏳ SDK packaging scripts 4. ⏳ Split data file support 5. 🔮 Future: Hot-reload game scripts without page refresh
Author
Owner

Implemented across multiple commits starting with c5cc022 (SDL2+OpenGL ES 2 renderer backend), through 1be2714 (Python REPL in browser), 67aa413 (FreeType text rendering), and 3ce7de6 (platform fixes). Build targets: make wasm (full game) and make playground (REPL-focused). Working features: full 2D rendering, Python scripting, text rendering, input handling, playground REPL widget. Remaining WASM polish tracked by #238-#240.

Implemented across multiple commits starting with `c5cc022` (SDL2+OpenGL ES 2 renderer backend), through `1be2714` (Python REPL in browser), `67aa413` (FreeType text rendering), and `3ce7de6` (platform fixes). Build targets: `make wasm` (full game) and `make playground` (REPL-focused). Working features: full 2D rendering, Python scripting, text rendering, input handling, playground REPL widget. Remaining WASM polish tracked by #238-#240.
john closed this issue 2026-02-07 19:25:17 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
john/McRogueFace#158
No description provided.