How Shaders Execute

How the GPU Executes Vertex and Fragment Shaders

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:

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.

Fragments grouped into 32-invocation warps, with warps that diverge on a branch highlightedHTMLLive
<!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>