(render-pipeline …)
Creates a render pipeline for vertex and fragment shader execution.
Syntax
Section titled “Syntax”(render-pipeline :name pipelineName :layout auto (vertex :module shaderName :entry vs (vertex-buffer :array-stride 12 (attribute :shader-location 0 :offset 0 :format float32x3))) (fragment :module shaderName :entry fs (target :format preferred-canvas-format)) (primitive :topology triangle-list :cull-mode back :front-face ccw) (depth-stencil :format depth24plus :depth-write-enabled true :depth-compare less) (multisample :count 4))Keys & Sub-forms
Section titled “Keys & Sub-forms”| Key / Sub-form | Type | Required | Default | Description |
|---|---|---|---|---|
:name |
symbol | Yes | - | Pipeline name |
:layout |
auto or reference |
Yes | - | auto, or a declared (pipeline-layout …) |
(vertex …) |
sub-form | Yes | - | Vertex stage (exactly one) |
(fragment …) |
sub-form | No | - | Fragment stage (at most one) |
(primitive …) |
sub-form | No | - | Primitive assembly (at most one) |
(depth-stencil …) |
sub-form | No | - | Depth/stencil state (at most one) |
(multisample …) |
sub-form | No | - | MSAA state (at most one) |
The five sub-forms are positional children, in any order. A second one of the
same kind is positional_too_many; a missing (vertex …) is
positional_missing.
:layout
Section titled “:layout”Type: auto, or the name of a (pipeline-layout …)
:layout is one required key, mirroring GPUPipelineDescriptorBase.layout,
which is one required member. auto derives the bind-group layouts from the
WGSL, which is what most documents want:
(shader-module :name code :code """ @vertex fn vs() -> @builtin(position) vec4f { return vec4f(0, 0, 0, 1); } @fragment fn fs() -> @location(0) vec4f { return vec4f(1, 0, 0, 1); }""")
(render-pipeline :name pipe :layout auto (vertex :module code :entry vs) (fragment :module code :entry fs (target :format bgra8unorm)))For an explicit layout (binding @group(0), @group(1), … to distinct
(bind-group-layout …) forms),
declare a (pipeline-layout …) and
name it here instead of auto:
(bind-group-layout :name bgl (entry :binding 0 :visibility [fragment] (buffer :type uniform)))
(pipeline-layout :name pl :bind-group-layouts [bgl])
(render-pipeline :name pipe :layout pl (vertex :module mod :entry vs) (fragment :module mod :entry fs (target :format bgra8unorm)))auto is a builtin spelling, so no form can be named auto. A :layout value
that is neither auto nor a declared (pipeline-layout …) is a
union_no_branch_matched validation error, naming a
(bind-group-layout …) here
included.
(vertex …)
Section titled “(vertex …)”Vertex shader stage, mirroring GPUVertexState.
| Key / Sub-form | Type | Required | Description |
|---|---|---|---|
:module |
reference | Yes | The (shader-module …) holding the @vertex entry |
:entry |
symbol | No | Entry function name; defaults to the module’s sole @vertex fn |
(vertex-buffer …) |
sub-form | No | One per bound vertex slot, in slot order |
(constant …) |
sub-form | No | WGSL override values for this stage |
:entry is optional: a module with exactly one @vertex function is used
without naming it. Zero or several is a located compile error (shader module 'code' declares 2 vertex entry points, so pngine cannot infer which one this stage uses), as is naming a function the module does not declare.
(vertex-buffer …)
Section titled “(vertex-buffer …)”Each (vertex-buffer …) is one bound vertex slot, holding its (attribute …)
children:
| Key / Sub-form | Type | Required | Description |
|---|---|---|---|
:array-stride |
number | Yes | Bytes between elements; multiple of 4 |
:step-mode |
symbol | No | vertex (default) or instance |
(attribute …) |
sub-form | No | Attribute definitions |
Attribute sub-form:
| Key | Type | Required | Description |
|---|---|---|---|
:shader-location |
number | Yes | @location(N) |
:offset |
number | Yes | Byte offset in vertex |
:format |
symbol | Yes | Vertex format |
Common vertex formats:
| Format | Description |
|---|---|
float32 |
Single float |
float32x2 |
vec2f |
float32x3 |
vec3f |
float32x4 |
vec4f |
unorm8x4 |
4 normalized u8 |
The full GPUVertexFormat set is accepted: uint8/sint8/unorm8/snorm8
and their 16-bit counterparts in scalar, x2 and x4 widths, float16/
float16x2/float16x4, the 32-bit float32/uint32/sint32 in scalar,
x2, x3 and x4 widths, plus the packed unorm10-10-10-2 and
unorm8x4-bgra.
(vertex :module shader :entry vs (vertex-buffer :array-stride 24 (attribute :shader-location 0 :offset 0 :format float32x3) (attribute :shader-location 1 :offset 12 :format float32x3)))Ordering is positional: the first (vertex-buffer …) is slot 0 and pairs with
the first entry of the render pass’s :vertex-buffers [...] list. Omit the
(vertex-buffer …) children entirely for pipelines that generate their vertices
from @builtin(vertex_index).
(fragment …)
Section titled “(fragment …)”Fragment shader stage, mirroring GPUFragmentState.
| Key / Sub-form | Type | Required | Description |
|---|---|---|---|
:module |
reference | Yes | The (shader-module …) holding the @fragment entry |
:entry |
symbol | No | Entry function name; defaults to the module’s sole @fragment fn |
(target …) |
sub-form | Yes | Color target states; at least one |
(constant …) |
sub-form | No | WGSL override values for this stage |
A fragment stage with no (target …) is positional_missing. WebGPU allows an
empty target list, for a depth-only stage; PNGine’s two backends do not build
the same pipeline from one, so the floor is stated here instead.
(target …)
Section titled “(target …)”One color target:
| Key / Sub-form | Type | Required | Default | Description |
|---|---|---|---|---|
:format |
symbol | Yes | - | Format of the attachment this target renders to |
:write-mask |
number or all |
No | 15 |
Channel write mask |
(blend …) |
sub-form | No | - | Blend state (at most one) |
:format takes preferred-canvas-format (resolved at runtime to the
platform’s canvas format) or one of the 42 colour-renderable
GPUTextureFormat spellings the schema lists, such as bgra8unorm,
rgba8unorm, rgba16float or r32uint; a depth format or an unknown
spelling is a not_member diagnostic.
:write-mask is either an integer bitmask (R=1 G=2 B=4 A=8, so 5 writes red
and blue, and hex works: 0xF) or the symbol all (15, the WebGPU default).
:write-mask 0 writes no colour at all, the idiom for a depth-only or occlusion
pre-pass that still needs a fragment stage. all is the only symbol it accepts;
a (define …) constant is written (* MASK 1) here.
Blend sub-form holds positional (color …) and (alpha …) components, both
required, each taking an optional :src-factor (WebGPU default one),
:dst-factor (default zero) and :operation (default add):
(blend (color :src-factor src-alpha :dst-factor one-minus-src-alpha :operation add) (alpha :src-factor one :dst-factor one-minus-src-alpha :operation add))Blend factors: zero, one, src, one-minus-src, src-alpha,
one-minus-src-alpha, dst, one-minus-dst, dst-alpha,
one-minus-dst-alpha, src-alpha-saturated, constant, one-minus-constant.
The constant factors multiply by the render pass’s
:blend-constant.
Four more (src1, one-minus-src1, src1-alpha, one-minus-src1-alpha)
reference the fragment shader’s second colour output (@blend_src(1)) and
need the dual-source-blending device feature.
Operations: add, subtract, reverse-subtract, min, max (the :operation key is optional; min/max ignore the factors)
A (blend …) on an integer target format is a located compile error: WebGPU
blends only blendable formats, and no uint or sint format is one.
(constant …)
Section titled “(constant …)”Specialises a WGSL override with a compile-time value. The declaration becomes
a const in the shipped shader, so the value participates in const-folding and
dead-code elimination.
| Key | Type | Required | Description |
|---|---|---|---|
:name |
symbol | Yes | The WGSL override identifier |
:value |
number | Yes | Literal, expression over (define …) constants, or a bare define name |
(shader-module :name code :code """ override SCALE: f32; @vertex fn vs() -> @builtin(position) vec4f { return vec4f(0, 0, 0, 1); } @fragment fn fs() -> @location(0) vec4f { return vec4f(SCALE, 0, 0, 1); }""")
(render-pipeline :name pipe :layout auto (vertex :module code :entry vs) (fragment :module code :entry fs (target :format bgra8unorm) (constant :name SCALE :value 0.5)))Constants are written per stage but resolved per module: the name must be a real
override in that stage’s module, the value must fit the override’s declared
WGSL type, and two stages specialising one module’s override differently is an
error.
(primitive …)
Section titled “(primitive …)”Primitive assembly configuration, mirroring GPUPrimitiveState. Omit the whole
sub-form when the source authors no primitive state; the compiler’s emitter only
writes what you declare.
| Key | Type | Default | Description |
|---|---|---|---|
:topology |
symbol | triangle-list |
Primitive topology |
:front-face |
symbol | ccw |
Front face winding |
:cull-mode |
symbol | none |
Culling mode |
:strip-index-format |
symbol | - | uint16 / uint32: index width for indexed strip draws; legal only under a strip topology |
:unclipped-depth |
boolean | false |
Clamp fragment depth to [0,1] instead of clipping (needs depth-clip-control) |
:strip-index-format is checked both ways. The key exists only under
:topology triangle-strip or :topology line-strip: the schema declares it in a
variant on those two values, so writing it under a list topology, or beside no
:topology at all, is an unknown_key validation error whose message adds that
:topology must be set before variant-only keys. The other direction is the
compiler’s, because it crosses
pipeline and pass: a (draw-indexed …)
in a pass whose pipeline is a strip without it is an error too.
:unclipped-depth disables depth clipping and relies on the
depth-clip-control device feature, which the runtime enables opportunistically
when the adapter supports it.
Topologies:
| Value | Description |
|---|---|
point-list |
Individual points |
line-list |
Separate lines |
line-strip |
Connected lines |
triangle-list |
Separate triangles |
triangle-strip |
Connected triangles |
Cull modes: none, front, back
Front face: ccw (counter-clockwise), cw (clockwise)
(depth-stencil …)
Section titled “(depth-stencil …)”Depth and stencil testing configuration, mirroring GPUDepthStencilState. This
is pipeline state; the texture it tests against is pass state, declared
by (depth-stencil-attachment …).
| Key / Sub-form | Type | Required | Default | Description |
|---|---|---|---|---|
:format |
symbol | Yes | - | Depth/stencil attachment format this pipeline renders to |
:depth-write-enabled |
boolean | Conditional | - | Write to the depth buffer |
:depth-compare |
symbol | Conditional | - | Depth test function |
:depth-bias |
number | No | 0 |
Constant depth bias (signed integer) |
:depth-bias-slope-scale |
number | No | 0 |
Slope-scaled bias |
:depth-bias-clamp |
number | No | 0 |
Bias clamp (0 = unclamped) |
:stencil-read-mask |
number | No | 0xFFFFFFFF |
Mask ANDed with stencil values for tests |
:stencil-write-mask |
number | No | 0xFFFFFFFF |
Mask ANDed with stencil writes |
(stencil-front …) |
sub-form | No | - | Front stencil state (at most one) |
(stencil-back …) |
sub-form | No | - | Back stencil state (at most one) |
:format must name a depth or stencil format, never a colour one, and it must
carry every aspect the pipeline uses. Four rules read the format’s value, so
they are the compiler’s rather than the schema’s, and each is reported where you
wrote it:
:depth-write-enabledis required once:formathas a depth aspect. WebGPU gives it no default, so an omitted one used to mean the compiler picked.:depth-compareis required once the pipeline writes depth, and also once a stencil face states a:depth-fail-opother thankeep, since that op consults the depth test.- A stencil face state on a format with no stencil aspect (
depth24plus, say) is an error, as is a colour format anywhere in:format. :depth-bias,:depth-bias-slope-scaleand:depth-bias-clampmust be0under a point or line topology: depth bias is derived from a triangle’s depth slope.
Compare functions: never, less, equal, less-equal, greater, not-equal, greater-equal, always
(stencil-front …) and (stencil-back …)
Section titled “(stencil-front …) and (stencil-back …)”Per-face stencil state. The two faces are independent: an unauthored face takes
the spec default (compare always, all ops keep), not a copy of the other
one.
| Key | Type | Required | Default | Description |
|---|---|---|---|---|
:compare |
symbol | No | always |
Stencil reference (masked) vs stored value |
:fail-op |
symbol | No | keep |
Applied when the stencil test fails |
:depth-fail-op |
symbol | No | keep |
Applied when stencil passes but depth fails |
:pass-op |
symbol | No | keep |
Applied when both tests pass |
Stencil operations: keep, zero, replace, invert, increment-clamp,
decrement-clamp, increment-wrap, decrement-wrap. The reference value that
compare and replace use is pass state,
:stencil-reference.
(multisample …)
Section titled “(multisample …)”MSAA state, mirroring GPUMultisampleState.
| Key | Type | Required | Default | Description |
|---|---|---|---|---|
:count |
number | No | 1 |
Samples per pixel (1 or 4) |
:mask |
number | No | 0xFFFFFFFF |
Sample coverage bitmask |
:alpha-to-coverage-enabled |
boolean | No | false |
Derive coverage from fragment alpha |
:count must equal the :sample-count of every attachment texture the pipeline
renders to; a mismatch is a located compile error naming both. A texture’s
:sample-count is 1 or 4 and nothing between, so those are the two counts a
pipeline can carry.
:alpha-to-coverage-enabled requires :count above 1, a (fragment …) stage,
and a first (target …) whose format has an alpha channel to derive the mask
from.
(define :name SAMPLE_COUNT :value 4)
(shader-module :name code :code """ @vertex fn vs() -> @builtin(position) vec4f { return vec4f(0, 0, 0, 1); } @fragment fn fs() -> @location(0) vec4f { return vec4f(1, 0, 0, 1); }""")
(render-pipeline :name msaaPipeline :layout auto (vertex :module code :entry vs) (fragment :module code :entry fs (target :format preferred-canvas-format)) (multisample :count SAMPLE_COUNT))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.5), vec2f(-0.5, -0.5), vec2f(0.5, -0.5)); return vec4f(pos[i], 0, 1); } @fragment fn fs() -> @location(0) vec4f { return vec4f(1, 0, 0, 1); }""")
(render-pipeline :name triangle :layout auto (vertex :module code :entry vs) (fragment :module code :entry fs (target :format preferred-canvas-format)))With Vertex Buffers
Section titled “With Vertex Buffers”(render-pipeline :name mesh :layout auto (vertex :module shader :entry vs (vertex-buffer :array-stride 32 (attribute :shader-location 0 :offset 0 :format float32x3) (attribute :shader-location 1 :offset 12 :format float32x3) (attribute :shader-location 2 :offset 24 :format float32x2))) (fragment :module shader :entry fs (target :format preferred-canvas-format)) (primitive :topology triangle-list :cull-mode back :front-face ccw) (depth-stencil :format depth24plus :depth-write-enabled true :depth-compare less))Instanced Rendering
Section titled “Instanced Rendering”(render-pipeline :name instanced :layout auto (vertex :module shader :entry vs (vertex-buffer :array-stride 12 :step-mode vertex (attribute :shader-location 0 :offset 0 :format float32x3)) (vertex-buffer :array-stride 16 :step-mode instance (attribute :shader-location 1 :offset 0 :format float32x4))))Alpha Blending
Section titled “Alpha Blending”(render-pipeline :name transparent :layout auto (vertex :module shader :entry vs) (fragment :module shader :entry fs (target :format preferred-canvas-format (blend (color :src-factor src-alpha :dst-factor one-minus-src-alpha :operation add) (alpha :src-factor one :dst-factor one-minus-src-alpha :operation add)))))Validation Rules
Section titled “Validation Rules”| Rule | Error |
|---|---|
:layout is required |
missing_required_key |
:layout must be auto or a declared (pipeline-layout …) |
union_no_branch_matched |
Exactly one (vertex …) sub-form |
positional_missing / positional_too_many |
A (fragment …) stage needs at least one (target …) |
positional_missing |
A (blend …) needs both (color …) and (alpha …) |
positional_missing |
:strip-index-format only under a strip topology |
unknown_key |
:array-stride must be a multiple of 4 |
number_not_multiple |
:write-mask takes an integer 0-15 or all |
number_above_max for 16 and up; not_member for any other symbol |
| Two pipelines may not share a name | duplicate_cross_ref_target |
| Referenced modules must exist | not_cross_ref |
A name is one namespace across every form kind, so a pipeline that reuses a
buffer’s or texture’s name is a compiler diagnostic instead: duplicate name 'x': already declared as a (buffer …) at line N. The other compiler-level
rules are the value-reading ones listed above under (depth-stencil …),
(multisample …), (target …) and (constant …); they carry a located message
and no code.
WebGPU Mapping
Section titled “WebGPU Mapping”Maps to GPURenderPipeline via:
device.createRenderPipeline({ layout: 'auto', vertex: { module: shaderModule, entryPoint: 'vs', buffers: [...] }, fragment: { module: shaderModule, entryPoint: 'fs', targets: [...] }, primitive: { topology: 'triangle-list', cullMode: 'back' }, depthStencil: { format: 'depth24plus', depthWriteEnabled: true, depthCompare: 'less' }, multisample: { count: 4 }});Related
Section titled “Related”(shader-module …)- Shader modules(render-pass …)- Use pipeline in passes(buffer …)- Vertex buffers(pipeline-layout …)- Explicit layout named by:layout