Skip to content

(queue …)

Defines queue operations for writing data to buffers and copying images to textures.

(queue :name writeUniforms
(write-buffer :buffer uniforms :offset 0 :data pngine-inputs))
(queue :name loadTexture
(copy-external-image-to-texture
(source :image photo)
(destination :texture albedo)))

A (queue …) holds its actions as positional sub-forms, run in document order when the frame reaches the queue. The five actions are:

Action Purpose
write-buffer Write bytes into a buffer
copy-buffer-to-buffer GPU buffer → buffer copy
copy-texture-to-texture GPU texture → texture copy
copy-external-image-to-texture Upload a decoded image into a texture
resolve-query-set Copy resolved query results into a buffer
(queue :name readback
(resolve-query-set :query-set occ :first-query 0 :query-count 6
:destination resolveBuf :destination-offset 0)
(copy-buffer-to-buffer :source resolveBuf :source-offset 0
:destination readbackBuf :destination-offset 0 :size 48))
Key Type Required Description
:name symbol Yes Unique name (referenced from (frame …))
actions sub-forms No Positional action sub-forms, run in the order written

Writes data to a GPU buffer.

Key Type Required Description
:buffer reference Yes Target buffer (needs copy-dst usage)
:offset number No Byte offset, a multiple of 4 (default: 0)
:data reference/symbol Yes Where the bytes come from

:data is one key for the three things a source can be: a built-in runtime source, a (data …) name, or a (wasm-call …) name. Which one it is follows from what that name was declared as, so there is no second key to choose between them.

Data sources:

Static data reference (from (data …)):

(data :name vertexData :float32 [0 0.5 -0.5 -0.5 0.5 -0.5])
(write-buffer :buffer vertices :data vertexData)

Runtime uniform data (built-in):

(write-buffer :buffer uniforms :offset 0 :data pngine-inputs)

The pngine-inputs symbol, the built-in source of time and canvas size, provides 16 bytes of runtime data written each frame by the executor (the small WASM interpreter embedded in each payload, which turns bytecode into GPU commands). The other built-in sources are scene-time-inputs (12 bytes) and pointer-inputs (48 bytes).

WASM result (from a (wasm-call …), a WebAssembly module the executor calls once per frame):

(wasm-call :name mvp :file "matrices.wasm" :func rotatingMVP :returns "mat4x4"
:args [time-total canvas-width canvas-height])
(write-buffer :buffer mvpBuffer :offset 0 :data mvp)

Naming a (wasm-call …) there calls it at frame time and writes its result into the buffer.

Copies a decoded image (an (image-bitmap …)) into a texture. Each end of a copy is a dictionary in WebGPU, so each is a child form: exactly one (source …) and exactly one (destination …).

(source …):

Key Type Required Description
:image reference Yes The (image-bitmap …) to upload

(destination …):

Key Type Required Default Description
:texture reference Yes - Target texture (needs copy-dst usage)
:mip-level number No 0 Destination mip level
:origin array No [0 0 0] [x y z] in texels; z picks the array layer or depth slice

:mip-level and :origin belong to the destination, not to the copy: they say which subresource receives the pixels, which is how a cubemap’s six faces load into one texture.

(copy-external-image-to-texture
(source :image photo)
(destination :texture albedo :mip-level 0 :origin [0 0 0]))

Copies between GPU textures without a CPU round trip, which is how a frame keeps a copy of itself for feedback. Both ends are child forms here too, and each takes one key:

Sub-form Key Type Required Description
(source …) :texture reference Yes Source texture, or context-current-texture
(destination …) :texture reference Yes Destination texture, or context-current-texture

context-current-texture names the canvas’s current texture; any other value is a (texture …) name. Neither texture copy takes a size: PNGine copies the whole source.

Copies bytes between two GPU buffers without a CPU round trip.

Key Type Required Default Description
:source reference Yes - Source buffer (needs copy-src usage)
:source-offset number No 0 Byte offset into the source
:destination reference Yes - Destination buffer (needs copy-dst usage)
:destination-offset number No 0 Byte offset into the destination
:size number Yes - Bytes to copy

All three of :source-offset, :destination-offset and :size must be multiples of 4.

(queue :name stage
(copy-buffer-to-buffer :source resolveBuf :source-offset 0
:destination readbackBuf :destination-offset 0 :size 48))

Copies resolved GPU query results into a buffer, 8 bytes per query.

Key Type Required Default Description
:query-set reference Yes - The (query-set …) to resolve
:first-query number No 0 Index of the first query to resolve
:query-count number Yes - Number of queries to resolve
:destination reference Yes - Destination buffer (needs query-resolve usage)
:destination-offset number No 0 Byte offset into the destination

