A GPU runs one instruction stream on many data items at once: the GTX 1650 that renders this chapter's figures has 896 shader cores. Your shader runs as thousands of invocations, one per vertex or fragment, in lockstep batches: 32 (a warp) on NVIDIA hardware, 32 or 64 (a wavefront) on AMD. Three consequences follow:
Invocations cannot talk to each other; shared data arrives as uniforms or textures.
Divergent branches cost both sides: if a warp disagrees on an if, it runs both paths and masks results.
Fragments run in 2x2 quads so the GPU can take derivatives (dFdx, dFdy) and pick mipmap levels; a one-pixel triangle still pays for four invocations.
The load is lopsided: a quad filling a 960x400 canvas runs the vertex shader 6 times and the fragment shader 384,000 times: move work out of the fragment shader whenever you can.
<!doctype html>
<style>
body { margin: 0; font: 12px system-ui, sans-serif; background: #f7f4ee; color: #333; }
canvas { display: block; width: 100%; max-width: 600px; image-rendering: pixelated; }
.key { max-width: 600px; margin-top: 6px; display: flex; gap: 14px; flex-wrap: wrap; }
.key span::before { content: ''; display: inline-block; width: 12px; height: 12px;
margin-right: 5px; vertical-align: -2px; background: var(--c); }
</style>
<canvas id="view" width="150" height="90"></canvas>
<div class="key">
<span style="--c:#1f5f8b">warp takes the if-branch</span>
<span style="--c:#e6dcc8">warp takes the else-branch</span>
<span style="--c:#e0782a">divergent warp: runs both paths</span>
<span id="count"></span>
</div>
<script>
// A deliberately tiny canvas (150x90 = 13,500 fragments) scaled up, so warps are visible.
const canvas = document.getElementById('view');
const gl = canvas.getContext('webgl2');
const WARP_W = 8, WARP_H = 4; // 8x4 = 32 invocations, one NVIDIA warp
const vs = `#version 300 es
void main() { // runs 3 times: one big triangle covers the canvas
vec2 p = vec2(gl_VertexID == 1 ? 3.0 : -1.0, gl_VertexID == 2 ? 3.0 : -1.0);
gl_Position = vec4(p, 0.0, 1.0);
}`;
const fs = `#version 300 es
precision highp float;
uniform vec2 uCenter; uniform float uRadius;
out vec4 outColor;
bool inside(vec2 p) { return distance(p, uCenter) < uRadius; }
void main() {
// Every invocation evaluates the same branch condition for its own pixel...
bool mine = inside(gl_FragCoord.xy);
// ...but lockstep hardware runs a whole 8x4 block together. Check the block's extremes:
vec2 lo = floor(gl_FragCoord.xy / vec2(${WARP_W}.0, ${WARP_H}.0)) * vec2(${WARP_W}.0, ${WARP_H}.0);
vec2 hi = lo + vec2(${WARP_W}.0, ${WARP_H}.0);
vec2 nearest = clamp(uCenter, lo, hi);
float far = length(max(abs(uCenter - lo), abs(uCenter - hi)));
bool anyInside = distance(nearest, uCenter) < uRadius;
bool allInside = far < uRadius;
vec3 color = allInside ? vec3(0.12, 0.37, 0.55)
: anyInside ? vec3(0.88, 0.47, 0.16)
: vec3(0.90, 0.86, 0.78);
if (anyInside && !allInside && mine) color *= 0.8; // pixels inside a divergent warp
// grid lines on warp borders
vec2 cell = mod(gl_FragCoord.xy - 0.5, vec2(${WARP_W}.0, ${WARP_H}.0));
if (cell.x < 0.5 || cell.y < 0.5) color *= 0.93;
outColor = vec4(color, 1.0);
}`;
const program = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, vs], [gl.FRAGMENT_SHADER, fs]]) {
const s = gl.createShader(type);
gl.shaderSource(s, src); gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s));
gl.attachShader(program, s);
}
gl.linkProgram(program);
gl.useProgram(program);
gl.bindVertexArray(gl.createVertexArray()); // no attributes needed: gl_VertexID builds the triangle
const uCenter = gl.getUniformLocation(program, 'uCenter');
gl.uniform1f(gl.getUniformLocation(program, 'uRadius'), 30);
const warps = (canvas.width / WARP_W | 0) * Math.ceil(canvas.height / WARP_H);
function frame(ms) {
const t = ms * 0.0006;
const cx = 75 + Math.cos(t) * 35, cy = 45 + Math.sin(t * 1.3) * 12;
gl.uniform2f(uCenter, cx, cy);
gl.drawArrays(gl.TRIANGLES, 0, 3); // 3 vertex invocations, 13,500 fragment invocations
requestAnimationFrame(frame);
}
document.getElementById('count').textContent =
`${canvas.width * canvas.height} fragment invocations in about ${warps} warps, 3 vertex invocations`;
requestAnimationFrame(frame);
</script>