Skip to content

SJON Syntax

PNGine source is SJON: an S-expression language in which every WebGPU resource is a form, validated against the WebGPU schema in schema/pngine.sjon, the single source of truth for what a .sjon document may contain. SJON owns the syntax, validation, cross-reference resolution, expressions, and sugar lowering (the expansion of shorthand forms into ordinary ones); PNGine owns the schema, the pngine/* lowering hooks, and bytecode emission.

This page is a tour of what a .sjon PNGine document looks like. For the language underneath (its reader syntax, value kinds, and how a host defines a schema), see the SJON documentation. For complete documentation of each PNGine form, see the SJON Reference.

A .sjon file is a sequence of S-expression forms. Each form is a head symbol followed by keyword keys (:key value) and positional sub-forms:

(form-name :key value :flag true
(sub-form positional :key value)
[array of items])
; comments start with a semicolon
Type Example Description
String "WGSL code" or """multi-line""" Quoted / triple-quoted text
Number 123, 0.5 Decimal or float
Boolean true, false Boolean values
Symbol myName, triangle-list Bare identifier (enum member or cross-ref)
Array [a b c] Space-separated list
Sub-form (vertex :module code :entry vs) Positional nested form
Expression (* NUM 16), (ceil (/ NUM 64)) Compile-time arithmetic

Shaders

Form Purpose Details
(shader-module …) Named WGSL module Full docs

Resources

Form Purpose Details
(buffer …) GPU buffer (vertex, uniform, storage, index) Full docs
(texture …) GPU texture Full docs
(texture-view …) Explicit view over a texture Full docs
(sampler …) Texture sampler Full docs
(image-bitmap …) Decoded image → texture Full docs
(data …) Embedded data and shape generators Full docs
(query-set …) Occlusion / timestamp queries Full docs

Pipelines

Form Purpose Details
(render-pipeline …) Render pipeline Full docs
(compute-pipeline …) Compute pipeline Full docs
(bind-group …) Resource bindings Full docs
(bind-group-layout …) Explicit bind group layout Full docs
(pipeline-layout …) Explicit pipeline layout Full docs

Execution

Form Purpose Details
(render-pass …) Render pass with draw commands Full docs
(compute-pass …) Compute pass with dispatch Full docs
(render-bundle …) Pre-recorded, replayable draws Full docs
(queue …) Buffer writes, copies, query resolves Full docs
(frame …) Frame execution order Full docs

Advanced

Form Purpose Details
(init …) One-shot compute init (sugar) Full docs
(pass-graph …) Fullscreen shader-art sugar Full docs
(wasm-call …) WASM function called every frame Full docs
(define …) Compile-time constants Full docs
(limits …) Raise WebGPU device limits Full docs
(canvas …) Canvas alpha mode Full docs
(shader-module :name code :code """
@vertex fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
var pos = array<vec2f, 3>(
vec2f(0.0, 0.5),
vec2f(-0.5, -0.5),
vec2f(0.5, -0.5)
);
return vec4f(pos[i], 0.0, 1.0);
}
@fragment fn fs() -> @location(0) vec4f {
return vec4f(1.0, 0.0, 0.0, 1.0);
}
""")
(render-pipeline :name pipeline
:layout auto
(vertex :module code :entry vs)
(fragment :module code :entry fs
(target :format preferred-canvas-format)))
(render-pass :name pass
(color-attachment :view context-current-texture
:clear-value [0 0 0 1] :load-op clear :store-op store)
:pipeline pipeline
(draw :vertex-count 3))
(frame :name main :perform [pass])

Resources reference each other using bare identifiers. SJON’s validator resolves them document-wide against the declared forms (a dangling name is a not_cross_ref diagnostic):

:pipeline myPipeline ; resolves to a (render-pipeline …) or (compute-pipeline …)
:module shader ; resolves to a (shader-module …)
:buffer vertices ; resolves to a (buffer …)

Every :name lives in one namespace across every form kind: a buffer and a texture cannot both be called shared, and the compiler reports the second declaration. The built-in spellings (canvas, context-current-texture, preferred-canvas-format, pngine-inputs) are reserved and cannot be declared.

See References for details.

Every numeric slot accepts a literal, a bounded compile-time expression over (define …) constants, or a bare constant name:

(define :name NUM_PARTICLES :value 2048)
(buffer :name particles :size (* NUM_PARTICLES 16) :usage [storage])
(compute-pass :name step :pipeline simPipeline
(dispatch :workgroups [(ceil (/ NUM_PARTICLES 64))]))

The usual functions are +, -, *, /, ceil, floor and fract; the full set (mod, pow, min, max, clamp, sqrt, abs, round, the trigonometric functions, comparisons and if) is on the expressions page.

A bare name works wherever a number does, so :size NUM_PARTICLES and :instance-count NUM_PARTICLES are both values, not cross-references to another form. A misspelt one is a not_cross_ref diagnostic on that slot.

See Expressions for details.

Generate vertex data at compile time with a positional shape sub-form inside (data …):

(data :name cubeVertices (cube :format [position4 color4 uv2]))
(data :name planeVertices (plane :format [position3 uv2]))

Format specifiers:

  • position3, position4 - Vertex position (vec3f or vec4f)
  • normal3 - Surface normal (vec3f)
  • color3, color4 - Vertex color (vec3f or vec4f)
  • uv2 - Texture coordinates (vec2f)

See (data …) for details.

Write runtime data (time, canvas size) to a uniform buffer using the pngine-inputs source in a write-buffer:

(buffer :name uniforms :size 16 :usage [uniform copy-dst])
(queue :name writeTime
(write-buffer :buffer uniforms :offset 0 :data pngine-inputs))

pngine-inputs provides 16 bytes. Two more built-in sources exist: scene-time-inputs (12 bytes: time, width, height) and pointer-inputs (48 bytes of pointer state).

pngine-inputs fields:

Field Type Description
time f32 Elapsed seconds
width f32 Canvas width
height f32 Canvas height
aspect f32 width / height

See (queue …) for details.

Run a pass once per loaded payload (not once per frame) using :init:

(frame :name main
:init [setupCompute] ; runs once per loaded payload
:perform [updateCompute renderPass]) ; runs every frame

Each :init entry names a compute pass: usually an (init …) form, the sugar that expands into a compute pipeline, a bind group and a compute pass, but any (compute-pass …) works there. Loading the same PNG again re-arms it; seeking, pausing or restarting the animation does not.

See (frame …) for details.

For compute simulations requiring double-buffering:

(buffer :name particles :size 32768 :usage [vertex storage] :pool 2)

With :pool 2, the runtime creates two buffer instances and alternates between them each frame. See (buffer …) for details.