Feature Detection

Feature-Detection Patterns for Production Code

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:

demos/ch04/gpu-helper.js: the setup code every Chapter 4 example sharesJavaScript
// 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:

demos/ch04/helper-demo.html: using the helperHTML
<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>
Output
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.

A layered initGPU() helper surviving a missing feature, an impossible limit, a compiler warning and a validation errorHTMLLive
<!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>