ArmorPaint WASM Compilation: How `--target wasm --compile --embed` Works

The --target wasm --compile --embed flags trigger the WasmExporter class in base/tools/make.js to compile ArmorPaint's C/Haxe sources into a self-contained WebAssembly binary using Clang, embedding all assets into a browser-ready package that requires SharedArrayBuffer support and runs without standard library dependencies.

When targeting web deployment, the armory3d/armorpaint build system transforms native source code into a portable WebAssembly application. The --target wasm --compile --embed workflow produces a standalone browser package by orchestrating Clang compilation, asset bundling, and custom runtime initialization through a layered architecture of JavaScript loaders and C exports.

Compilation Pipeline with WasmExporter

The build process begins in base/tools/make.js where the WasmExporter class extends the base Exporter to handle WebAssembly-specific logic.

The exporter configures Clang to emit a WASM binary with strict constraints:

clang --target=wasm32 -nostdlib -matomics -mbulk-memory \
     -Wl,--import-memory,--shared-memory,--allow-undefined,--no-entry,\
     --initial-memory=671088640,--max-memory=671088640,-z,stack-size=256000

Key flags include:

  • -nostdlib removes the standard C library dependency
  • --no-entry indicates the module has no main function
  • -matomics and -mbulk-memory enable WebAssembly thread primitives
  • --shared-memory and --import-memory configure the memory model for multi-threading
  • Memory is capped at approximately 640 MiB via --initial-memory and --max-memory

The exporter automatically includes the WASM-specific runtime layer from sources/backends/wasm_system.* and sources/backends/wasm_thread.* alongside all project C/C++ files.

Asset Embedding and Project Configuration

The --embed flag instructs the build system to bundle all runtime assets—shaders, textures, and UI files—into the final distribution folder.

In base/project.js, the WASM target registers backend sources and assets:

project.add_assets("sources/backends/data/wasm/*");

This line (located at lines 113-121 in base/project.js) ensures the build script copies the JavaScript loader (start.js) and worker script (worker.js) alongside the generated start.wasm binary. The resulting folder contains no external dependencies; opening start.js in a browser loads everything required to run the application.

Runtime Layer and JavaScript Integration

The bridge between JavaScript and WebAssembly is implemented in sources/backends/wasm_system.c.

Each public API function is explicitly exported using compiler attributes:

__attribute__((export_name("wasm_start"))) void wasm_start() { ... }
__attribute__((export_name("wasm_update"))) void wasm_update() { ... }

These exports (lines 100-136) include input callbacks like wasm_mousedown, keyboard handlers, and networking functions. The JavaScript loader in sources/backends/data/wasm/start.js instantiates the module with a shared memory import object:

const { instance } = await WebAssembly.instantiate(wasmBytes, {
    env: { memory },
    imports: stubs
});

// Initialize the engine
instance.exports.wasm_start();

// Drive the render loop
while (true) {
    await instance.exports.wasm_update();
}

Threading Architecture

ArmorPaint leverages WebAssembly threads via SharedArrayBuffer to parallelize work across CPU cores. The sources/backends/data/wasm/worker.js script loads the same WASM module in a dedicated Web Worker:

onmessage = async ({ data: { wasm_module, memory, func_ptr, param_ptr, done_ptr } }) => {
    const { instance } = await WebAssembly.instantiate(wasm_module, { 
        env: { memory }, 
        imports: stubs 
    });
    instance.exports.wasm_thread_run(func_ptr, param_ptr, done_ptr);
};

This architecture requires the host page to serve COOP (Cross-Origin-Opener-Policy) and COEP (Cross-Origin-Embedder-Policy) headers. Without these security headers, browsers block SharedArrayBuffer, causing the worker initialization to fail.

Runtime Constraints and Limitations

Running the compiled output imposes several strict constraints on the execution environment:

No Standard Library – The binary cannot use stdio, malloc, or free from libc. Instead, it relies on custom implementations (wasm_malloc/wasm_free) defined in the project's stdlib.c.

Fixed Memory Ceiling – The linker flags lock initial and maximum linear memory at 640 MiB. Exceeding this limit immediately crashes the module with an out-of-memory error.

Explicit Initialization – Unlike typical WASM modules that expose a _start function, ArmorPaint requires the JavaScript host to explicitly call wasm_start() before any other export. Generic WASM runtimes cannot execute this binary without the custom loader.

Single-Threaded Fallback – When wasm_can_suspend evaluates to false (indicating SharedArrayBuffer is unavailable), the loader queues input events in the wasm_queued array until the next frame processes them. This prevents event loss but eliminates concurrent execution benefits.

Summary

  • The WasmExporter class in base/tools/make.js orchestrates Clang compilation with -nostdlib and fixed 640 MiB memory limits.
  • Assets are embedded via project.add_assets() in base/project.js, producing a self-contained distribution.
  • The runtime relies on explicitly exported functions in sources/backends/wasm_system.c with __attribute__((export_name(...))) annotations.
  • WebAssembly threads require SharedArrayBuffer and proper COOP/COEP headers to enable the worker.js thread pool.
  • Applications must use custom memory management (wasm_malloc/wasm_free) instead of standard library functions.

Frequently Asked Questions

What compiler flags does ArmorPaint use for WASM builds?

ArmorPaint invokes Clang with --target=wasm32 -nostdlib -matomics -mbulk-memory and linker flags including --import-memory, --shared-memory, --allow-undefined, --no-entry, and fixed memory sizes of 671088640 bytes (640 MiB) for both initial and maximum heap.

Why does the WASM build require SharedArrayBuffer?

The engine uses sources/backends/data/wasm/worker.js to implement multi-threading via WebAssembly threads, which depend on SharedArrayBuffer to share linear memory between the main thread and workers. Browsers require Cross-Origin-Opener-Policy (COOP) and Cross-Origin-Embedder-Policy (COEP) headers to enable this feature.

How does JavaScript initialize the ArmorPaint WASM module?

The sources/backends/data/wasm/start.js loader fetches start.wasm, instantiates it with an import object containing env: { memory }, and explicitly calls wasm_start() to initialize the engine, followed by repeated invocations of wasm_update() to drive the render loop.

What happens if the browser doesn't support WebAssembly threads?

When wasm_can_suspend returns false, the JavaScript loader falls back to a single-threaded mode by queuing input callbacks in the wasm_queued array until the next frame, ensuring events are not lost but processed sequentially rather than concurrently.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →