Files
yaze/docs/internal/wasm/dev_guide.md

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

  1. Emscripten SDK installed and activated:

    source /path/to/emsdk/emsdk_env.sh
    
  2. Verify emcmake is 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:

  1. Build the WASM app using wasm-release preset
  2. Package everything into build-wasm/dist/
  3. 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:

  1. Build + serve:
    ./scripts/build-wasm.sh debug           # or release
    ./scripts/serve-wasm.sh --force 8080    # serves dist/, frees the port
    
  2. In Antigravity, allow/whitelist http://127.0.0.1:8080 (or your chosen port) and open that URL.
  3. Open the Terminal tab (backtick key or bottom panel). Focus is automatic; clicking inside also focuses input.
  4. Verify hooks from DevTools console:
    window.Module?.calledRun                 // should be true
    window.z3edTerminal?.executeCommand('help')
    toggleCollabConsole()                    // opens collab pane if needed
    
  5. If input is stolen by global shortcuts, click inside the panel; terminal/collab inputs now stop propagation of shortcuts while focused.
  6. 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:

  1. Use wasm-debug preset (has SAFE_HEAP=1)
  2. Rebuild and test
  3. The error will show exact function name and line number
  4. Fix the bounds check in the code
  5. Switch back to wasm-release for 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=32MB to 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=1 enabled
  • 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 to dist/
  • 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:

  1. Check browser console for errors
  2. Verify ROM file size is valid (Zelda 3 ROMs are ~1MB)
  3. Check if Module.ccall or Module._LoadRomFromWeb exists:
    console.log(typeof Module.ccall);
    console.log(typeof Module._LoadRomFromWeb);
    
  4. If functions are missing, verify EXPORTED_FUNCTIONS in app.cmake includes them
  5. Check FilesystemManager.ready is true before loading

Module Initialization Fails

Symptom: createYazeModule is not defined or similar errors

Solution:

  • Verify MODULARIZE=1 and EXPORT_NAME='createYazeModule' are in app.cmake
  • Check that yaze.js is loaded before app.js tries to call createYazeModule()
  • 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.sh which 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 script
  • scripts/serve-wasm.sh - Local development server

Web Assets:

  • src/web/shell.html - HTML shell template
  • src/web/app.js - Main UI logic, module initialization
  • src/web/core/ - Core JavaScript functionality (agent automation, control APIs)
  • src/web/components/ - UI components (terminal, collaboration, etc.)
  • src/web/styles/ - Stylesheets and theme definitions
  • src/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 declarations
  • src/app/platform/wasm/wasm_session_bridge.cc - Session/collaboration bridge
  • src/app/platform/wasm/wasm_drop_handler.cc - File drop handler
  • src/app/platform/wasm/wasm_loading_manager.cc - Loading progress UI
  • src/app/platform/wasm/wasm_storage.cc - IndexedDB storage with memory-safe error handling
  • src/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 calling
  • stringToUTF8, UTF8ToString, lengthBytesUTF8 - String conversion
  • FS, IDBFS - Filesystem access
  • allocateUTF8 - String allocation helper

Debugging Tips

  1. Use Browser DevTools:

    • Console tab: WASM errors, async errors
    • Network tab: Check if WASM files load
    • Sources tab: Source maps (if -g flag used)
  2. Enable Verbose Logging:

    • Check browser console for Emscripten messages
    • Look for [symbolize_emscripten.inc] warnings (can be ignored)
  3. Test Locally First:

    • Always test with wasm-debug before deploying
    • Use serve-wasm.sh to ensure correct directory structure
  4. Memory Issues:

    • Use wasm-debug preset for precise error locations
    • Check heap resize messages in console
    • Verify INITIAL_MEMORY is sufficient (64MB default)

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 Editor
  • s0.Palette::CustomPaletteColorEdit
  • s1.Dungeon::Room Properties

Available Editor Names

Predefined constants in gui::EditorNames:

  • kOverworld - Overworld editor
  • kPalette - Palette editor
  • kDungeon - Dungeon editor
  • kGraphics - Graphics editor
  • kSprite - 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:

  1. Builds using wasm-release preset
  2. Packages to build-wasm/dist/
  3. Deploys to GitHub Pages

No manual steps needed - just push to master/main branch.

Manual Deployment

  1. Build: ./scripts/build-wasm.sh
  2. Upload build-wasm/dist/ contents to your web server
  3. Ensure server serves index.html as 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 -O3 and -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_SIZE constants
  • 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 Uint8Array directly
  • Don't re-read files in downstream handlers
  • Use methods like handleRomData(filename, data) instead of handleRomUpload(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.md for window.yaze.* API documentation
    • window.yaze.editor - Query editor state and selection
    • window.yaze.data - Read-only ROM data access
    • window.yaze.gui - GUI element discovery and automation
    • window.yaze.control - Programmatic editor control
  • Debug APIs - See docs/internal/wasm-yazeDebug-api-reference.md for window.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

External Resources