(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 initialization 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 initialize, 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”A simulation step that reads its neighbours cannot safely write over the generation it is still reading, so it keeps two buffers and alternates:
(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()]);Where these words come from
Section titled “Where these words come from”Every key and value on this page traced to the WebGPU name it stands for, with a link to the definition. How the tracing is made, and what keeps it from rotting, is the subject of Where the Words Come From.
(frame …) is PNGine’s own. WebGPU has no frame: a page calls requestAnimationFrame and encodes what it likes. The frame form is the schedule the runtime replays, and none of its keys is a WebGPU word.
| Key | WebGPU | Note |
|---|---|---|
:name |
PNGine’s own | the name the runtime plays |
:init |
PNGine’s own | compute passes run once, before the first frame |
:before |
PNGine’s own | queues run at the top of every frame |
:perform |
PNGine’s own | the passes, in order, every frame |
(init …) is PNGine’s own. Sugar, lowered by the pngine/init-v1 hook into a (compute-pipeline …), a (bind-group …) and a one-shot (compute-pass …); the words are the ones those forms carry.
| Key | WebGPU | Note |
|---|---|---|
:name |
PNGine’s own | the name (frame :init [...]) refers to |
:buffer |
PNGine’s own | the storage buffer the module fills, bound at @binding(0) |
:module |
PNGine’s own | the (shader-module …) with the compute entry point |
:workgroups |
GPUComputePassEncoder.dispatchWorkgroups() |
the three counts of the lowered dispatch |
Checked against the WebGPU specification at revision b8c0fa9; the links go to the current draft.
Related
Section titled “Related”(render-pass …)- Render passes(compute-pass …)- Compute passes(queue …)- Buffer writes with pngine-inputs