(shader-module …)
Defines a named shader module containing WGSL code. The source is opaque text to SJON (the S-expression source language); pipelines reference the module by name for their vertex, fragment, or compute stages.
Syntax
Section titled “Syntax”(shader-module :name name :code """ @fragment fn fs() -> @location(0) vec4f { return vec4f(1.0, 0.0, 0.0, 1.0); }""")| Key | Type | Required | Default | Description |
|---|---|---|---|---|
:name |
symbol | Yes | - | Unique module name |
:code |
string | Yes | - | Complete WGSL shader code |
Type: string
The complete WGSL shader source code including all entry points. Use a
triple-quoted string ("""…""") for multi-line WGSL; the body is preserved
verbatim.
(shader-module :name triangle :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.5, 0.0, 1.0); }""")Uniform Reflection
Section titled “Uniform Reflection”There is no (uniform …) form and no uniform-metadata key. Uniforms are
declared in the WGSL:
@group(G) @binding(B) var<uniform> NAME : Type;and the compiler reflects every var<uniform> binding into PNGB’s (the compiled
bytecode’s) uniform table, so the runtime can address a field by its WGSL name
without recompiling: setUniform(p, 'time', 0.25) from the
JavaScript API. Nested structs are flattened into
dot-notation paths at compile time, so a material.albedo field is addressed as
"material.albedo", and a fixed-size array element is addressed with a dotted
index, env.1.
A binding declared in the WGSL has to be bound in the document. A
(bind-group …) entry supplies the resource; leave it out and the compiler
reports binding ‘u’ (@group(0) @binding(0)) is used by the shader but never
bound in SJON.
(shader-module :name main :code """ struct Uniforms { time: f32, resolution: vec2f, } @group(0) @binding(0) var<uniform> u: Uniforms;
@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(sin(u.time), 0.0, 0.0, 1.0); }""")
(buffer :name uniformBuffer :size 16 :usage [uniform copy-dst])
(render-pipeline :name pipeline :layout auto (vertex :module main :entry vs) (fragment :module main :entry fs (target :format preferred-canvas-format)))
(bind-group :name bindings :layout pipeline :group 0 (entry :binding 0 :buffer uniformBuffer))Examples
Section titled “Examples”Basic Triangle
Section titled “Basic Triangle”(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)))A stage’s :entry may be omitted when the module holds exactly one entry point
of that kind: (vertex :module code) picks vs here. With none, or with
several, the compiler asks for a name.
With Uniform Buffer
Section titled “With Uniform Buffer”(shader-module :name mesh :code """ struct Inputs { mvp: mat4x4f, time: f32, } @group(0) @binding(0) var<uniform> inputs: Inputs;
@vertex fn vs(@location(0) pos: vec3f) -> @builtin(position) vec4f { return inputs.mvp * vec4f(pos, 1.0); }
@fragment fn fs() -> @location(0) vec4f { return vec4f(sin(inputs.time), 0.0, 0.0, 1.0); }""")
(buffer :name uniformBuffer :size 80 :usage [uniform copy-dst])
(render-pipeline :name meshPipeline :layout auto (vertex :module mesh :entry vs (vertex-buffer :array-stride 12 (attribute :shader-location 0 :offset 0 :format float32x3))) (fragment :module mesh :entry fs (target :format preferred-canvas-format)))
(bind-group :name meshBindings :layout meshPipeline :group 0 (entry :binding 0 :buffer uniformBuffer))
(queue :name writeUniforms (write-buffer :buffer uniformBuffer :offset 64 :data pngine-inputs))pngine-inputs is the built-in 16-byte runtime source (time, canvas width,
canvas height, aspect), written here after the 64-byte matrix.
Compute Shader
Section titled “Compute Shader”(shader-module :name particleStep :code """ struct Particle { pos: vec2f, vel: vec2f, }
@group(0) @binding(0) var<storage, read> particlesA: array<Particle>; @group(0) @binding(1) var<storage, read_write> particlesB: array<Particle>;
@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) id: vec3u) { let i = id.x; particlesB[i].pos = particlesA[i].pos + particlesA[i].vel; particlesB[i].vel = particlesA[i].vel; }""")
(buffer :name particlesA :size 16384 :usage [storage])(buffer :name particlesB :size 16384 :usage [storage])
(compute-pipeline :name step :layout auto (compute :module particleStep :entry main))
(bind-group :name particleBindings :layout step :group 0 (entry :binding 0 :buffer particlesA) (entry :binding 1 :buffer particlesB))Validation Rules
Section titled “Validation Rules”| Rule | Error |
|---|---|
Two (shader-module …) forms may not share a name |
duplicate_cross_ref_target |
| A name already declared by another form kind is a clash: names are one namespace | duplicate name 'x': already declared as a (buffer …) at line L |
:code key is required |
missing_required_key |
| The WGSL must compile | located WGSL diagnostic (compile and render skip it with --no-validate) |
Every binding the WGSL declares must be bound by a (bind-group …) entry |
binding 'u' (@group(0) @binding(0)) is used by the shader but never bound in SJON |
A named :entry must exist in the module |
entry point 'nope' not found in shader module 'main' |
An omitted :entry needs exactly one entry point of that stage kind |
shader module 'm' declares 2 fragment entry points, so pngine cannot infer which one this stage uses |
WebGPU Mapping
Section titled “WebGPU Mapping”Compiles to GPUShaderModule via:
device.createShaderModule({ code: shaderCode});Related
Section titled “Related”(render-pipeline …)- Uses modules for vertex/fragment(compute-pipeline …)- Uses modules for compute(queue …)- Write uniform data to buffers