References
In the JavaScript API you wire WebGPU together by holding object references:
createRenderPipeline returns a value, and you hand that value to
setPipeline. A document has no values to hold, only names. So in PNGine
SJON (the S-expression source language) every resource declares a :name,
every slot that needs a resource takes a bare identifier, and the validator
resolves each identifier document-wide against the declared forms. A name
that doesn’t resolve to a declaration of the expected kind is a
not_cross_ref diagnostic.
The triangle from Getting Started is already a resolution chain:
(frame :name main) :perform [pass] | v(render-pass :name pass) --:pipeline--> (render-pipeline :name pipeline) :module | v (shader-module :name code)Three names, three slots, and each slot knows what kind of form its name must find. This page is the rulebook for that resolution: which slot admits which kinds, the built-in spellings that are not names at all, and the one-namespace rule that keeps every link in such a chain unambiguous (that rule gets its own section below).
Bare Identifier Syntax
Section titled “Bare Identifier Syntax”Reference resources by their bare name, with no # prefix and no quotes:
:pipeline mainPipeline:buffer vertexData:module shaderCodeThe slot determines which kind of declaration the name must resolve to. When you
write :module code, the validator requires a (shader-module …) named code.
Reference Slots
Section titled “Reference Slots”Every slot that resolves a name, and what it resolves to:
| Slot | Resolves to |
|---|---|
:module (pipeline stage) |
(shader-module …) |
:pipeline (render pass, render bundle) |
(render-pipeline …) |
:pipeline (compute pass) |
(compute-pipeline …) |
:layout (pipeline) |
auto, or a (pipeline-layout …) |
:layout (bind group) |
(render-pipeline …), (compute-pipeline …) or (bind-group-layout …) |
:bind-group-layouts |
(bind-group-layout …) |
:buffer, :vertex-buffers, :index-buffer |
(buffer …) |
:texture |
(texture …); a copy endpoint’s :texture also takes context-current-texture |
:texture-view |
(texture-view …) |
:view (colour attachment) |
context-current-texture or a (texture …) |
:view (depth-stencil attachment) |
(texture …) |
:sampler |
(sampler …) |
:data (buffer, image-bitmap) |
(data …) |
:data (write-buffer) |
a runtime source, a (data …), or a (wasm-call …) |
:image (image copy source) |
(image-bitmap …) |
:query-set, :occlusion-query-set |
(query-set …) |
:bind-groups |
(bind-group …) |
:execute-bundles |
(render-bundle …) |
:init |
(compute-pass …), or (init …), which lowers to one |
:before |
(queue …) |
:perform |
(render-pass …), (compute-pass …) or (queue …) |
numeric slots (:size, :offset, :vertex-count, …) |
(define …) |
A slot admits one target group, and every :name in the document lives in one
namespace, so a reference has exactly one possible match. Where a group spans
several kinds (a bind group’s :layout, a frame step), the one namespace is
what keeps the choice unambiguous.
Examples
Section titled “Examples”The same chain, slot family by slot family. Each example is the triangle’s shape with one kind of reference swapped into focus.
Pipeline References
Section titled “Pipeline References”The base chain, in full:
(shader-module :name code :code """ @vertex fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f { return vec4f(f32(i) - 1.0, f32(i & 1u) * 2.0 - 1.0, 0.0, 1.0); }
@fragment fn fs() -> @location(0) vec4f { return vec4f(1.0, 0.4, 0.1, 1.0); }""")
(render-pipeline :name mainPipeline :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 mainPipeline ; resolves to the (render-pipeline …) (draw :vertex-count 3))Shader Module References
Section titled “Shader Module References”Split the one module in two, and each stage names its own:
(shader-module :name vertexShader :code """ @vertex fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f { return vec4f(f32(i) - 1.0, f32(i & 1u) * 2.0 - 1.0, 0.0, 1.0); }""")
(shader-module :name fragmentCode :code """ @fragment fn fs() -> @location(0) vec4f { return vec4f(0.2, 0.7, 1.0, 1.0); }""")
(render-pipeline :name render :layout auto (vertex :module vertexShader :entry vs) ; (shader-module …) (fragment :module fragmentCode :entry fs ; (shader-module …) (target :format preferred-canvas-format)))A stage’s :entry is optional when the module holds exactly one entry point of
that kind: (vertex :module vertexShader) picks vs on its own. Zero or
several, and the compiler asks you to name one.
Buffer References
Section titled “Buffer References”Give the pass geometry to draw, and two more names enter the chain:
(buffer :name positions :size 1024 :usage [vertex])(buffer :name indices :size 256 :usage [index])
(render-pass :name draw (color-attachment :view context-current-texture :clear-value [0 0 0 1] :load-op clear :store-op store) :pipeline meshPipeline :vertex-buffers [positions] ; resolves to the (buffer …) :index-buffer indices ; resolves to the (buffer …) (draw-indexed :index-count 100))Bind Group References
Section titled “Bind Group References”A bind group states one :layout, naming either a pipeline (whose auto-derived
layout it uses, selected by :group) or an explicit (bind-group-layout …):
(buffer :name settings :size 16 :usage [uniform copy-dst])(texture :name noise :size [64 64] :format rgba8unorm :usage [texture-binding])
(bind-group :name uniforms :layout drawPipeline :group 0 (entry :binding 0 :buffer settings))(bind-group :name textures :layout drawPipeline :group 1 (entry :binding 0 :texture noise))
(render-pass :name render (color-attachment :view context-current-texture :clear-value [0 0 0 1] :load-op clear :store-op store) :pipeline drawPipeline :bind-groups [uniforms textures] ; each resolves to a (bind-group …) (draw :vertex-count 3))Pass References in Frame
Section titled “Pass References in Frame”At the top of the chain, the frame names everything it performs:
; declared elsewhere in the document:; (render-pass :name shadowPass …) (render-pass :name mainPass …); (compute-pass :name physics …) (queue :name writeUniforms …)
(frame :name game :before [writeUniforms] ; (queue …) :perform [physics shadowPass mainPass]) ; (compute-pass …), (render-pass …)A frame step is one name across three kinds, so the pass and queue names in
:perform are drawn from the same namespace as everything else.
Built-in Symbols
Section titled “Built-in Symbols”Some identifiers are built-in symbols rather than cross-references: the
validator accepts them as enum members or runtime sources, not as names of
declared forms. The triangle uses two of them (context-current-texture and
preferred-canvas-format) without declaring either.
Layout
Section titled “Layout”auto: auto-derive the bind-group layouts from the shaders, in a pipeline’s required:layoutkey
:layout autoTexture Sizes
Section titled “Texture Sizes”canvas: a texture that tracks the canvas size at runtime
:size canvasTexture Views
Section titled “Texture Views”context-current-texture: the current canvas texture
:view context-current-textureLoad / Store Operations
Section titled “Load / Store Operations”clear,load: load operationsstore,discard: store operations
:load-op clear:store-op storeColour Target / Texture Formats
Section titled “Colour Target / Texture Formats”preferred-canvas-format: the browser’s preferred format- WebGPU formats:
bgra8unorm,rgba8unorm,depth24plus, etc.
:format preferred-canvas-format:format rgba8unormPrimitive Topology
Section titled “Primitive Topology”triangle-list,triangle-strip,line-list,line-strip,point-list
:topology triangle-listCull Mode and Front Face
Section titled “Cull Mode and Front Face”none,front,back: cull modesccw,cw: front-face winding
:cull-mode back:front-face ccwFilter and Address Modes
Section titled “Filter and Address Modes”nearest,linear: texture filtersclamp-to-edge,repeat,mirror-repeat: address modes
:mag-filter linear:address-mode repeatRuntime Data Sources
Section titled “Runtime Data Sources”pngine-inputs(time and canvas size),scene-time-inputs,pointer-inputs: built-inwrite-bufferdata sources
(write-buffer :buffer uniforms :data pngine-inputs)One Namespace
Section titled “One Namespace”Now the rule promised at the top. Every :name in a document lives in one
namespace, whatever form kind declares it. Two buffers cannot share a name,
and neither can a buffer and a sampler:
; invalid: duplicate name 'noise'(buffer :name noise :size 256 :usage [storage])(sampler :name noise :mag-filter linear) ; ERROR: the name is takenThe second declaration is the one reported, by line, and the message states the rule itself:
duplicate name 'noise': already declared as a (buffer …) at line 2 —names are one namespace across every form, whatever kind declares themThis is not tidiness for its own sake. The one namespace is what lets a slot
admit several kinds at once (a bind group’s :layout, a frame step) without a
reference ever becoming ambiguous: whatever kind noise turns out to be,
there is exactly one noise.
The built-in spellings are reserved on the same principle: canvas,
context-current-texture, preferred-canvas-format, pngine-inputs,
scene-time-inputs, pointer-inputs, auto and the enum members cannot name
a form. An exercise: try it anyway. Declare
(buffer :name canvas :size 16 :usage [uniform]) in any document and run
pngine validate:
'canvas' is a builtin symbol and cannot name a (buffer …) — pnginereads it as the builtin wherever it appears, so this declaration cannever be referencedThe refusal is the design argument in one sentence: a form nothing could ever reference is a form that shouldn’t exist.
Validation Errors
Section titled “Validation Errors”| Error | Cause |
|---|---|
not_cross_ref |
No declaration of a kind the slot admits has that name (a misspelt (define …) in a numeric slot reports here too) |
duplicate_cross_ref_target |
Two forms of the same kind declare that name |
not_member |
A symbol is not a member of the slot’s enum |
missing_required_key |
A required :key is absent |
unknown_key |
The form has no such key |
positional_missing |
A required sub-form is absent (a compute pass with no (dispatch …)) |
required_one_of_missing |
An exclusive group has no alternative present (a bind-group (entry …) with no resource) |
dependent_key_missing |
A key that :requires another was written without it (:offset without :buffer) |
Two name rules are the compiler’s rather than the validator’s, so they carry a located message and no code: the duplicate-name and builtin-spelling refusals quoted in the One Namespace section above.
Related
Section titled “Related”- Expressions - Compile-time arithmetic
(define …)- Define constants- SJON language docs - Cross-reference resolution is a SJON feature