(frame …)
Defines a frame that orchestrates the execution of passes and queue operations.
Syntax
Section titled “Syntax”(frame :name name :init [initStep] :before [writeUniforms] :perform [pass1 pass2 pass3])| Key | Type | Required | Default | Description |
|---|---|---|---|---|
:name |
symbol | Yes | - | Unique frame name |
:perform |
array | Yes | - | Passes and queues run every frame, in order |
:init |
array | No | [] |
One-shot compute steps: (compute-pass …) and (init …) names |
:before |
array | No | [] |
(queue …) ops run before each frame’s passes |
All three lists are capped at 2048 entries. The number is the 64 KB command buffer’s: even the cheapest step that emits anything costs about 18 bytes of it, so a longer list would describe a frame the runtime cannot execute.
:perform
Section titled “:perform”Type: array of references
The main sequence of steps to execute each frame. Order matters: they run left to right, and repeating a name repeats the work, which is how a multi-stage algorithm names the same pass several times.
:perform [writeUniforms computePass renderPass]Can reference:
(render-pass …)- Render passes(compute-pass …)- Compute passes(queue …)- Queue operations (buffer writes)
The three form kinds share one name namespace, so a step is never ambiguous: a
render pass and a queue that both claim update collide at the second
declaration with a duplicate_cross_ref_target.
Type: array of references
One-shot compute steps, for work like seeding a particle buffer or filling a
heightmap. It takes (compute-pass …) and (init …) names only, so a
(queue …) named here is a not_cross_ref; a queue that has to run belongs in
:before or :perform.
:init [resetParticles seedHeightmap]Each entry lowers to the exec_pass_once opcode, which runs once per loaded
payload, keyed by the pass. Not once per frame, not once per scene: load the
same PNG twice and each load runs it once, while seeking, pausing, restarting
the animation or handing the runtime a saved frame counter does not re-arm it.
Only a fresh load does.
Three consequences follow:
- A repeated entry is refused.
:init [spawn spawn]asks for two runs and can only get one, so the compiler rejects it rather than emitting an op that can never fire.:performis the opposite: repetition there is meaning. - Two frames may share one
:initpass. It runs once in total, in whichever frame is rendered first. For per-scene setup, give each scene its own pass. - A pass in both
:initand:performwarns. Both lists are honoured as written, so it runs twice on the first frame and once per frame after.
:before
Section titled “:before”Type: array of references
(queue …) operations run before each frame’s passes, typically the ones that
update uniforms. This list takes queue names only.
:before [updateSimParams]The (init …) Compute-Init Sugar
Section titled “The (init …) Compute-Init Sugar”(init …) is a sugar form for one-shot compute initialisation of a storage
buffer, run once per loaded payload. It is referenced from a frame’s :init
array. The
pngine/init-v1 lowering hook (the compiler step that turns sugar into ordinary
forms) expands it into a (compute-pipeline …), a (bind-group …) (with one
(entry …) entry), and a (compute-pass …).
(define :name NUM_PARTICLES :value 2048)
(init :name initParticles :buffer particleBuffers :module initShader :workgroups [(ceil (/ NUM_PARTICLES 64))])| Key | Type | Required | Description |
|---|---|---|---|
:name |
symbol | Yes | Unique name (referenced from :init) |
:buffer |
reference | Yes | Storage buffer to initialise, bound at @binding 0 |
:module |
reference | Yes | (shader-module …) holding the @compute init entry |
:workgroups |
array | Yes | Workgroup counts as [x], [x y] or [x y z] |
:workgroups has the same shape as the (dispatch :workgroups …) it lowers
to: a vector of one to three elements, each a literal, a bare (define …)
constant, or a bounded expression over those constants. It is read while the
hook lowers, before the emitter runs, but against the same document-wide
constant environment, so both slots behave the same. A name in it that no
(define …) declares is a located error on the (init …), not a silent zero.
Examples
Section titled “Examples”Simple Frame
Section titled “Simple Frame”(frame :name main :perform [renderPass])With Uniform Updates
Section titled “With Uniform Updates”Include (queue …) operations in :perform to update uniforms each frame:
(buffer :name uniforms :size 16 :usage [uniform copy-dst])
(queue :name writeTime (write-buffer :buffer uniforms :offset 0 :data pngine-inputs))
(render-pass :name render (color-attachment :view context-current-texture :clear-value [0 0 0 1] :load-op clear :store-op store) :pipeline mainPipeline :bind-groups [uniformGroup] (draw :vertex-count 3))
(frame :name main :perform [writeTime render])With Initialization
Section titled “With Initialization”Use :init with an (init …) step for one-time compute setup that runs before
the first frame:
(init :name resetParticles :buffer particles :module initShader :workgroups [32])
(compute-pass :name updateParticles :pipeline simPipeline :bind-groups [simGroup] (dispatch :workgroups [64]))
(render-pass :name drawParticles (color-attachment :view context-current-texture :clear-value [0 0 0 1] :load-op clear :store-op store) :pipeline renderPipeline (draw :vertex-count 2048))
(frame :name particles :init [resetParticles] :perform [updateParticles drawParticles])Multi-Pass Rendering
Section titled “Multi-Pass Rendering”(frame :name deferred :perform [geometryPass lightingPass postProcess])Compute + Render
Section titled “Compute + Render”(frame :name simulation :init [initBuffers] :before [writeUniforms] :perform [physicsCompute renderParticles])Execution Order
Section titled “Execution Order”Each frame execution follows this order:
:initsteps (once per loaded payload, viaexec_pass_once):beforequeues (every frame, before the main passes):performpasses and queues (every frame, viaexec_pass)
Ping-Pong Pattern
Section titled “Ping-Pong Pattern”For double-buffered compute simulations:
(buffer :name particles :size 32768 :usage [vertex storage] :pool 2)
(compute-pass :name simulate :pipeline simPipeline :bind-groups [simGroup] :bind-groups-pool-offsets [0] (dispatch :workgroups [64]))
(render-pass :name draw (color-attachment :view context-current-texture :clear-value [0 0 0 1] :load-op clear :store-op store) :pipeline renderPipeline :vertex-buffers [particles] :vertex-buffers-pool-offsets [0] (draw :vertex-count 2048))
(frame :name boids :perform [simulate draw])The runtime automatically alternates pool offsets each frame.
Validation Rules
Section titled “Validation Rules”| Rule | Error |
|---|---|
| A name is unique across every form kind | duplicate_cross_ref_target |
:perform required |
missing_required_key |
| Referenced passes and queues must exist | not_cross_ref |
:init names a (compute-pass …) or (init …), never a queue |
not_cross_ref |
:before names a (queue …) |
not_cross_ref |
| A list longer than 2048 entries | vector_too_long |
Two more are the compiler’s, and carry a located message with no code:
| Rule | Message shape |
|---|---|
:init names the same pass twice |
frame 'main': ':init' names 'setup' more than once, but a one-shot pass runs once per loaded payload |
A pass in both :init and :perform (a warning, not an error) |
frame 'main': 'setup' runs as a one-shot :init AND every frame via :perform, so it runs twice on the first frame |
WebGPU Mapping
Section titled “WebGPU Mapping”Each frame creates a command encoder and submits to queue:
const encoder = device.createCommandEncoder();
// Execute init steps (once per loaded payload, keyed by pass id)if (!hasRunOnce) { for (const step of frame.init) { executePass(step, encoder); }}
// Execute perform passes (every frame)for (const pass of frame.perform) { executePass(pass, encoder);}
device.queue.submit([encoder.finish()]);Related
Section titled “Related”(render-pass …)- Render passes(compute-pass …)- Compute passes(queue …)- Buffer writes with pngine-inputs