WebGPU requires :destination-offset to be a multiple of 256 here.

(queue :name resolve
(resolve-query-set :query-set occ :first-query 0 :query-count 6
:destination resolveBuf :destination-offset 0))

Three reserved symbols name runtime data the player fills in each frame. Any of them is a write-buffer :data value, and none of them can be declared as a form name:

Symbol Size Contents (all f32)
pngine-inputs 16 bytes time, canvasWidth, canvasHeight, aspect
scene-time-inputs 12 bytes time, canvasWidth, canvasHeight (the first 12 bytes of pngine-inputs)
pointer-inputs 48 bytes x, y, clickX, clickY, dx, dy, buttons, pressure, modifiers, scrollX, scrollY, _pad

The pngine-inputs symbol provides runtime data that updates every frame. It writes 16 bytes:

Field Type Offset Description
time f32 0 Elapsed seconds since start
width f32 4 Canvas width in pixels
height f32 8 Canvas height in pixels
aspect f32 12 width / height

Your uniform buffer must be at least 16 bytes when using pngine-inputs.

Your WGSL struct must match this layout:

struct PngineInputs {
time: f32,
canvasWidth: f32,
canvasHeight: f32,
aspect: f32,
}
@group(0) @binding(0) var<uniform> inputs: PngineInputs;
(buffer :name uniforms :size 16 :usage [uniform copy-dst])
(queue :name writeTime
(write-buffer :buffer uniforms :offset 0 :data pngine-inputs))
(frame :name main :perform [writeTime renderPass])
(data :name vertexData :float32 [0 0.5 -0.5 -0.5 0.5 -0.5])
(buffer :name vertices :size 24 :usage [vertex copy-dst])
(queue :name initVertices
(write-buffer :buffer vertices :data vertexData))
(data :name imageFile :file "texture.png" :mime "image/png")
(image-bitmap :name textureBitmap :data imageFile)
(texture :name colorMap :size textureBitmap
:format rgba8unorm
:usage [texture-binding copy-dst render-attachment])
(queue :name loadTexture
(copy-external-image-to-texture
(source :image textureBitmap)
(destination :texture colorMap)))
(frame :name main :perform [loadTexture renderPass])
(wasm-call :name mvp :file "matrices.wasm" :func rotatingMVP
:args [time-total canvas-width canvas-height] :returns "mat4x4")
(buffer :name mvpBuffer :size 64 :usage [uniform copy-dst])
(queue :name writeMVP
(write-buffer :buffer mvpBuffer :offset 0 :data mvp))
(texture :name sourceTexture :size canvas :format rgba8unorm
:usage [render-attachment copy-src])
(texture :name targetTexture :size canvas :format rgba8unorm
:usage [texture-binding copy-dst])
(queue :name copyResult
(copy-texture-to-texture
(source :texture sourceTexture)
(destination :texture targetTexture)))

Queue operations run when a (frame …) names them, in :perform alongside the passes:

(frame :name main :perform [writeUniforms renderPass])

or in :before, which runs its queues ahead of every frame’s passes:

(frame :name main :before [writeUniforms] :perform [renderPass])

A frame’s :init list is for one-shot compute steps: it takes (compute-pass …) and (init …) names only, so a queue named there is a not_cross_ref. A queue op runs every frame, which for an image upload is wasteful but correct.

Rule Error
A name is unique across every form kind duplicate_cross_ref_target
Referenced buffers, textures, query sets and data must exist not_cross_ref
write-buffer’s :data names a built-in, a (data …) or a (wasm-call …) union_no_branch_matched
write-buffer’s :offset, and copy-buffer-to-buffer’s offsets and :size, are multiples of 4 number_not_multiple
resolve-query-set’s :destination-offset is a multiple of 256 number_not_multiple
An action head the form does not accept not_head_member
Exactly one (source …) and one (destination …) per copy positional_missing / positional_too_many

Usage is the compiler’s rule rather than the schema’s, because a buffer’s usage is fixed when it is created. It reports on the (buffer …):

buffer 'src' is the :source of a copy but its :usage has no copy-src

Maps to queue operations:

// write-buffer (static data)
device.queue.writeBuffer(buffer, offset, data);
// write-buffer (pngine-inputs) - runtime provides data
device.queue.writeBuffer(buffer, offset,
new Float32Array([time, width, height, aspect]));
// copy-external-image-to-texture: (source …) and (destination …) are the
// two dictionaries this call already takes
device.queue.copyExternalImageToTexture(
{ source: imageBitmap },
{ texture: gpuTexture, mipLevel: 0, origin: [0, 0, 0] },
[width, height]
);