Skip to content

(bind-group-layout …)

Defines the layout of a bind group explicitly, specifying what resource can be bound at each binding index and which shader stages may access it. Use it instead of inferring the layout from a pipeline’s auto-layout.

Compose one or more of these into an explicit (pipeline-layout …) to bind @group(0), @group(1), … to distinct layouts, or name one directly as a (bind-group …)’s :layout.

(bind-group-layout :name layoutName
(entry :binding 0 :visibility [vertex fragment] (buffer :type uniform))
(entry :binding 1 :visibility [fragment] (sampler :type filtering))
(entry :binding 2 :visibility [fragment] (texture :sample-type float))
(entry :binding 3 :visibility [compute] (storage-texture :format rgba8unorm)))
Key / Sub-form Type Required Default Description
:name symbol Yes - Layout name
(entry …) sub-form No - Binding layout entries, one per slot

A layout with no (entry …) children is legal and describes an empty group, the same as WebGPU’s empty entries list.

Each (entry …) sub-form describes a single binding slot:

Key / Sub-form Type Required Description
:binding number Yes Binding index (0-based, up to 255)
:visibility array Yes Shader stages that can access
resource sub-form sub-form Yes Exactly one of (buffer …), (sampler …), (texture …), (storage-texture …)

Exactly one resource per entry, as GPUBindGroupLayoutEntry says: none is positional_missing and two is positional_too_many, both located, rather than a binding kind chosen for you.

Array of shader stages:

Value Description
vertex Vertex shader
fragment Fragment shader
compute Compute shader

Any other spelling is a not_member error naming the three that are allowed.

Buffer binding layout:

Key Type Required Description
:type symbol No (uniform) How the buffer is exposed to WGSL
Type WGSL
uniform var<uniform> (read-only, size-limited)
storage var<storage, read_write>
read-only-storage var<storage, read>
Key Type Default Description
:type symbol filtering filtering, non-filtering, or comparison

comparison corresponds to WGSL’s sampler_comparison; pair it with a (sampler … :compare …) for shadow mapping.

Sampled-texture binding layout:

Key Type Default Description
:sample-type symbol float float, unfilterable-float, depth, sint, uint
:view-dimension symbol 2d View dimension (see the table below)
:multisampled boolean false Whether the binding is a multisampled texture
Key Type Default Description
:format symbol required Texel format; WebGPU requires a storage-capable one, e.g. rgba8unorm
:access symbol write-only write-only, read-only, or read-write
:view-dimension symbol 2d View dimension (see the table below)

:view-dimension is spelled the way the WebGPU spec spells it, the same spellings (texture-view …) takes:

Value GPUTextureViewDimension
1d One-dimensional view (texture_1d)
2d Two-dimensional view (texture_2d), the default
2d-array Array of 2d layers (texture_2d_array)
cube Cube map, six 2d layers (texture_cube)
cube-array Array of cube maps (texture_cube_array)
3d Volume view (texture_3d)

The digit-leading spellings lex as numbers with a unit in SJON (the S-expression source format PNGine compiles), and the schema matches them on that parsed pair, so they are written bare, unquoted. A plain integer is a wrong_underlying error.

(bind-group-layout :name uniformLayout
(entry :binding 0 :visibility [vertex fragment] (buffer :type uniform)))
(bind-group-layout :name sceneLayout
(entry :binding 0 :visibility [vertex fragment] (buffer :type uniform))
(entry :binding 1 :visibility [fragment] (buffer :type uniform))
(entry :binding 2 :visibility [vertex fragment] (buffer :type uniform)))
(bind-group-layout :name computeLayout
(entry :binding 0 :visibility [compute] (buffer :type read-only-storage))
(entry :binding 1 :visibility [compute] (buffer :type storage)))
(bind-group-layout :name imageLayout
(entry :binding 0 :visibility [fragment] (sampler :type filtering))
(entry :binding 1 :visibility [fragment] (texture :sample-type float))
(entry :binding 2 :visibility [compute]
(storage-texture :format rgba8unorm :access write-only :view-dimension 2d)))
(bind-group-layout :name arrayLayout
(entry :binding 0 :visibility [fragment]
(texture :sample-type float :view-dimension 2d-array))
(entry :binding 1 :visibility [fragment]
(texture :sample-type float :view-dimension cube)))
(bind-group-layout :name shadowLayout
(entry :binding 0 :visibility [fragment] (sampler :type comparison))
(entry :binding 1 :visibility [fragment] (texture :sample-type depth)))
Rule Error
Name must be unique across every form kind duplicate_cross_ref_target
Each entry requires :binding and :visibility missing_required_key
Each entry must carry exactly one resource sub-form positional_missing / positional_too_many
(buffer …) requires :type missing_required_key
(storage-texture …) requires :format missing_required_key
:visibility takes only vertex, fragment, compute not_member
:view-dimension takes a spec spelling, not an integer wrong_underlying

A bind-group layout, a render pipeline and a compute pipeline share one cross-reference target group, because a (bind-group …) may name any of them, so a name collision between two of those kinds is a duplicate_cross_ref_target. Across unrelated kinds the compiler reports it instead, with a located message and no code: duplicate name 'x': already declared as a (buffer …) at line N.

The entry types must match the WGSL binding they front, or WebGPU rejects the pipeline; the native renderer reports that as a nonzero exit rather than a blank frame.

Maps to GPUBindGroupLayout via:

device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
buffer: { type: 'uniform' }
},
{
binding: 1,
visibility: GPUShaderStage.FRAGMENT,
texture: { sampleType: 'float', viewDimension: '2d' }
}
]
});

:visibility becomes the ORed GPUShaderStage mask (vertex 0x1, fragment 0x2, compute 0x4). Each resource sub-form’s enum values are the WebGPU spellings, passed through verbatim.