The two APIs coexist on one page, but not on one canvas: a canvas keeps its first context for life (WebGL Interop). Decide at startup, WebGPU first with WebGL 2 behind it:
async function createRenderer(canvas) {
const adapter = await navigator.gpu?.requestAdapter();
if (adapter) { // WebGPU first (Chapter 4)
const device = await adapter.requestDevice(), context = canvas.getContext('webgpu');
context.configure({ device, format: navigator.gpu.getPreferredCanvasFormat() });
const { vendor, architecture } = adapter.info;
return { api: `WebGPU (${vendor}, ${architecture})`, device, context };
}
const gl = canvas.getContext('webgl2'); // a canvas keeps its first context
if (!gl) throw new Error('No GPU API: show a static image');
const info = gl.getExtension('WEBGL_debug_renderer_info');
return { api: `WebGL 2 (${gl.getParameter(info.UNMASKED_RENDERER_WEBGL)})`, gl };
}
console.log((await createRenderer(document.createElement('canvas'))).api);WebGPU (nvidia, turing)
Launched with --disable-gpu or with SwiftShader 2,546 forced, the same page logged "No available adapters." and chose WebGL 2 (ANGLE 2,592 (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero) (0x0000C0DE)), SwiftShader driver)): WebGL degrades to the CPU, WebGPU disappears. From here, WebGPU offers three migration paths. Port a module at a time with A WebGL-to-WebGPU Concept Map's concept map and 4.19.5's GLSL-to-WGSL rules; keep a legacy WebGL renderer on an OffscreenCanvas and copy its frames into WebGPU (4.19.7); or move up to a library with both back ends (4.20). Everything this chapter taught, from matrices and lighting to shadow maps, deferred shading and picking, carries over unchanged in concept: only the API around the shaders differs.
<!doctype html>
<style>
body { margin: 0; font: 11px system-ui, sans-serif; background: #f7f4ee; color: #333; }
.row { display: flex; gap: 8px; max-width: 600px; padding: 6px; box-sizing: border-box; }
.row div { flex: 1; }
canvas { display: block; width: 100%; aspect-ratio: 1; background: #ece8e0; }
p { margin: 4px 0; font: 11px monospace; min-height: 3em; }
</style>
<div class="row">
<div><canvas id="a" width="290" height="290"></canvas><p id="pa">choosing...</p></div>
<div><canvas id="b" width="290" height="290"></canvas><p id="pb"></p></div>
</div>
<script>
// Six covers as quads, positions in clip space and one colour each: the same data for both APIs.
const covers = [[0.12, 0.37, 0.55], [0.36, 0.25, 0.6], [0.88, 0.6, 0.06], [0.25, 0.49, 0.23], [0.71, 0.27, 0.18], [0.16, 0.62, 0.56]];
const data = [];
covers.forEach((c, i) => {
const x = -0.78 + (i % 3) * 0.54, y = i < 3 ? 0.05 : -0.8, w = 0.46, h = 0.72;
for (const [u, v] of [[0, 0], [1, 0], [1, 1], [0, 0], [1, 1], [0, 1]]) data.push(x + u * w, y + v * h, ...c.map(k => k * (0.8 + 0.2 * v)));
});
const vertices = new Float32Array(data); // x, y, r, g, b
async function createRenderer(canvas) {
const adapter = await navigator.gpu?.requestAdapter();
if (adapter) { // WebGPU first (Chapter 4)
const device = await adapter.requestDevice(), context = canvas.getContext('webgpu');
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ device, format, alphaMode: 'opaque' });
const { vendor = '', architecture = '' } = adapter.info ?? {};
return { api: `WebGPU (${vendor}, ${architecture})`, draw: () => drawWebGPU(device, context, format) };
}
const gl = canvas.getContext('webgl2'); // a canvas keeps its first context
if (!gl) throw new Error('No GPU API: show a static image');
const info = gl.getExtension('WEBGL_debug_renderer_info');
return { api: `WebGL 2 (${info ? gl.getParameter(info.UNMASKED_RENDERER_WEBGL) : 'renderer hidden'})`, draw: () => drawWebGL(gl) };
}
function drawWebGPU(device, context, format) {
const module = device.createShaderModule({ code: `
struct Out { @builtin(position) position: vec4f, @location(0) color: vec3f };
@vertex fn vs(@location(0) p: vec2f, @location(1) c: vec3f) -> Out { return Out(vec4f(p, 0, 1), c); }
@fragment fn fs(in: Out) -> @location(0) vec4f { return vec4f(in.color, 1); }` });
const pipeline = device.createRenderPipeline({ layout: 'auto',
vertex: { module, buffers: [{ arrayStride: 20, attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x2' }, { shaderLocation: 1, offset: 8, format: 'float32x3' }] }] },
fragment: { module, targets: [{ format }] } });
const buffer = device.createBuffer({ size: vertices.byteLength, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST });
device.queue.writeBuffer(buffer, 0, vertices);
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({ colorAttachments: [{ view: context.getCurrentTexture().createView(),
clearValue: [0.93, 0.91, 0.87, 1], loadOp: 'clear', storeOp: 'store' }] });
pass.setPipeline(pipeline); pass.setVertexBuffer(0, buffer); pass.draw(36); pass.end();
device.queue.submit([encoder.finish()]);
}
function drawWebGL(gl) {
const program = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, `#version 300 es
layout(location = 0) in vec2 aPosition; layout(location = 1) in vec3 aColor; out vec3 vColor;
void main() { vColor = aColor; gl_Position = vec4(aPosition, 0.0, 1.0); }`], [gl.FRAGMENT_SHADER, `#version 300 es
precision mediump float; in vec3 vColor; out vec4 c; void main() { c = vec4(vColor, 1.0); }`]]) {
const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s); gl.attachShader(program, s);
}
gl.linkProgram(program); gl.useProgram(program);
gl.bindVertexArray(gl.createVertexArray());
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 20, 0); gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 20, 8); gl.enableVertexAttribArray(1);
gl.clearColor(0.93, 0.91, 0.87, 1); gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 36);
}
createRenderer(document.getElementById('a')).then((renderer) => {
renderer.draw();
document.getElementById('pa').textContent = 'createRenderer(): ' + renderer.api;
}).catch((error) => { document.getElementById('pa').textContent = error.message; });
// Two APIs coexist on one page, but not on one canvas.
const b = document.getElementById('b'), gl = b.getContext('webgl2');
drawWebGL(gl);
document.getElementById('pb').textContent = `always WebGL 2 | then getContext('webgpu') on this canvas: ${b.getContext('webgpu')}`;
</script>