Skip to content

(wasm-call …)

Calls a function from a WebAssembly (WASM) module every frame and makes its result available to a (queue …) write. Use it for values the CPU computes per frame, a camera matrix being the usual one.

(wasm-call :name myCall
:file "path/to/module.wasm"
:func functionName
:args [time-total canvas-width]
:returns "f32")
Key Type Required Default Description
:name symbol Yes - Unique name, referenced by a (write-buffer … :data …)
:file string Yes - WASM file path, relative to the source file
:func symbol Yes - Exported function called each frame
:returns string Yes - Result type, which fixes how many bytes are written
:args array No [] Per-frame arguments (at most 32)

Type: string

The WASM file is read at compile time, relative to the source file, and its bytes are embedded in PNGB (PNGine’s compiled bytecode). Two (wasm-call …) forms naming the same path share one module:

:file "math.wasm"

Type: symbol

Exported function name to call:

:func computeMatrix

Type: array

Arguments to pass to the function. Each is either a built-in symbol the runtime fills every frame, or a numeric literal:

Built-in arg Description
canvas-width Canvas width in pixels
canvas-height Canvas height in pixels
time-total Elapsed seconds since start
time-delta Seconds since last frame
:args [time-total canvas-width canvas-height]
:args [45.0 1.777 0.1 100.0]

An integer literal is passed as a u32, anything else as an f32. The call opcode carries at most 32 arguments; a longer list is rejected at compile time rather than truncated.

Type: string

The result type, which is how the compiler knows how many bytes to copy out of the module’s memory. The accepted spellings are exactly these:

Type Bytes
"f32", "i32", "u32" 4
"vec2" 8
"vec3" 12
"vec4" 16
"mat3x3" 36
"mat4x4" 64

Anything else is a compile error: (wasm-call …) declares :returns ‘vec4f’, which is not a return type the wire can carry. The longer array<f32, N> spelling belongs to (wasm-data …), the once-at-creation form, not here.

(wasm-call :name perspective :file "matrices.wasm" :func perspective
:args [1.0472 1.777 0.1 100.0] :returns "mat4x4")
(buffer :name mvpBuffer :size 64 :usage [uniform copy-dst])
(queue :name writeMVP
(write-buffer :buffer mvpBuffer :offset 0 :data perspective))

A (write-buffer … :data <name>) naming the call writes its result into the buffer each frame. That one key covers all three things the source can be, a runtime built-in such as pngine-inputs, a (data …) entry, or a (wasm-call …); which one it is follows from what the name was declared as.

(wasm-call :name modelMatrix :file "transform.wasm" :func rotateY
:args [time-total] :returns "mat4x4")
(wasm-call :name viewMatrix :file "transform.wasm" :func lookAt
:args [0 0 5 0 0 0 0 1 0] :returns "mat4x4")
(wasm-call :name projMatrix :file "transform.wasm" :func perspective
:args [45 1.0 0.1 100] :returns "mat4x4")

A table the shader reads but nothing animates does not belong here: a (wasm-data …) inside a (data …) runs once at buffer-create time instead, and its :returns may be an array.

(data :name noiseTable
(wasm-data :file "noise.wasm" :func generateNoise :returns "array<f32,1024>"))
(buffer :name noiseBuffer :usage [storage] :data noiseTable)
(wasm-call :name rotatedMVP :file "matrices.wasm" :func rotatingMVP
:args [time-total canvas-width canvas-height] :returns "mat4x4")

The module either exports its memory as memory or imports one as env.memory (the player supplies a one-page memory under that name and reads the result from whichever the module uses; an exported memory wins). The called function returns a pointer: a byte offset into that memory where the result already sits. The runtime copies :returns-many bytes from there into the target buffer, so the function writes its output wherever it likes and hands back the address.

// Example WASM source (C)
static float out[16];
__attribute__((export_name("perspective")))
float *perspective(float fov, float aspect, float near, float far) {
// fill out[0..15]
return out; // the runtime reads 64 bytes from here
}

Or in Rust:

static mut OUT: [f32; 16] = [0.0; 16];
#[unsafe(no_mangle)]
pub extern "C" fn perspective(fov: f32, aspect: f32, near: f32, far: f32) -> *const f32 {
// fill OUT
unsafe { OUT.as_ptr() }
}

examples/assets/mvp.wasm in the engine repository is a working module of this shape: buildMVPMatrix(f32, f32, f32) -> i32, exporting memory.

(wasm-call …) runs every frame. Two neighbours run at other times:

  • (wasm-data …), a positional child of (data …), runs once at buffer-create time and fills the buffer through mappedAtCreation. It takes the same :file / :func / :returns keys, and its :returns also accepts array<f32, N>.
  • (buffer … :file "mod.wasm") takes both the size and the initial bytes from the module’s own l / s / gen exports, so no :size is authored.

Native --frame rendering stubs the WASM tiers, so a document that depends on one of these renders in the browser rather than through the CLI.

(wasm-call …) calls :func with :args, once per frame
returned pointer into the module's memory
(write-buffer :buffer B :offset O :data <call name>) copies :returns bytes
GPU buffer, readable by the shaders bound to it
Rule Error
Two (wasm-call …) forms may not share a name, and a (data …) may not share it either: they are one reference group duplicate_cross_ref_target
:file, :func and :returns are required missing_required_key
The file must exist, relative to the source cannot read 'math.wasm': FileNotFound
:returns must be one of the spellings above (wasm-call …) declares :returns ‘X’, which is not a return type the wire can carry
:args takes at most 32 arguments wasm-call ‘:args’ lists 40 arguments, but the call opcode carries at most 32
An :args symbol must be one of the four runtime built-ins; a (define …) name is not accepted here, write it as arithmetic, (* SCALE 1) union_no_branch_matched