Creating a GPURenderPipeline

device.createRenderPipeline(descriptor) returns at once and reports errors later; createRenderPipelineAsync() returns a promise that resolves when the pipeline is compiled (or rejects with a GPUPipelineError), so it never stalls a frame. The descriptor mirrors the stages a draw passes through:

The parts of a render pipeline descriptor and the stages they configure
The parts of a render pipeline descriptor and the stages they configure

Only layout and vertex.module are required; the defaults are triangle-list, no culling, one sample and no depth, and a pipeline without fragment is legal for depth-only passes. Since everything is fixed at creation, switching "state" is one setPipeline(); build all pipelines at startup with Promise.all().

Four render pipelines built together with createRenderPipelineAsync() and Promise.all(), each fixing different stateHTMLLive
<!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="340"></canvas>
  <canvas id="labels" width="600" height="340"></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);
}

// Three overlapping covers; odd covers are wound clockwise to show culling.
const code = /* wgsl */ `
struct Out { @builtin(position) pos: vec4f, @location(0) color: vec4f }
@vertex fn vs(@builtin(vertex_index) v: u32, @builtin(instance_index) i: u32) -> Out {
  var q = array(vec2f(0, 0), vec2f(1, 0), vec2f(0, 1), vec2f(1, 0), vec2f(1, 1), vec2f(0, 1))[v];
  if (i % 2 == 1) { q = vec2f(q.y, q.x); }        // swapping x and y flips the winding
  let p = vec2f(-0.75 + f32(i) * 0.4 + q.x * 0.7, -0.6 + q.y * 1.1 - f32(i) * 0.1);
  let tints = array(vec3f(0.08, 0.40, 0.75), vec3f(0.85, 0.40, 0.18), vec3f(0.16, 0.56, 0.30));
  return Out(vec4f(p, 0, 1), vec4f(tints[i], 0.6));
}
@fragment fn fs(in: Out) -> @location(0) vec4f { return in.color; }      // alpha matters only with blend`;

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 });
  const module = device.createShaderModule({ code });

  // Only layout and vertex.module are required; everything else has a default.
  const base = { layout: 'auto', vertex: { module }, fragment: { module, targets: [{ format }] } };
  const alphaBlend = { color: { srcFactor: 'src-alpha', dstFactor: 'one-minus-src-alpha' },
                          alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha' } };
  const variants = [
    ['defaults: triangle-list, no culling', base],
    ["primitive: { cullMode: 'back' }", { ...base, primitive: { cullMode: 'back' } }],
    ["primitive: { topology: 'line-list' }", { ...base, primitive: { topology: 'line-list' } }],
    ['targets: [{ format, blend }]', { ...base, fragment: { module, targets: [{ format, blend: alphaBlend }] } }]];
  const t0 = performance.now();
  const pipelines = await Promise.all(variants.map(([, d]) => device.createRenderPipelineAsync(d)));
  const elapsed = performance.now() - t0;

  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' }] });
  pipelines.forEach((pipeline, i) => {
    pass.setViewport((i % 2) * 300 + 10, Math.floor(i / 2) * 160 + 34, 280, 120, 0, 1);
    pass.setPipeline(pipeline);                   // switching "state" is one call
    pass.draw(6, 3);
  });
  pass.end();
  device.queue.submit([encoder.finish()]);

  ink.font = '12px ui-monospace, monospace'; ink.fillStyle = '#222';
  variants.forEach(([name], i) => ink.fillText(name, (i % 2) * 300 + 12, Math.floor(i / 2) * 160 + 24));
  ink.font = '12px system-ui, sans-serif'; ink.fillStyle = '#1f4f8a';
  ink.fillText(`All four pipelines compiled in parallel with Promise.all() in ${elapsed.toFixed(1)} ms, before the first frame.`, 12, 334);
}
main();
</script>