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

538 lines
18 KiB
Markdown

# 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:
```bash
source /path/to/emsdk/emsdk_env.sh
```
2. Verify `emcmake` is available:
```bash
which emcmake
```
### Building
#### Debug Build (Local Development)
For debugging memory errors, stack overflows, and async issues:
```bash
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:
```bash
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
```bash
./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
```bash
./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:
```bash
./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:
```js
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:
```js
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`:
```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
```cpp
#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:
```javascript
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:
```cpp
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:
```javascript
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:
```javascript
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:
```cpp
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:
```javascript
// 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`](./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`](../wasm_dungeon_debugging.md) Section 12: "Antigravity: Debugging Dungeon Object Rendering Issues".
**Quick Reference for Antigravity:**
```javascript
// 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](../wasm-yazeDebug-api-reference.md) - Full JavaScript API documentation, Agent Discoverability Infrastructure
- [WASM Antigravity Playbook](./wasm-antigravity-playbook.md) - AI agent workflows, Gemini integration, quick start guides
**Archived:** `archive/wasm-docs-2025/` - Historical WASM docs
### External Resources
- [Emscripten Documentation](https://emscripten.org/docs/getting_started/index.html)
- [WASM Memory Management](https://emscripten.org/docs/porting/emscripten-runtime-environment.html)
- [ASYNCIFY Guide](https://emscripten.org/docs/porting/asyncify.html)