draw(vertexCount, instanceCount = 1, firstVertex = 0, firstInstance = 0) runs the vertex shader vertexCount x instanceCount times. drawIndexed(indexCount, instanceCount, firstIndex, baseVertex, firstInstance) reads vertex numbers from the buffer set with setIndexBuffer(buffer, 'uint16' | 'uint32') and adds baseVertex to each, so many meshes can share one vertex buffer. A cover quad as a triangle list needs 6 vertices (48 bytes at 8 bytes each); indexed, it needs 4 corners (32 bytes) plus 12 bytes of uint16 indices [0, 1, 2, 0, 2, 3], and the GPU's post-transform cache can skip re-shading repeated indices; a grid mesh reuses each vertex about six times. Pass instanceCount rather than looping over draws: First Cover Quad draws six covers with one drawIndexed(6, 6).
<!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) color: vec3f }
@vertex fn vs(@location(0) p: vec2f, @builtin(instance_index) i: u32) -> Out {
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 + vec2f(f32(i) * 0.3, 0), 0, 1), tints[i % 3]);
}
@fragment fn fs(in: Out) -> @location(0) vec4f { return vec4f(in.color, 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 });
const B = GPUBufferUsage;
const upload = (data, usage) => {
const b = device.createBuffer({ size: Math.ceil(data.byteLength / 4) * 4, usage: usage | B.COPY_DST });
device.queue.writeBuffer(b, 0, data.buffer, 0, Math.ceil(data.byteLength / 4) * 4);
return b;
};
// Left: a triangle list repeats two corners: 6 vertices x 8 bytes = 48 bytes per quad.
const listVerts = new Float32Array([-0.95, -0.5, -0.75, -0.5, -0.95, 0.5, -0.75, -0.5, -0.75, 0.5, -0.95, 0.5]);
// Right: one vertex buffer holding two meshes: a quad (4 corners) and a bookmark (5 corners).
const sharedVerts = new Float32Array([0.05, -0.5, 0.25, -0.5, 0.25, 0.5, 0.05, 0.5, // quad, vertices 0-3
0.05, 0.5, 0.05, -0.5, 0.15, -0.3, 0.25, -0.5, 0.25, 0.5]); // bookmark, 4-8
const indices = new Uint16Array([0, 1, 2, 0, 2, 3, // the quad: 12 bytes
0, 1, 2, 0, 2, 4, 2, 3, 4]); // the bookmark, relative to its first vertex
const listBuffer = upload(listVerts, B.VERTEX), sharedBuffer = upload(sharedVerts, B.VERTEX);
const indexBuffer = upload(new Uint16Array([...indices, 0]), B.INDEX); // padded to 4 bytes
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' }] }] } });
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, listBuffer);
pass.draw(6, 3); // vertexCount 6, 3 instances
pass.setVertexBuffer(0, sharedBuffer);
pass.setIndexBuffer(indexBuffer, 'uint16');
pass.drawIndexed(6, 1, 0, 0, 0); // the quad: indices 0-5, baseVertex 0
pass.drawIndexed(9, 2, 6, 4, 1); // the bookmark: indices 6-14, baseVertex 4, instances 1-2
pass.end();
device.queue.submit([encoder.finish()]);
const toPx = (x, y) => [(x + 1) * 300, (1 - y) * 170];
ink.font = '10px ui-monospace, monospace';
const names = new Map(); // vertices at the same spot share one label
const add = (x, y, k) => { const key = toPx(x, y).join(); names.set(key, names.has(key) ? names.get(key) + '/' + k : String(k)); };
for (let k = 0; k < 6; k++) add(listVerts[k * 2], listVerts[k * 2 + 1], k);
for (let k = 0; k < 9; k++) add(sharedVerts[k * 2] + (k >= 4 ? 0.3 : 0), sharedVerts[k * 2 + 1], k);
for (const [key, text] of names) {
const [px, py] = key.split(',').map(Number);
ink.fillStyle = '#222'; ink.beginPath(); ink.arc(px, py, 4, 0, 7); ink.fill(); ink.fillText(text, px + 5, py - 6);
}
ink.font = '12px ui-monospace, monospace'; ink.fillStyle = '#222';
ink.fillText('draw(6, 3)', 12, 30);
ink.fillText('drawIndexed(6, 1, 0, 0, 0)', 310, 22);
ink.fillText('drawIndexed(9, 2, 6, 4, 1)', 310, 40);
ink.font = '12px system-ui, sans-serif'; ink.fillStyle = '#444';
ink.fillText('48 bytes per quad: corners 0/3 and 2/5 repeat', 12, 280);
ink.fillText('quad: 32 bytes of corners + 12 bytes of indices', 310, 280);
ink.fillText('bookmark: baseVertex 4 is added to each of its indices,', 310, 300);
ink.fillText('so both meshes share one vertex buffer', 310, 318);
}
main();
</script>