Skip to content

(compute-pipeline …)

Creates a compute pipeline for general-purpose GPU computation.

(compute-pipeline :name pipelineName :layout auto
(compute :module shaderName :entry main))

A compute pipeline states its stage the way a (render-pipeline …) states its vertex and fragment stages: one positional (compute …) child, the same GPUProgrammableStage shape, naming the module and its @compute entry point.

Key / Sub-form Type Required Default Description
:name symbol Yes - Pipeline name
:layout auto or reference Yes - auto, or a declared (pipeline-layout …)
(compute …) sub-form Yes - Compute stage (exactly one)

:layout is one required key, mirroring GPUPipelineDescriptorBase.layout, which is one required member. auto derives the bind-group layouts from the WGSL; the alternative is the name of an explicit (pipeline-layout …). A value that is neither is a union_no_branch_matched validation error, and no form may be named auto, which is a builtin spelling.

Key / Sub-form Type Required Description
:module reference Yes The (shader-module …) holding the @compute entry
:entry symbol No Entry function name; defaults to the module’s sole @compute fn
(constant …) sub-form No WGSL override values for this stage

:module is resolved as a cross-reference. :entry is optional: a module with exactly one @compute function is used without naming it, while zero or several is a located compile error (shader module 'code' declares 2 compute entry points, so pngine cannot infer which one this stage uses).

A (constant …) child specialises a WGSL override with a compile-time value, taking a required :name (the override identifier) and :value (a literal, an expression over (define …) constants, or a bare define name). Specialising @workgroup_size(WG) this way is worth doing: with WG resolved to a number, the dispatch-grid and device-limit checks run instead of being skipped.

(shader-module :name computeShader :code """
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) id: vec3u) {}
""")
(compute-pipeline :name simPipeline :layout auto
(compute :module computeShader :entry main))
(shader-module :name computeShader :code """
@group(0) @binding(0) var<storage, read_write> data: array<f32>;
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) id: vec3u) {
data[id.x] = data[id.x] * 2.0;
}
""")
(buffer :name data :size 4096 :usage [storage copy-src])
(compute-pipeline :name doublePipeline :layout auto
(compute :module computeShader :entry main))
(bind-group :name dataGroup :layout doublePipeline :group 0
(entry :binding 0 :buffer data))
(compute-pass :name double
:pipeline doublePipeline
:bind-groups [dataGroup]
(dispatch :workgroups [16 1 1]))
(shader-module :name particleSim :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>;
@group(0) @binding(2) var<uniform> params: vec4f;
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) id: vec3u) {
let i = id.x;
let dt = params.x;
var p = particlesA[i];
p.pos = p.pos + p.vel * dt;
// Wrap around
p.pos = fract(p.pos);
particlesB[i] = p;
}
""")
(buffer :name particlesA :size 65536 :usage [storage])
(buffer :name particlesB :size 65536 :usage [storage])
(buffer :name params :size 16 :usage [uniform copy-dst])
(compute-pipeline :name simPipeline :layout auto
(compute :module particleSim :entry main))
(bind-group :name simGroup :layout simPipeline :group 0
(entry :binding 0 :buffer particlesA)
(entry :binding 1 :buffer particlesB)
(entry :binding 2 :buffer params))

Every buffer binding the WGSL declares must be bound by a (bind-group …); one that is not is a located compile error naming the @group/@binding pair.

(shader-module :name blur :code """
@group(0) @binding(0) var inputTex: texture_2d<f32>;
@group(0) @binding(1) var outputTex: texture_storage_2d<rgba8unorm, write>;
@compute @workgroup_size(8, 8)
fn main(@builtin(global_invocation_id) id: vec3u) {
let dims = textureDimensions(inputTex);
if (id.x >= dims.x || id.y >= dims.y) { return; }
var sum = vec4f(0);
for (var dy: i32 = -1; dy <= 1; dy++) {
for (var dx: i32 = -1; dx <= 1; dx++) {
let coord = vec2i(id.xy) + vec2i(dx, dy);
sum += textureLoad(inputTex, clamp(coord, vec2i(0), vec2i(dims) - 1), 0);
}
}
textureStore(outputTex, id.xy, sum / 9.0);
}
""")
(compute-pipeline :name blurPipeline :layout auto
(compute :module blur :entry main))

Name a (pipeline-layout …) as :layout to give the pipeline an explicit layout composed of (bind-group-layout …) forms, rather than auto-deriving it from the shader. Bind groups then take their per-group layout from the pipeline (:layout simPipeline :group N) or from a bind-group layout directly (:layout bgl).

(bind-group-layout :name bgl
(entry :binding 0 :visibility [compute] (buffer :type read-only-storage))
(entry :binding 1 :visibility [compute] (buffer :type storage)))
(pipeline-layout :name pl :bind-group-layouts [bgl])
(compute-pipeline :name simPipeline
:layout pl
(compute :module particleSim :entry main))

The shader defines workgroup size with @workgroup_size(x, y, z):

@compute @workgroup_size(64) // 1D: 64 threads
@compute @workgroup_size(8, 8) // 2D: 8x8 = 64 threads
@compute @workgroup_size(4, 4, 4) // 3D: 4x4x4 = 64 threads

How many workgroups run is a pass-level command: (compute-pass …) takes a (dispatch :workgroups [x y z]) child, or a (dispatch-indirect :buffer b :offset o) one when the counts live in a buffer. A compute pass with no command at all is a positional_missing error.

Rule Error
:layout is required missing_required_key
:layout must be auto or a declared (pipeline-layout …) union_no_branch_matched
Exactly one (compute …) sub-form positional_missing / positional_too_many
(compute …) requires :module missing_required_key
Referenced module must exist not_cross_ref
Two pipelines may not share a name duplicate_cross_ref_target

:module and :entry are keys of the (compute …) child, not of the pipeline: writing them on the form itself is unknown_key. A name is one namespace across every form kind, so a pipeline reusing a buffer’s or texture’s name is a compiler diagnostic with no code: duplicate name 'x': already declared as a (buffer …) at line N. An :entry naming a function the module does not declare is a compiler diagnostic too (entry point 'nope' not found in shader module 'code').

Maps to GPUComputePipeline via:

device.createComputePipeline({
layout: 'auto',
compute: {
module: shaderModule,
entryPoint: 'main'
}
});