Vertex Buffer Layouts

Vertex Buffer Layouts and Step Modes

Buffers and Bind Groups's shelf computed corners from vertex_index and read books from storage. Vertex buffers are the fixed-function alternative: vertex.buffers describes up to 8 buffers (maxVertexBuffers), each with an arrayStride, a stepMode and attributes that map byte offsets to @location inputs. 'vertex' advances per vertex; 'instance' advances per instance, so one buffer of corners and one of books draw a whole shelf. The layout from First Cover Quad:

Two vertex buffers: quad corners per vertex, book data per instanceJavaScript
vertex: { module, buffers: [
  { arrayStride: 8, attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x2' }] },
  { arrayStride: 8, stepMode: 'instance', attributes: [
    { shaderLocation: 1, offset: 0, format: 'float32' },     // left edge
    { shaderLocation: 2, offset: 4, format: 'uint16' },      // page count
    { shaderLocation: 3, offset: 6, format: 'uint8' }] }] }  // atlas cell

pass.setVertexBuffer(slot, buffer) binds each, the slot being its index in buffers. A shader input with no matching attribute fails pipeline creation ("Vertex attribute slot 1 used in ... is not present in the VertexState"). Vertex buffers suit data read once per vertex, in order; storage buffers suit random access. Vertex buffers also work where storage in the vertex stage is limited: the specification's maxStorageBuffersInVertexStage limit can be lower than the per-stage default on small devices.

Two vertex buffers: quad corners stepped per vertex, packed book records stepped per instanceHTMLLive
<!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);
}

const code = /* wgsl */ `
struct Out { @builtin(position) pos: vec4f, @location(0) uv: vec2f, @location(1) @interpolate(flat) cell: u32 }
@vertex fn vs(@location(0) corner: vec2f,          // buffer 0, stepMode 'vertex'
              @location(1) left: f32,              // buffer 1, stepMode 'instance': float32
              @location(2) pages: u32,             //   uint16 page count
              @location(3) cell: vec2u) -> Out {   //   uint8x2: atlas cell, spare byte
  let height = f32(pages) / 450 * 1.4;
  return Out(vec4f(left + corner.x * 0.26, -0.75 + corner.y * height, 0, 1), corner, cell.x);
}
@fragment fn fs(in: Out) -> @location(0) vec4f {
  let tints = array(vec3f(.12, .37, .55), vec3f(.36, .25, .60), vec3f(.88, .60, .06),
                    vec3f(.25, .49, .23), vec3f(.71, .27, .18), vec3f(.16, .62, .56));
  let band = step(0.72, in.uv.y) * step(in.uv.y, 0.8);
  let shade = 0.75 + 0.25 * smoothstep(0.0, 0.15, in.uv.x);
  return vec4f(mix(tints[in.cell] * shade, vec3f(0.98, 0.95, 0.86), band), 1);
}`;

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 });

  // Buffer 0: six corners of a quad, 8 bytes each.
  const corners = new Float32Array([0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1]);
  // Buffer 1: one 8-byte record per book: float32 left edge, uint16 pages, uint8 cell, 1 spare byte.
  const pages = [312, 428, 256, 198, 344, 176];
  const records = new ArrayBuffer(8 * 6), view = new DataView(records);
  pages.forEach((p, i) => {
    view.setFloat32(i * 8, -0.9 + i * 0.3, true);
    view.setUint16(i * 8 + 4, p, true);
    view.setUint8(i * 8 + 6, (i * 5) % 6);
  });
  const upload = (data) => {
    const b = device.createBuffer({ size: data.byteLength, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST });
    device.queue.writeBuffer(b, 0, data);
    return b;
  };
  const cornerBuffer = upload(corners), bookBuffer = upload(records);

  const module = device.createShaderModule({ code });
  const pipeline = device.createRenderPipeline({ layout: 'auto', fragment: { module, targets: [{ format }] },
    vertex: { module, buffers: [
      { arrayStride: 8, attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x2' }] },
      { arrayStride: 8, stepMode: 'instance', attributes: [
        { shaderLocation: 1, offset: 0, format: 'float32' },     // left edge
        { shaderLocation: 2, offset: 4, format: 'uint16' },      // page count
        { shaderLocation: 3, offset: 6, format: 'uint8x2' }] }] } });  // atlas cell (+ spare)

  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.setVertexBuffer(0, cornerBuffer);             // slot = index in vertex.buffers
  pass.setVertexBuffer(1, bookBuffer);
  pass.draw(6, 6);                                   // 6 corners x 6 instances
  pass.end();
  device.queue.submit([encoder.finish()]);

  ink.font = '12px ui-monospace, monospace'; ink.fillStyle = '#222';
  ink.fillText("buffer 0: arrayStride 8, stepMode 'vertex'   -> 6 corners", 12, 20);
  ink.fillText("buffer 1: arrayStride 8, stepMode 'instance' -> 6 books", 12, 38);
  // Byte map of one instance record
  const parts = [['float32 left', 4, '#3d7fc4'], ['uint16 pages', 2, '#d98a2b'], ['u8 cell', 1, '#5aa36b'], ['spare', 1, '#bbb']];
  let x = 12;
  for (const [name, bytes, color] of parts) {
    ink.fillStyle = color; ink.fillRect(x, 300, bytes * 70 - 3, 22);
    ink.fillStyle = '#fff'; ink.font = '11px system-ui, sans-serif'; ink.fillText(name, x + 4, 315);
    x += bytes * 70;
  }
  ink.fillStyle = '#444'; ink.fillText('one 8-byte instance record (three float32s would take 12)', x + 8, 315);
  ink.textAlign = 'center';
  pages.forEach((p, i) => ink.fillText(`${p} p.`, 75 + i * 90, 280));
}
main();
</script>