Invocations
In WebGPU, a shader does not run as a single monolithic program. Instead, it is executed thousands or millions of times in parallel. Each individual execution of a shader entry point is called an invocation.
The way invocations are spawned, scheduled, and grouped depends entirely on the shader stage (Vertex, Fragment, or Compute). Understanding these execution models is key to writing high-performance, race-free parallel programs.
Vertex Shader Invocations
In a vertex shader, each vertex in the input stream (defined by your vertex buffers or index buffers) triggers exactly one invocation.
- Purpose: Transform 3D coordinates, calculate vertex normals, and pass per-vertex attributes to the rasterizer.
- Key Built-ins:
@builtin(vertex_index): The 0-based index of the vertex being processed.@builtin(instance_index): The index of the active instance when using instanced rendering.
Example: Generating vertex coordinates procedurally
Example
@vertex
fn main(@builtin(vertex_index) vertexIndex: u32) -> @builtin(position) vec4<f32> {
// Generate a fullscreen triangle using only the vertex index!
var pos = vec2<f32>(0.0, 0.0);
if (vertexIndex == 0u) {
pos = vec2<f32>(-1.0, -1.0);
} else if (vertexIndex == 1u) {
pos = vec2<f32>(3.0, -1.0);
} else {
pos = vec2<f32>(-1.0, 3.0);
}
return vec4<f32>(pos, 0.0, 1.0);
}
Fragment Shader Invocations
After the vertex stage, WebGPU's hardware rasterizer determines which screen pixels (or sub-pixels) are covered by the rendered primitives. Each covered area triggers one fragment invocation.
- Purpose: Determine the color, depth, and visibility of a single screen pixel.
- Key Built-ins:
@builtin(frag_coord): Avec4<f32>representing the \((x, y, z, 1/w)\) screen-space coordinates of the fragment.@builtin(front_facing): Aboolindicating if the fragment is part of a front-facing or back-facing polygon.
Example: Color mapping based on screen-space coordinates
Example
Compute Shader Invocations
Compute shaders run arbitrary, non-graphics tasks. Invocations are defined explicitly on a 3D grid and grouped into blocks called workgroups.
- Purpose: General-purpose parallel data processing (GPGPU), physics simulation, skinning, or pre-processing textures.
- Key Built-ins:
@builtin(global_invocation_id): The absolute 3D coordinate of this invocation within the entire dispatch grid.@builtin(local_invocation_id): The 3D coordinate of this invocation relative only to its enclosing workgroup.@builtin(workgroup_id): The 3D coordinate of the active workgroup within the dispatch grid.
Example: Running thread-specific work within a workgroup
Example
@group(0) @binding(0) var<storage, read_write> outputBuffer: array<u32>;
@compute @workgroup_size(64, 1, 1)
fn main(
@builtin(global_invocation_id) global_id: vec3<u32>,
@builtin(local_invocation_id) local_id: vec3<u32>
) {
let index = global_id.x;
// Each invocation performs computations on its specific index in the buffer
outputBuffer[index] = index * local_id.x;
}
Control Flow Uniformity & Divergence
Because thousands of invocations run the same shader code concurrently, their execution paths can split and merge. This behavior is described by Control Flow Uniformity.
Uniform Control Flow
An execution path is in uniform control flow when all active invocations in a given group (such as a \(2 \times 2\) pixel quad, a workgroup, or a subgroup) are guaranteed to execute the exact same instruction at the exact same time.
At the start of any shader entry point (vs_main, fs_main, cs_main), execution is always in uniform control flow.
Divergent (Non-Uniform) Control Flow
If a conditional statement (like if or switch) or a loop condition depends on a value that varies across different invocations, the group splits. Some invocations take the branch, while others skip it or take the else path. This is called execution divergence (or non-uniform control flow).
Uniform Control Flow
[ I0 I1 I2 I3 ]
│
if (global_id.x > 1u) <-- Divergent Condition
/ \
/ \
(Branch Taken) (Branch Skipped)
[ I2 I3 ] [ I0 I1 ]
│ │
execute_code() idle_wait()
│ │
\ /
\ /
Reconverge / Merge
[ I0 I1 I2 I3 ]
│
Uniform Control Flow
Once a group of invocations diverges, they remain in divergent control flow until their execution paths reconverge (typically after the closing brace of the conditional block).
Uniform vs. Divergent Conditions
The uniformity of control flow is determined entirely by the expressions used in branch conditions.
Uniform Values and Conditions
A value is uniform if it is guaranteed to be identical for all active invocations in a draw or dispatch call. Branching on a uniform value preserves uniform control flow:
- Shader Constants: Constants declared with
constorletvariables derived from uniform sources. - Pipeline Overrides: Values declared with
override. - Uniform Buffers: Variables residing in the
uniformaddress space (var<uniform>). - Literals: Concrete values like
42u,3.14f, ortrue.
// Uniform condition: all invocations check the same buffer value
if (myUniformBuffer.scale > 1.0) {
// Inside this block, execution remains in UNIFORM control flow!
}
Divergent (Non-Uniform) Values and Conditions
A value is divergent if it can differ between different invocations. Branching on a divergent value causes execution divergence:
- Stage Inputs: Attributes like
@location(N)or built-ins like@builtin(vertex_index),@builtin(frag_coord), or@builtin(global_invocation_id). - Storage Buffers: Data loaded from the
storageaddress space (var<storage>). - Textures: Pixel values loaded or sampled from textures.
- Derived Variables: Any variable calculated using a divergent value.
// Divergent condition: depends on the specific thread coordinate
if (global_id.x % 2u == 0u) {
// Inside this block, execution is in DIVERGENT control flow!
// Calling uniform-requiring operations (like barriers or derivatives) here is forbidden.
}
Built-in Variable Mapping Reference
The following table lists common built-in input and output variables. Because their values vary per invocation, using them in any control-flow condition causes execution divergence:
| Shader Stage | Built-in Attribute | Type | Explanation |
|---|---|---|---|
| Vertex | @builtin(vertex_index) |
u32 |
0-based index of current vertex. |
| Vertex | @builtin(instance_index) |
u32 |
0-based index of current draw instance. |
| Fragment | @builtin(frag_coord) |
vec4<f32> |
Position of the fragment in window coordinates \((x, y, z, 1/w)\). |
| Fragment | @builtin(front_facing) |
bool |
True if the fragment belongs to a front-facing primitive. |
| Compute | @builtin(local_invocation_id) |
vec3<u32> |
Coordinate of the invocation within its workgroup. |
| Compute | @builtin(global_invocation_id) |
vec3<u32> |
Coordinate of the invocation in the overall dispatch grid. |
| Compute | @builtin(workgroup_id) |
vec3<u32> |
Index of the active workgroup being executed. |
| Compute | @builtin(local_invocation_index) |
u32 |
Flattened 1D index of the invocation within its workgroup. |
| Any Stage | @builtin(position) |
vec4<f32> |
In vertex: output clip-space coordinates. In fragment: input window-space coordinates. |