The first 3D accelerators were fixed-function: OpenGL 1.0 (1992) exposed built-in transform, lighting and texturing stages as switches and parameters, and you could not change the math. Around 2001, DirectX 8 hardware made the vertex and pixel stages programmable, and OpenGL 2.0 (2004) added GLSL, the language WebGL still uses. Once shaders were ordinary programs on thousands of small cores, NVIDIA's CUDA (2007) and then OpenCL opened the GPU to general-purpose computing.
The second shift came when Metal (2014), Direct3D 12 (2015) and Vulkan (2016) replaced OpenGL's global context with explicit APIs: pipeline state objects built ahead of time, command buffers recorded on any thread, and resources bound in groups. WebGL (2011) stayed on the older model; WebGPU brings the explicit model, with compute, to the web.

WebGPU treats the GPU as a parallel processor that also rasterizes triangles, so this chapter spends as much time on compute (Compute and Workgroups-Particles and Culling) as on drawing.
<!doctype html>
<style>
body { margin: 0; background: #f7f4ee; font: 14px system-ui, sans-serif; }
.stage { position: relative; width: 100%; max-width: 600px; }
.stage canvas { display: block; width: 100%; }
.stage canvas + canvas { position: absolute; inset: 0; pointer-events: none; }
</style>
<div class="stage">
<canvas id="view" width="600" height="360"></canvas>
<canvas id="labels" width="600" height="360"></canvas>
</div>
<script>
const canvas = document.getElementById('view');
const ink = document.getElementById('labels').getContext('2d');
function showMessage(text) { // 2D fallback when WebGPU is missing
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#fbeaea'; ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#8a2b2b'; ctx.font = '18px system-ui, sans-serif'; ctx.textAlign = 'center';
ctx.fillText(text, canvas.width / 2, canvas.height / 2);
}
function label(text, x, y, size = 12, color = '#2b2b2b', align = 'center') {
ink.font = `${size}px system-ui, sans-serif`; ink.fillStyle = color; ink.textAlign = align;
ink.fillText(text, x, y);
}
const BLUE = [0.12, 0.40, 0.72], GREEN = [0.16, 0.56, 0.30];
// year, name, label height above the axis, text alignment
const native = [[1992, 'OpenGL 1.0', 40], [2001, 'DirectX 8 shaders', 70], [2004, 'OpenGL 2.0 + GLSL', 100],
[2007, 'CUDA', 40], [2009, 'OpenCL', 70], [2014, 'Metal', 40, 'right'], [2015, 'Direct3D 12', 75, 'right'], [2016, 'Vulkan', 110]];
const web = [[2011, 'WebGL 1.0'], [2017, 'WebGL 2'], [2023, 'WebGPU in Chrome', 'right'], [2024, 'WebGPU CR']];
const eras = [[1990, 2001, 'Fixed-function', [0.93, 0.90, 0.84]], [2001, 2007, 'Programmable', [0.90, 0.92, 0.88]],
[2007, 2014, 'GPGPU compute', [0.88, 0.91, 0.94]], [2014, 2027, 'Explicit APIs', [0.92, 0.89, 0.94]]];
const x = year => 20 + (year - 1990) / 37 * 560;
const AXIS = 190;
async function main() {
const adapter = await navigator.gpu?.requestAdapter();
if (!adapter) return showMessage('WebGPU is not available in this browser');
const device = await adapter.requestDevice();
const context = canvas.getContext('webgpu');
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ device, format });
// Every shape is a rounded box: x, y, w, h, r, g, b, corner radius (pixels).
const boxes = [];
for (const [a, b, name, c] of eras) { boxes.push([x(a), 40, x(b) - x(a), 300, ...c, 0]); label(name, (x(a) + x(b)) / 2, 58, 12, '#555'); }
boxes.push([x(1990), AXIS - 2, x(2027) - x(1990), 4, 0.3, 0.3, 0.3, 2]);
native.forEach(([year, name, rise, align = 'center']) => {
const top = AXIS - rise;
boxes.push([x(year) - 1, top, 2, rise, ...BLUE, 1], [x(year) - 7, AXIS - 7, 14, 14, ...BLUE, 7]);
label(name, x(year) + (align === 'right' ? 4 : 0), top - 6, 11, '#1f4f8a', align);
});
web.forEach(([year, name, align = 'center'], i) => {
const bottom = AXIS + 45 + (i % 2) * 35;
boxes.push([x(year) - 1, AXIS, 2, bottom - AXIS, ...GREEN, 1], [x(year) - 7, AXIS - 7, 14, 14, ...GREEN, 7]);
label(name, x(year) - (align === 'right' ? 4 : 0), bottom + (align === 'right' ? 0 : 14), 11, '#1e6b3a', align);
});
for (let year = 1990; year <= 2025; year += 5) label(String(year), x(year), 330, 11, '#666');
const module = device.createShaderModule({ code: `
struct Box { rect: vec4f, style: vec4f } // style: r, g, b, corner radius
@group(0) @binding(0) var<storage> boxes: array<Box>;
struct Out { @builtin(position) pos: vec4f, @location(0) local: vec2f,
@location(1) @interpolate(flat) i: u32 }
@vertex fn vs(@builtin(vertex_index) v: u32, @builtin(instance_index) i: u32) -> Out {
let corner = vec2f(f32(v & 1), f32(v >> 1)); // triangle-strip corners
let r = boxes[i].rect;
let px = r.xy + corner * r.zw;
return Out(vec4f(px.x / 300 - 1, 1 - px.y / 180, 0, 1), corner * r.zw, i);
}
@fragment fn fs(in: Out) -> @location(0) vec4f {
let b = boxes[in.i];
let half = b.rect.zw / 2;
let q = abs(in.local - half) - half + b.style.w; // signed distance to a rounded box
let d = length(max(q, vec2f(0))) + min(max(q.x, q.y), 0) - b.style.w;
let a = clamp(0.5 - d, 0, 1);
return vec4f(b.style.rgb * a, a); // premultiplied alpha
}` });
const blend = { srcFactor: 'one', dstFactor: 'one-minus-src-alpha' };
const pipeline = device.createRenderPipeline({ layout: 'auto',
vertex: { module }, primitive: { topology: 'triangle-strip' },
fragment: { module, targets: [{ format, blend: { color: blend, alpha: blend } }] } });
const data = new Float32Array(boxes.flat());
const buffer = device.createBuffer({ size: data.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
device.queue.writeBuffer(buffer, 0, data);
const group = device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer } }] });
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({ colorAttachments: [{ view: context.getCurrentTexture().createView(),
clearValue: [0.97, 0.96, 0.93, 1], loadOp: 'clear', storeOp: 'store' }] });
pass.setPipeline(pipeline);
pass.setBindGroup(0, group);
pass.draw(4, boxes.length); // one instanced strip per box
pass.end();
device.queue.submit([encoder.finish()]);
label('native APIs', 26, 90, 12, '#1f4f8a', 'left');
label('web APIs', 26, 300, 12, '#1e6b3a', 'left');
}
main();
</script>