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

> Learn how ArmorPaint WASM compilation with --target wasm --compile --embed creates a browser-ready package. Discover its runtime constraints and asset embedding process.

- Repository: [Armory 3D/armorpaint](https://github.com/armory3d/armorpaint)
- Tags: internals
- Published: 2026-09-14

---

**The `--target wasm --compile --embed` flags trigger the `WasmExporter` class in [`base/tools/make.js`](https://github.com/armory3d/armorpaint/blob/main/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`](https://github.com/armory3d/armorpaint/blob/main/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:

```bash
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`](https://github.com/armory3d/armorpaint/blob/main/base/project.js), the WASM target registers backend sources and assets:

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

```

This line (located at lines 113-121 in [`base/project.js`](https://github.com/armory3d/armorpaint/blob/main/base/project.js)) ensures the build script copies the JavaScript loader ([`start.js`](https://github.com/armory3d/armorpaint/blob/main/start.js)) and worker script ([`worker.js`](https://github.com/armory3d/armorpaint/blob/main/worker.js)) alongside the generated `start.wasm` binary. The resulting folder contains no external dependencies; opening [`start.js`](https://github.com/armory3d/armorpaint/blob/main/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`](https://github.com/armory3d/armorpaint/blob/main/sources/backends/wasm_system.c).

Each public API function is explicitly exported using compiler attributes:

```c
__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`](https://github.com/armory3d/armorpaint/blob/main/sources/backends/data/wasm/start.js) instantiates the module with a shared memory import object:

```javascript
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`](https://github.com/armory3d/armorpaint/blob/main/sources/backends/data/wasm/worker.js) script loads the same WASM module in a dedicated Web Worker:

```javascript
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`](https://github.com/armory3d/armorpaint/blob/main/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`](https://github.com/armory3d/armorpaint/blob/main/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`](https://github.com/armory3d/armorpaint/blob/main/base/project.js), producing a self-contained distribution.
- The runtime relies on explicitly exported functions in [`sources/backends/wasm_system.c`](https://github.com/armory3d/armorpaint/blob/main/sources/backends/wasm_system.c) with `__attribute__((export_name(...)))` annotations.
- WebAssembly threads require `SharedArrayBuffer` and proper COOP/COEP headers to enable the [`worker.js`](https://github.com/armory3d/armorpaint/blob/main/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`](https://github.com/armory3d/armorpaint/blob/main/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`](https://github.com/armory3d/armorpaint/blob/main/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.