Production code folds these steps into one routine with three habits: detect in layers (navigator.gpu, an adapter, then each feature and limit) with a fallback at each; request what you use and degrade the rest, branching on device.features and device.limits, never adapter.info; and install error and loss handlers before creating resources. The chapter's helper does all three, plus reporting functions for later sections:
// gpu-helper.js: the WebGPU setup every Chapter 4 example shares (Section 4.3.7)
export async function initGPU({ canvas, powerPreference, features = [], limits = {} } = {}) {
if (!('gpu' in navigator)) throw new Error('WebGPU is not available: use the WebGL 2 path');
const adapter = await navigator.gpu.requestAdapter({ powerPreference });
if (!adapter) throw new Error('No usable GPU adapter: use the WebGL 2 path');
const requiredFeatures = features.filter((f) => adapter.features.has(f)); // only real ones
const requiredLimits = {};
for (const [name, want] of Object.entries(limits)) {
const clamp = name.startsWith('min') ? Math.max : Math.min; // min*: smaller is better
requiredLimits[name] = clamp(want, adapter.limits[name]);
}
const device = await adapter.requestDevice({ requiredFeatures, requiredLimits });
device.lost.then((info) => console.warn(`device lost (${info.reason}): ${info.message}`));
device.addEventListener('uncapturederror',
(e) => console.error(`GPU error: ${e.error.message}`));
const format = navigator.gpu.getPreferredCanvasFormat();
const context = canvas ? canvas.getContext('webgpu') : null;
context?.configure({ device, format, alphaMode: 'opaque' });
return { adapter, device, context, format };
}
export async function compileWGSL(device, code, label = 'shader') { // print messages
const module = device.createShaderModule({ label, code });
const { messages } = await module.getCompilationInfo();
for (const m of messages) {
console.log(`${label}:${m.lineNum}:${m.linePos} ${m.type}: ${m.message}`);
}
return module;
}
export async function checked(device, what, fn, filter = 'validation') { // scope report
device.pushErrorScope(filter);
const result = fn();
const error = await device.popErrorScope();
console.log(error ? `${what}: ${error.constructor.name}: ${error.message}` : `${what}: ok`);
return result;
}
export const markDone = (device) => // tells the book's capture pipeline the frame is ready
device.queue.onSubmittedWorkDone().then(() => { window.__done = true; });A page beside it exercises every path: a missing feature, an impossible limit, a compiler warning and a validation error:
<canvas id="shelf" width="240" height="120"></canvas>
<script type="module">
import { initGPU, compileWGSL, checked, markDone } from './gpu-helper.js';
const { adapter, device, format } = await initGPU({ canvas: shelf,
features: ['timestamp-query', 'texture-compression-astc'], // the second is absent here
limits: { maxBufferSize: 2 ** 40 } }); // clamped to the adapter's
console.log(`${adapter.info.vendor}, ${format}, ${[...device.features].join(', ')}`);
console.log(`maxBufferSize: ${device.limits.maxBufferSize}`);
await compileWGSL(device, `@fragment fn fs() -> @location(0) vec4f {
return vec4f(0.08, 0.40, 0.75, 1);
let unused = 2;
}`, 'spine.wgsl');
await checked(device, 'createBuffer', () => device.createBuffer({ size: 4, usage: 0 }));
markDone(device);
</script>nvidia, bgra8unorm, core-features-and-limits, timestamp-query maxBufferSize: 2147483648 spine.wgsl:3:5 warning: code is unreachable createBuffer: GPUValidationError: Buffer usages must not be 0. - While calling [Device].CreateBuffer([BufferDescriptor]).
The page survived all four problems and reported each (Errors and Device Loss and getCompilationInfo() go deeper). Rendered figures must be self-contained, so later listings inline the helper lines they need.
<!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');
const report = []; // [status, title, detail] rows for the picture
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);
}
// Layer 1-3: API, adapter, then only the features and limits the adapter really has.
async function initGPU({ canvas, powerPreference, features = [], limits = {} } = {}) {
if (!('gpu' in navigator)) throw new Error('WebGPU is not available: use the WebGL 2 path');
const adapter = await navigator.gpu.requestAdapter({ powerPreference });
if (!adapter) throw new Error('No usable GPU adapter: use the WebGL 2 path');
const requiredFeatures = features.filter((f) => adapter.features.has(f));
const requiredLimits = {};
for (const [name, want] of Object.entries(limits)) {
const clamp = name.startsWith('min') ? Math.max : Math.min; // min*: smaller is better
requiredLimits[name] = clamp(want, adapter.limits[name]);
}
const device = await adapter.requestDevice({ requiredFeatures, requiredLimits });
device.lost.then((info) => console.warn(`device lost (${info.reason}): ${info.message}`));
device.addEventListener('uncapturederror', (e) => console.error(`GPU error: ${e.error.message}`));
const format = navigator.gpu.getPreferredCanvasFormat();
const context = canvas ? canvas.getContext('webgpu') : null;
context?.configure({ device, format, alphaMode: 'opaque' });
return { adapter, device, context, format };
}
async function compileWGSL(device, code, label = 'shader') {
const module = device.createShaderModule({ label, code });
const { messages } = await module.getCompilationInfo();
return { module, messages };
}
async function checked(device, fn, filter = 'validation') {
device.pushErrorScope(filter);
const result = fn();
return { result, error: await device.popErrorScope() };
}
async function main() {
let gpu;
try {
gpu = await initGPU({ canvas, features: ['timestamp-query', 'texture-compression-astc'],
limits: { maxBufferSize: 2 ** 40 } });
} catch (error) { return showMessage(error.message); }
const { device, context, format } = gpu;
for (const f of ['timestamp-query', 'texture-compression-astc']) {
report.push([device.features.has(f), `feature ${f}`, device.features.has(f) ? 'requested and granted' : 'absent on this adapter: skipped, not requested']);
}
report.push([true, 'limit maxBufferSize: asked for 2^40', `clamped to the adapter's ${device.limits.maxBufferSize.toLocaleString('en-US')} bytes`]);
const { module, messages } = await compileWGSL(device, `
@vertex fn vs(@builtin(vertex_index) v: u32) -> @builtin(position) vec4f {
let p = array(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3))[v];
return vec4f(p, 0, 1);
}
@fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
return vec4f(vec3f(0.08, 0.40, 0.75) * (0.8 + 0.2 * step(0.5, fract(p.x / 48))), 1);
let unused = 2;
}`, 'spine.wgsl');
const m = messages[0];
report.push([!m || m.type !== 'error', 'compileWGSL(spine.wgsl)', m ? `${m.lineNum}:${m.linePos} ${m.type}: ${m.message}` : 'no messages']);
const { error } = await checked(device, () => device.createBuffer({ size: 4, usage: 0 }));
report.push([!error, 'createBuffer({ size: 4, usage: 0 })', error ? `${error.constructor.name}: ${error.message.split('\n')[0]}` : 'ok']);
// Draw the (still working) shader as a backdrop, then the report cards on top.
const pipeline = device.createRenderPipeline({ layout: 'auto', vertex: { module }, fragment: { module, targets: [{ format }] } });
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.setViewport(0, 0, 600, 44, 0, 1); // a strip of BookNest-blue spines
pass.draw(3);
pass.end();
device.queue.submit([encoder.finish()]);
ink.font = 'bold 15px system-ui, sans-serif'; ink.fillStyle = '#fff';
ink.fillText('initGPU(): the page survived all four problems', 14, 28);
report.forEach(([ok, title, detail], i) => {
const y = 58 + i * 58;
ink.fillStyle = ok ? '#e2f1e5' : '#fbe9d2'; ink.beginPath(); ink.roundRect(10, y, 580, 50, 8); ink.fill();
ink.fillStyle = ok ? '#2a8f4d' : '#d08a1c'; ink.beginPath(); ink.arc(30, y + 25, 9, 0, 7); ink.fill();
ink.fillStyle = '#222'; ink.font = 'bold 13px system-ui, sans-serif'; ink.fillText(title, 48, y + 20);
ink.fillStyle = '#444'; ink.font = '12px system-ui, sans-serif'; ink.fillText(detail.slice(0, 88), 48, y + 38);
});
}
main();
</script>