18 KiB
WASM Development Guide
Status: Active Last Updated: 2025-11-25 Purpose: Technical reference for building, debugging, and deploying WASM builds Audience: AI agents and developers working on the YAZE web port
Quick Start
Prerequisites
-
Emscripten SDK installed and activated:
source /path/to/emsdk/emsdk_env.sh -
Verify
emcmakeis available:which emcmake
Building
Debug Build (Local Development)
For debugging memory errors, stack overflows, and async issues:
cmake --preset wasm-debug
cmake --build build-wasm --parallel
Debug flags enabled:
-s SAFE_HEAP=1- Bounds checking on all memory accesses (shows exact error location)-s ASSERTIONS=2- Verbose runtime assertions-g- Debug symbols for source mapping
Output: build-wasm/bin/yaze.html
Release Build (Production)
For optimized performance:
cmake --preset wasm-release
cmake --build build-wasm --parallel
Optimization flags:
-O3- Maximum optimization-flto- Link-time optimization- No debug overhead
Output: build-wasm/bin/yaze.html
Using the Build Scripts
Full Build and Package
./scripts/build-wasm.sh
This will:
- Build the WASM app using
wasm-releasepreset - Package everything into
build-wasm/dist/ - Copy all web assets (CSS, JS, icons, etc.)
Serve Locally
./scripts/serve-wasm.sh [--debug] [port]
./scripts/serve-wasm.sh --dist /path/to/dist --port 9000 # custom path (rare)
./scripts/serve-wasm.sh --force --port 8080 # reclaim a busy port
Serves from build-wasm/dist/ on port 8080 by default. The dist reflects the
last preset configured in build-wasm/ (debug or release), so rebuild with the
desired preset when switching modes.
Important: Always serve from the dist/ directory, not bin/!
Gemini + Antigravity Extension Debugging (Browser)
These steps get Gemini (via the Antigravity browser extension) attached to your local WASM build:
- Build + serve:
./scripts/build-wasm.sh debug # or release ./scripts/serve-wasm.sh --force 8080 # serves dist/, frees the port - In Antigravity, allow/whitelist
http://127.0.0.1:8080(or your chosen port) and open that URL. - Open the Terminal tab (backtick key or bottom panel). Focus is automatic; clicking inside also focuses input.
- Verify hooks from DevTools console:
window.Module?.calledRun // should be true window.z3edTerminal?.executeCommand('help') toggleCollabConsole() // opens collab pane if needed - If input is stolen by global shortcuts, click inside the panel; terminal/collab inputs now stop propagation of shortcuts while focused.
- For a clean slate between sessions:
localStorage.clear(); sessionStorage.clear(); location.reload();
Common Issues and Solutions
Memory Access Out of Bounds
Symptom: RuntimeError: memory access out of bounds
Solution:
- Use
wasm-debugpreset (hasSAFE_HEAP=1) - Rebuild and test
- The error will show exact function name and line number
- Fix the bounds check in the code
- Switch back to
wasm-releasefor production
Stack Overflow
Symptom: Aborted(stack overflow (Attempt to set SP to...))
Solution:
- Stack size is set to 32MB in both presets
- If still overflowing, increase
-s STACK_SIZE=32MBto 64MB or higher - Check for deep recursion in ROM loading code
Async Operation Failed
Symptom: Please compile your program with async support or can't start an async op while one is in progress
Solution:
- Both presets have
-s ASYNCIFY=1enabled - If you see nested async errors, check for:
emscripten_sleep()called during another async operation- Multiple
emscripten_async_call()running simultaneously
- Remove
emscripten_sleep(0)calls if not needed (loading manager already yields)
Icons Not Displaying
Symptom: Material Symbols icons show as boxes or don't appear
Solution:
- Check browser console for CORS errors
- Verify
icons/directory is copied todist/ - Check network tab to see if Google Fonts is loading
- Icons use Material Symbols from CDN - ensure internet connection
ROM Loading Fails Silently
Symptom: ROM file is dropped/selected but nothing happens
Solution:
- Check browser console for errors
- Verify ROM file size is valid (Zelda 3 ROMs are ~1MB)
- Check if
Module.ccallorModule._LoadRomFromWebexists:console.log(typeof Module.ccall); console.log(typeof Module._LoadRomFromWeb); - If functions are missing, verify
EXPORTED_FUNCTIONSinapp.cmakeincludes them - Check
FilesystemManager.readyistruebefore loading
Module Initialization Fails
Symptom: createYazeModule is not defined or similar errors
Solution:
- Verify
MODULARIZE=1andEXPORT_NAME='createYazeModule'are inapp.cmake - Check that
yaze.jsis loaded beforeapp.jstries to callcreateYazeModule() - Look for JavaScript errors in console during page load
Directory Listing Instead of App
Symptom: Browser shows file list instead of the app
Solution:
- Server must run from
build-wasm/dist/directory - Use
scripts/serve-wasm.shwhich handles this automatically - Or manually:
cd build-wasm/dist && python3 -m http.server 8080
File Structure
build-wasm/
├── bin/ # Raw build output (yaze.html, yaze.wasm, etc.)
└── dist/ # Packaged output for deployment
├── index.html # Main entry point (copied from bin/yaze.html)
├── yaze.js # WASM loader
├── yaze.wasm # Compiled WASM binary
├── *.css # Web stylesheets
├── *.js # Web JavaScript
├── icons/ # PWA icons
└── ...
Key Files
Build Configuration & Scripts:
CMakePresets.json- Build configurations (wasm-debug,wasm-release)src/app/app.cmake- WASM linker flags (EXPORTED_FUNCTIONS, MODULARIZE, etc.)scripts/build-wasm.sh- Full build and packaging scriptscripts/serve-wasm.sh- Local development server
Web Assets:
src/web/shell.html- HTML shell templatesrc/web/app.js- Main UI logic, module initializationsrc/web/core/- Core JavaScript functionality (agent automation, control APIs)src/web/components/- UI components (terminal, collaboration, etc.)src/web/styles/- Stylesheets and theme definitionssrc/web/pwa/- Progressive Web App files (service worker, manifest)src/web/debug/- Debug and development utilities
C++ Platform Layer:
src/app/platform/wasm/wasm_control_api.cc- Control API implementation (JS interop)src/app/platform/wasm/wasm_control_api.h- Control API declarationssrc/app/platform/wasm/wasm_session_bridge.cc- Session/collaboration bridgesrc/app/platform/wasm/wasm_drop_handler.cc- File drop handlersrc/app/platform/wasm/wasm_loading_manager.cc- Loading progress UIsrc/app/platform/wasm/wasm_storage.cc- IndexedDB storage with memory-safe error handlingsrc/app/platform/wasm/wasm_error_handler.cc- Error handling with callback cleanup
GUI Utilities:
src/app/gui/core/popup_id.h- Session-aware ImGui popup ID generation
CI/CD:
.github/workflows/web-build.yml- CI/CD for GitHub Pages
CMake WASM Configuration
The WASM build uses specific Emscripten flags in src/app/app.cmake:
# Key flags for WASM build
-s MODULARIZE=1 # Allows async initialization via createYazeModule()
-s EXPORT_NAME='createYazeModule' # Function name for module factory
-s EXPORTED_RUNTIME_METHODS='[...]' # Runtime methods available in JS
-s EXPORTED_FUNCTIONS='[...]' # C functions callable from JS
Important Exports:
_main,_SetFileSystemReady,_LoadRomFromWeb- Core functions_yazeHandleDroppedFile,_yazeHandleDropError- Drag & drop handlers_yazeHandleDragEnter,_yazeHandleDragLeave- Drag state tracking_malloc,_free- Memory allocation for JS interop
Runtime Methods:
ccall,cwrap- Function callingstringToUTF8,UTF8ToString,lengthBytesUTF8- String conversionFS,IDBFS- Filesystem accessallocateUTF8- String allocation helper
Debugging Tips
-
Use Browser DevTools:
- Console tab: WASM errors, async errors
- Network tab: Check if WASM files load
- Sources tab: Source maps (if
-gflag used)
-
Enable Verbose Logging:
- Check browser console for Emscripten messages
- Look for
[symbolize_emscripten.inc]warnings (can be ignored)
-
Test Locally First:
- Always test with
wasm-debugbefore deploying - Use
serve-wasm.shto ensure correct directory structure
- Always test with
-
Memory Issues:
- Use
wasm-debugpreset for precise error locations - Check heap resize messages in console
- Verify
INITIAL_MEMORYis sufficient (64MB default)
- Use
ImGui ID Conflict Prevention
When multiple editors are docked together, ImGui popup IDs must be unique to prevent undefined behavior. The popup_id.h utility provides session-aware ID generation.
Usage
#include "app/gui/core/popup_id.h"
// Generate unique popup ID (default session)
std::string id = gui::MakePopupId(gui::EditorNames::kOverworld, "Entrance Editor");
ImGui::OpenPopup(id.c_str());
// Match in BeginPopupModal
if (ImGui::BeginPopupModal(
gui::MakePopupId(gui::EditorNames::kOverworld, "Entrance Editor").c_str(),
nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
// ...
ImGui::EndPopup();
}
// With explicit session ID for multi-session support
std::string id = gui::MakePopupId(session_id, "Overworld", "Entrance Editor");
ID Pattern
Pattern: s{session_id}.{editor}::{popup_name}
Examples:
s0.Overworld::Entrance Editors0.Palette::CustomPaletteColorEdits1.Dungeon::Room Properties
Available Editor Names
Predefined constants in gui::EditorNames:
kOverworld- Overworld editorkPalette- Palette editorkDungeon- Dungeon editorkGraphics- Graphics editorkSprite- Sprite editor
Why This Matters
Without unique IDs, clicking "Entrance Editor" popup in one docked window may open/close the popup in a different docked editor, causing confusing behavior. The session+editor prefix guarantees uniqueness.
Deployment
GitHub Pages
The workflow (.github/workflows/web-build.yml) automatically:
- Builds using
wasm-releasepreset - Packages to
build-wasm/dist/ - Deploys to GitHub Pages
No manual steps needed - just push to master/main branch.
Manual Deployment
- Build:
./scripts/build-wasm.sh - Upload
build-wasm/dist/contents to your web server - Ensure server serves
index.htmlas default
Performance Notes
Debug build (wasm-debug):
- 2-5x slower due to SAFE_HEAP
- 10-20% slower due to ASSERTIONS
- Use only for debugging
Release build (wasm-release):
- Optimized with
-O3and-flto - No debug overhead
- Use for production and performance testing
When to Use Each Preset
Use wasm-debug when:
- Debugging memory access errors
- Investigating stack overflows
- Testing async operation issues
- Need source maps for debugging
Use wasm-release when:
- Testing performance
- Preparing for deployment
- CI/CD builds
- Production releases
Performance Best Practices
Based on the November 2025 performance audit, follow these guidelines when developing for WASM:
JavaScript Performance
Event Handling:
- Avoid adding event listeners to both canvas AND document for the same events
- Use WeakMap to cache processed event objects and avoid redundant work
- Only sanitize/process properties relevant to the specific event type
Data Structures:
- Use circular buffers instead of arrays with
shift()for log/history buffers Array.shift()is O(n) - avoid in high-frequency code paths- Example circular buffer pattern:
var buffer = new Array(maxSize); var index = 0; function add(item) { buffer[index] = item; index = (index + 1) % maxSize; }
Polling/Intervals:
- Always store interval/timeout handles for cleanup
- Clear intervals when the feature is no longer needed
- Set max retry limits to prevent infinite polling
- Use flags (e.g.,
window.YAZE_MODULE_READY) to track initialization state
Memory Management
Service Worker Caching:
- Implement cache size limits with LRU eviction
- Don't cache indefinitely - set
MAX_CACHE_SIZEconstants - Clean up old cache versions on activation
C++ Memory in EM_JS:
- Always
free()allocated memory in error paths, not just success paths - Check if pointers are non-null before freeing
- Example pattern:
if (result != 0) { if (data_ptr) free(data_ptr); // Always free on error return absl::InternalError(...); }
Callback Cleanup:
- Add timeout/expiry tracking for stored callbacks
- Register cleanup handlers for page unload events
- Periodically clean stale entries (e.g., every minute)
Race Condition Prevention
Module Initialization:
- Use explicit ready flags, not just existence checks
- Set ready flag AFTER all initialization is complete
- Pattern:
window.YAZE_MODULE_READY = false; createModule().then(function(instance) { window.Module = instance; window.YAZE_MODULE_READY = true; // Set AFTER assignment });
Promise Initialization:
- Create promises synchronously before any async operations
- Use synchronous lock patterns to prevent duplicate promises:
if (this.initPromise) return this.initPromise; this.initPromise = new Promise(...); // Immediate assignment // Then do async work
Redundant Operations:
- Use flags to track completed operations
- Avoid multiple setTimeout calls for the same operation
- Check flags before executing expensive operations
File Handling
Avoid Double Reading:
- When files are read via FileReader, pass the
Uint8Arraydirectly - Don't re-read files in downstream handlers
- Use methods like
handleRomData(filename, data)instead ofhandleRomUpload(file)
C++ Mutex Best Practices
JS Calls and Locks:
- Always call JS functions OUTSIDE mutex locks
- JS calls can block/yield - holding a lock during JS calls risks deadlock
- Pattern:
std::string data; { std::lock_guard<std::mutex> lock(mutex_); data = operations_[handle]->data; // Copy inside lock } js_function(data.c_str()); // Call outside lock
JavaScript APIs
The WASM build exposes JavaScript APIs for programmatic control and debugging. These are available after the module initializes.
API Documentation
For detailed API reference documentation:
- Control & GUI APIs - See
docs/internal/wasm-yazeDebug-api-reference.mdforwindow.yaze.*API documentationwindow.yaze.editor- Query editor state and selectionwindow.yaze.data- Read-only ROM data accesswindow.yaze.gui- GUI element discovery and automationwindow.yaze.control- Programmatic editor control
- Debug APIs - See
docs/internal/wasm-yazeDebug-api-reference.mdforwindow.yazeDebug.*API documentation- ROM reading, graphics diagnostics, arena status, emulator state
- Palette inspection, timeline analysis
- AI-formatted state dumps for Gemini/Antigravity debugging
Quick API Check
To verify APIs are available in the browser console:
// Check if module is ready
window.yazeDebug.isReady()
// Get ROM status
window.yazeDebug.rom.getStatus()
// Get formatted state for AI
window.yazeDebug.formatForAI()
Gemini/Antigravity Debugging
For AI-assisted debugging workflows using the Antigravity browser extension, see docs/internal/agents/wasm-antigravity-playbook.md for detailed instructions on:
- Connecting Gemini to your local WASM build
- Using debug APIs with AI agents
- Common debugging workflows and examples
Dungeon Object Rendering Debugging
For debugging dungeon object rendering issues (objects appearing at wrong positions, wrong sprites, visual discrepancies), see docs/internal/wasm_dungeon_debugging.md Section 12: "Antigravity: Debugging Dungeon Object Rendering Issues".
Quick Reference for Antigravity:
// 1. Capture screenshot for visual analysis
const result = window.yaze.gui.takeScreenshot();
const dataUrl = result.dataUrl;
// 2. Get room data for comparison
const roomData = window.aiTools.getRoomData();
const tiles = window.yaze.data.getRoomTiles(roomData.id || 0);
// 3. Check graphics loading status
const arena = window.yazeDebug.arena.getStatus();
// 4. Full diagnostic dump
async function getDiagnostic(roomId) {
const data = {
room_id: roomId,
objects: window.yaze.data.getRoomObjects(roomId),
properties: window.yaze.data.getRoomProperties(roomId),
arena: window.yazeDebug?.arena?.getStatus(),
visible_cards: window.aiTools.getVisibleCards()
};
await navigator.clipboard.writeText(JSON.stringify(data, null, 2));
return data;
}
Common Issues:
| Symptom | Check |
|---|---|
| Objects invisible | window.yazeDebug.arena.getStatus().pending_textures |
| Wrong position | Compare getRoomObjects() pixel coords vs visual |
| Wrong colors | Module.getDungeonPaletteEvents() |
| Black squares | Wait for deferred texture loading |
Additional Resources
Primary WASM Documentation (3 docs total)
- This Guide - Building, debugging, CMake config, performance, ImGui ID conflict prevention
- WASM API Reference - Full JavaScript API documentation, Agent Discoverability Infrastructure
- WASM Antigravity Playbook - AI agent workflows, Gemini integration, quick start guides
Archived: archive/wasm-docs-2025/ - Historical WASM docs