diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml
new file mode 100644
index 0000000..76a04a2
--- /dev/null
+++ b/.gitea/workflows/ci.yaml
@@ -0,0 +1,123 @@
+name: CI
+
+on:
+ push:
+ branches: [master]
+ pull_request:
+ branches: [master]
+
+jobs:
+ build-and-test:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ submodules: recursive
+
+ - name: Install build dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y \
+ build-essential cmake git \
+ zlib1g-dev libx11-dev libxrandr-dev libxcursor-dev \
+ libfreetype-dev libudev-dev libvorbis-dev libflac-dev \
+ libgl-dev libopenal-dev
+
+ - name: Check for pre-built libraries
+ run: |
+ if [ ! -d "__lib" ]; then
+ echo "::error::__lib/ directory not found. Pre-built libraries must be available on the runner."
+ echo "See BUILD_FROM_SOURCE.md for instructions on building dependencies."
+ exit 1
+ fi
+
+ - name: Build (Release)
+ run: make linux
+
+ - name: Run tests (Release)
+ run: cd tests && python3 run_tests.py -v
+
+ debug-test:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ submodules: recursive
+
+ - name: Install build dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y \
+ build-essential cmake git \
+ zlib1g-dev libx11-dev libxrandr-dev libxcursor-dev \
+ libfreetype-dev libudev-dev libvorbis-dev libflac-dev \
+ libgl-dev libopenal-dev
+
+ - name: Check for debug libraries
+ run: |
+ if [ ! -d "__lib_debug" ]; then
+ echo "::error::__lib_debug/ directory not found. Build debug Python first: tools/build_debug_python.sh"
+ exit 1
+ fi
+
+ - name: Build and test (debug Python)
+ run: make debug-test
+
+ asan-test:
+ runs-on: ubuntu-latest
+ if: github.event_name == 'pull_request'
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ submodules: recursive
+
+ - name: Install build dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y \
+ build-essential cmake git \
+ zlib1g-dev libx11-dev libxrandr-dev libxcursor-dev \
+ libfreetype-dev libudev-dev libvorbis-dev libflac-dev \
+ libgl-dev libopenal-dev
+
+ - name: Check for debug libraries
+ run: |
+ if [ ! -d "__lib_debug" ]; then
+ echo "::error::__lib_debug/ directory not found. Build debug Python first: tools/build_debug_python.sh"
+ exit 1
+ fi
+
+ - name: Build and test (ASan + UBSan)
+ run: make asan-test
+
+ valgrind-test:
+ runs-on: ubuntu-latest
+ if: github.event_name == 'pull_request'
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ submodules: recursive
+
+ - name: Install build dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y \
+ build-essential cmake git valgrind \
+ zlib1g-dev libx11-dev libxrandr-dev libxcursor-dev \
+ libfreetype-dev libudev-dev libvorbis-dev libflac-dev \
+ libgl-dev libopenal-dev
+
+ - name: Check for debug libraries
+ run: |
+ if [ ! -d "__lib_debug" ]; then
+ echo "::error::__lib_debug/ directory not found. Build debug Python first: tools/build_debug_python.sh"
+ exit 1
+ fi
+
+ - name: Build and test (Valgrind memcheck)
+ run: make valgrind-test
+ timeout-minutes: 30
diff --git a/CLAUDE.md b/CLAUDE.md
index 0c78aa6..fa4cdc0 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -311,6 +311,8 @@ make serve # Serve at http://localhost:8080
|--------|------------------|---------|
| `make wasm` | `build-emscripten/` | Full game with all scripts/assets |
| `make playground` | `build-playground/` | Minimal REPL build for interactive testing |
+| `make wasm-debug` | `build-wasm-debug/` | Debug build with DWARF symbols and source maps |
+| `make playground-debug` | `build-playground-debug/` | Debug playground with DWARF and source maps |
### Rendering Backend Selection
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 326c181..0a02462 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -28,6 +28,7 @@ option(MCRF_SANITIZE_ADDRESS "Build with AddressSanitizer" OFF)
option(MCRF_SANITIZE_UNDEFINED "Build with UBSan" OFF)
option(MCRF_SANITIZE_THREAD "Build with ThreadSanitizer" OFF)
option(MCRF_DEBUG_PYTHON "Link against debug CPython from __lib_debug/" OFF)
+option(MCRF_WASM_DEBUG "Build WASM with DWARF debug info and source maps" OFF)
# Validate mutually exclusive sanitizers
if(MCRF_SANITIZE_ADDRESS AND MCRF_SANITIZE_THREAD)
@@ -398,6 +399,17 @@ if(EMSCRIPTEN)
message(STATUS "Emscripten SDL2 options enabled: -sUSE_SDL=2 -sUSE_SDL_MIXER=2 -sFULL_ES2=1 -sUSE_FREETYPE=1")
endif()
+ # WASM debug builds: DWARF symbols, source maps, symbol map for stack traces
+ if(MCRF_WASM_DEBUG)
+ list(APPEND EMSCRIPTEN_LINK_OPTIONS
+ -g4
+ -gsource-map
+ --emit-symbol-map
+ )
+ target_compile_options(mcrogueface PRIVATE -g4)
+ message(STATUS "Emscripten debug enabled: DWARF (-g4), source maps, symbol map")
+ endif()
+
target_link_options(mcrogueface PRIVATE ${EMSCRIPTEN_LINK_OPTIONS})
# Output as HTML to use the shell file
diff --git a/Makefile b/Makefile
index b14e076..fd2b4fd 100644
--- a/Makefile
+++ b/Makefile
@@ -27,7 +27,7 @@
# Tags HEAD with current version, builds all packages, bumps to NEXT_VERSION
.PHONY: all linux windows windows-debug clean clean-windows clean-dist run
-.PHONY: wasm wasm-game playground serve serve-game serve-playground clean-wasm
+.PHONY: wasm wasm-game wasm-debug playground playground-debug serve serve-game serve-playground clean-wasm
.PHONY: package-windows-light package-windows-full package-linux-light package-linux-full package-all
.PHONY: version-bump
.PHONY: debug debug-test asan asan-test valgrind-test massif-test analyze clean-debug
@@ -273,7 +273,55 @@ serve-demo:
clean-wasm:
@echo "Cleaning Emscripten builds..."
- @rm -rf build-emscripten build-playground build-wasm-game build-wasm-demo
+ @rm -rf build-emscripten build-playground build-wasm-game build-wasm-demo build-wasm-debug build-playground-debug
+
+wasm-debug:
+ @if ! command -v emcmake >/dev/null 2>&1; then \
+ echo "Error: emcmake not found. Run 'source ~/emsdk/emsdk_env.sh' first."; \
+ exit 1; \
+ fi
+ @if [ ! -f build-wasm-debug/Makefile ]; then \
+ echo "Configuring WebAssembly debug build (DWARF + source maps)..."; \
+ mkdir -p build-wasm-debug; \
+ cd build-wasm-debug && emcmake cmake .. \
+ -DCMAKE_BUILD_TYPE=Debug \
+ -DMCRF_SDL2=ON \
+ -DMCRF_WASM_DEBUG=ON; \
+ fi
+ @echo "Building McRogueFace for WebAssembly (debug)..."
+ @emmake make -C build-wasm-debug -j$(JOBS)
+ @echo "Debug WASM build complete! Files in build-wasm-debug/"
+ @echo "Debug artifacts: .wasm.map (source map), .symbols (symbol map)"
+ @echo "Run 'make serve-wasm-debug' to test locally"
+
+serve-wasm-debug:
+ @echo "Serving debug WASM build at http://localhost:8080"
+ @echo "Press Ctrl+C to stop"
+ @cd build-wasm-debug && python3 -m http.server 8080
+
+playground-debug:
+ @if ! command -v emcmake >/dev/null 2>&1; then \
+ echo "Error: emcmake not found. Run 'source ~/emsdk/emsdk_env.sh' first."; \
+ exit 1; \
+ fi
+ @if [ ! -f build-playground-debug/Makefile ]; then \
+ echo "Configuring Playground debug build (DWARF + source maps)..."; \
+ mkdir -p build-playground-debug; \
+ cd build-playground-debug && emcmake cmake .. \
+ -DCMAKE_BUILD_TYPE=Debug \
+ -DMCRF_SDL2=ON \
+ -DMCRF_PLAYGROUND=ON \
+ -DMCRF_WASM_DEBUG=ON; \
+ fi
+ @echo "Building McRogueFace Playground for WebAssembly (debug)..."
+ @emmake make -C build-playground-debug -j$(JOBS)
+ @echo "Playground debug build complete! Files in build-playground-debug/"
+ @echo "Run 'make serve-playground-debug' to test locally"
+
+serve-playground-debug:
+ @echo "Serving debug Playground build at http://localhost:8080"
+ @echo "Press Ctrl+C to stop"
+ @cd build-playground-debug && python3 -m http.server 8080
# Current version extracted from source
CURRENT_VERSION := $(shell grep 'MCRFPY_VERSION' src/McRogueFaceVersion.h | sed 's/.*"\(.*\)"/\1/')
diff --git a/docs/WASM_TROUBLESHOOTING.md b/docs/WASM_TROUBLESHOOTING.md
new file mode 100644
index 0000000..0975f94
--- /dev/null
+++ b/docs/WASM_TROUBLESHOOTING.md
@@ -0,0 +1,162 @@
+# WASM / Emscripten Troubleshooting Guide
+
+Practical solutions for common issues when building, testing, and deploying McRogueFace as a WebAssembly application.
+
+## Build Issues
+
+### "emcmake not found"
+
+The Emscripten SDK must be activated in your current shell before building:
+
+```bash
+source ~/emsdk/emsdk_env.sh
+make wasm
+```
+
+This sets `PATH`, `EMSDK`, and other environment variables. You need to re-run it for each new terminal session.
+
+### Build fails during CMake configure
+
+If CMake fails during the Emscripten configure step, delete the build directory and re-configure:
+
+```bash
+rm -rf build-emscripten
+make wasm
+```
+
+The Makefile targets skip CMake if a `Makefile` already exists in the build directory. Stale CMake caches from a prior SDK version or changed options cause configure errors.
+
+### "memory access out of bounds" at startup
+
+Usually caused by insufficient stack or memory. The build defaults to a 2 MB stack (`-sSTACK_SIZE=2097152`) and growable heap (`-sALLOW_MEMORY_GROWTH=1`). If you hit stack limits with deep recursion (e.g. during Python import), increase the stack size in `CMakeLists.txt`:
+
+```cmake
+-sSTACK_SIZE=4194304 # 4 MB
+```
+
+### Link errors about undefined symbols
+
+The Emscripten build uses `-sERROR_ON_UNDEFINED_SYMBOLS=0` because some libc/POSIX symbols are stubbed. If you add new C++ code that calls missing POSIX APIs, you will get a runtime error rather than a link error. Check the browser console for `Aborted(Assertion failed: missing function: ...)`.
+
+## Runtime Issues
+
+### Python import errors
+
+The WASM build bundles a filtered Python stdlib at build time via `--preload-file`. If a Python module is missing at runtime:
+
+1. Check `wasm_stdlib/lib/` — this is the preloaded stdlib tree
+2. If the module should be included, add it to `tools/stdlib_modules.yaml` under the appropriate category
+3. Rebuild: `rm -rf build-emscripten && make wasm`
+
+Some modules (like `socket`, `ssl`, `multiprocessing`) are intentionally excluded because they require OS features unavailable in the browser.
+
+### "Synchronous XMLHttpRequest on the main thread is deprecated"
+
+This warning appears when Python code triggers synchronous file I/O during module import. It's harmless but can cause slight UI freezes. The engine preloads all files into Emscripten's virtual filesystem before Python starts, so actual network requests don't happen.
+
+### IndexedDB / persistent storage errors
+
+The build uses `-lidbfs.js` for persistent storage (save games, user preferences). Common issues:
+
+- **"mkdir failed" on first load**: The engine calls `FS.mkdir('/idbfs')` during initialization. If the path already exists from a prior version, this fails silently. The `emscripten_pre.js` file patches this.
+- **Data not persisting**: Call `FS.syncfs(false, callback)` from JavaScript to flush changes to IndexedDB. The C++ side exposes `sync_storage()` via `Module.ccall`.
+- **Private browsing**: IndexedDB is unavailable in some private/incognito modes. The engine falls back gracefully but data won't persist.
+
+### Black screen / no rendering
+
+Check the browser's developer console (F12) for errors. Common causes:
+
+- **WebGL 2 not supported**: The build requires WebGL 2 (`-sMIN_WEBGL_VERSION=2`). Very old browsers or software renderers may not support it.
+- **Canvas size is zero**: If the HTML container has no explicit size, the canvas may render at 0x0. The custom `shell.html` handles this, but custom embedding needs to set canvas dimensions.
+- **Exception during init**: A Python error during `game.py` execution will abort rendering. Check console for Python tracebacks.
+
+### Audio not working
+
+Audio is stubbed in the WASM build. `SoundBuffer`, `Sound`, and `Music` objects exist but do nothing. This is documented in the Web Build Constraints table in CLAUDE.md.
+
+## Debugging
+
+### Enable debug builds
+
+Use the debug WASM targets for full DWARF symbols and source maps:
+
+```bash
+make wasm-debug # Full game with debug info
+make playground-debug # REPL with debug info
+```
+
+These produce larger binaries but enable:
+- **Source-level debugging** in Chrome DevTools (via DWARF and source maps)
+- **Readable stack traces** (via `--emit-symbol-map`)
+
+### Reading WASM stack traces
+
+Production WASM stack traces show mangled names like `$_ZN7UIFrame6renderEv`. To demangle:
+
+1. Use the debug build which emits a `.symbols` file
+2. Or pipe through `c++filt`: `echo '_ZN7UIFrame6renderEv' | c++filt`
+3. Or use Chrome's DWARF extension for inline source mapping
+
+### Browser developer tools
+
+- **Chrome**: DevTools > Sources > shows C++ source files with DWARF debug builds
+- **Firefox**: Debugger > limited DWARF support, better with source maps
+- **Console**: All `printf`/`std::cout` output goes to the browser console
+- **Network**: Check that `.data` (preloaded files) and `.wasm` loaded successfully
+- **Memory**: Use Chrome's Memory tab to profile WASM heap usage
+
+### Assertions
+
+The build enables `-sASSERTIONS=2` and `-sSTACK_OVERFLOW_CHECK=2` by default (both debug and release). These catch:
+- Null pointer dereferences in WASM memory
+- Stack overflow before it corrupts the heap
+- Invalid Emscripten API usage
+
+## Deployment
+
+### File sizes
+
+Typical build sizes:
+
+| Build | .wasm | .data | .js | Total |
+|-------|-------|-------|-----|-------|
+| Release | ~15 MB | ~25 MB | ~200 KB | ~40 MB |
+| Debug | ~40 MB | ~25 MB | ~300 KB | ~65 MB |
+
+The `.data` file contains the Python stdlib and game assets. Use the "light" stdlib preset to reduce it.
+
+### Serving requirements
+
+WASM files require specific HTTP headers:
+- `Content-Type: application/wasm` for `.wasm` files
+- CORS headers if serving from a CDN
+
+The `make serve` targets use Python's `http.server` which handles MIME types correctly for local development.
+
+### Embedding in custom pages
+
+The build produces an HTML file from `shell.html` (or `shell_game.html`). To embed in your own page, you need:
+
+1. The `.js`, `.wasm`, and `.data` files from the build directory
+2. A canvas element with `id="canvas"`
+3. Load the `.js` file, which bootstraps everything:
+
+```html
+
+
+```
+
+### Game shell vs playground shell
+
+- `make wasm` / `make wasm-game`: Uses `shell.html` or `shell_game.html` — includes REPL widget or fullscreen canvas
+- `make playground`: Uses `shell.html` with REPL chrome — intended for interactive testing
+- Set `MCRF_GAME_SHELL=ON` in CMake for fullscreen-only (no REPL)
+
+## Known Limitations
+
+1. **No dynamic module loading**: All Python modules must be preloaded at build time
+2. **No threading**: JavaScript is single-threaded; Python's `threading` module is non-functional
+3. **No filesystem writes to disk**: Writes go to an in-memory filesystem (optionally synced to IndexedDB)
+4. **No audio**: Sound API is fully stubbed
+5. **No ImGui console**: The debug overlay is desktop-only
+6. **Input differences**: Some keyboard shortcuts are intercepted by the browser (Ctrl+W, F5, etc.)