Skip to main content

Compute Shaders Advanced

For most compute shaders, the simplified syntax described on the Compute Shaders page is all you need — declare resources in WGSL and the engine reflects them automatically.

This page covers manual control over bind groups, which remains fully supported. You provide a computeBindGroupFormat (and optionally computeUniformBufferFormats) describing the resources yourself, and write explicit @group/@binding indices in the WGSL. Reach for this when you need:

  • precise control over the binding layout,
  • to interop with engine- or application-owned buffers at fixed bindings, or
  • to mix explicitly-bound resources with reflected ones.
note

Manual bind groups are not deprecated. If you have existing compute shaders that hand-author a computeBindGroupFormat, they continue to work unchanged — you can migrate them to the simplified syntax whenever convenient, or not at all.

Bind Group Format​

The computeBindGroupFormat defines what resources are available to the compute shader. When you supply it, write explicit @group/@binding indices in the WGSL to match. You can bind various types of resources:

const shader = new pc.Shader(device, {
name: 'MyComputeShader',
shaderLanguage: pc.SHADERLANGUAGE_WGSL,
cshader: shaderCode,
computeBindGroupFormat: new pc.BindGroupFormat(device, [
// Resource bindings go here
])
});

Storage Buffers​

Storage buffers allow read/write access to large amounts of data:

// Read-write storage buffer
new pc.BindStorageBufferFormat('particles', pc.SHADERSTAGE_COMPUTE)

// Read-only storage buffer
new pc.BindStorageBufferFormat('spheres', pc.SHADERSTAGE_COMPUTE, true)

In WGSL, access storage buffers like this:

@group(0) @binding(0) var<storage, read_write> particles: array<f32>;
@group(0) @binding(1) var<storage, read> spheres: array<vec4f>;

Storage Textures​

Storage textures allow the compute shader to write directly to a texture:

new pc.BindStorageTextureFormat('outTexture', pc.PIXELFORMAT_RGBA8, pc.TEXTUREDIMENSION_2D)

In WGSL:

@group(0) @binding(0) var outputTexture: texture_storage_2d<rgba8unorm, write>;

// Writing to the texture
textureStore(outputTexture, vec2i(global_id.xy), color);

Input Textures​

Input textures provide read-only texture data. The last parameter controls whether a sampler is included:

// Texture without sampler (for textureLoad)
new pc.BindTextureFormat('inputTexture', pc.SHADERSTAGE_COMPUTE, undefined, undefined, false)

// Texture with sampler (for textureSampleLevel)
new pc.BindTextureFormat('inputTexture', pc.SHADERSTAGE_COMPUTE, undefined, undefined, true)

In WGSL, when a sampler is included, it uses the texture name with a _sampler suffix:

// Without sampler - use textureLoad for direct texel access
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
let color = textureLoad(inputTexture, position, 0);

// With sampler - use textureSampleLevel for filtered sampling
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
@group(0) @binding(1) var inputTexture_sampler: sampler;
let color = textureSampleLevel(inputTexture, inputTexture_sampler, uv, 0.0);
note

In compute shaders, use textureSampleLevel instead of textureSample because you must explicitly specify the mip level (LOD).

Uniform Buffers​

For passing uniform data to compute shaders, first define the uniform buffer format:

const uniformBufferFormat = new pc.UniformBufferFormat(device, [
new pc.UniformFormat('tint', pc.UNIFORMTYPE_VEC4),
new pc.UniformFormat('time', pc.UNIFORMTYPE_FLOAT),
new pc.UniformFormat('count', pc.UNIFORMTYPE_UINT)
]);

Then include it in the shader definition along with the bind group:

const shader = new pc.Shader(device, {
name: 'ComputeShader',
shaderLanguage: pc.SHADERLANGUAGE_WGSL,
cshader: shaderCode,

// Assign the uniform buffer format
computeUniformBufferFormats: {
ub: uniformBufferFormat
},

// Include uniform buffer in bind group
computeBindGroupFormat: new pc.BindGroupFormat(device, [
new pc.BindUniformBufferFormat('ub', pc.SHADERSTAGE_COMPUTE),
// ... other bindings
])
});

In WGSL:

struct ub_compute {
tint: vec4f,
time: f32,
count: u32
}

@group(0) @binding(0) var<uniform> ubCompute: ub_compute;

@compute @workgroup_size(1, 1, 1)
fn main(@builtin(global_invocation_id) global_id: vec3u) {
let t = ubCompute.time;
let c = ubCompute.count;
}

Mixing Reflected and Manual Resources​

A caller-provided computeBindGroupFormat and the simplified (reflected) syntax can be used together in the same shader:

  • Resources you describe in computeBindGroupFormat use the bindings you give them, in group 0.
  • Any resources you declare with the simplified syntax (no @group/@binding) are still reflected — they are placed in a separate bind group at index 1.
  • Explicitly-bound (@group/@binding) declarations in the WGSL are left untouched.

If you do not provide a computeBindGroupFormat, reflected resources are placed in group 0 instead (see Compute Shaders).

In all cases, values are assigned by name with setParameter, regardless of whether a resource is manually bound or reflected.

Examples​

The following examples use manually-authored bind group formats:

Histogram
Texture Generation
Indirect Dispatch