The page runs 768 pages for 150 frames (2.5 simulated seconds), each frame one dispatch and one instanced draw, then signals the capture; it fetches pages.wgsl from Billboard Particles:
<canvas id="sky" width="480" height="270"></canvas>
<script type="module">
const device = await (await navigator.gpu.requestAdapter()).requestDevice();
const COUNT = 768, { STORAGE, UNIFORM, COPY_DST } = GPUBufferUsage;
const init = new Float32Array(COUNT * 8); // 8 floats per page, all zero...
for (let i = 0; i < COUNT; i++) init[i * 8 + 6] = -3 * i / COUNT; // ...and staggered ages
const pages = device.createBuffer({ size: init.byteLength, usage: STORAGE | COPY_DST });
device.queue.writeBuffer(pages, 0, init);
const sim = device.createBuffer({ size: 16, usage: UNIFORM | COPY_DST });
const code = await (await fetch('pages.wgsl')).text();
const module = device.createShaderModule({ code });
const format = navigator.gpu.getPreferredCanvasFormat(), context = sky.getContext('webgpu');
context.configure({ device, format, alphaMode: 'opaque' });
const update = device.createComputePipeline({ layout: 'auto', compute: { module } });
const blend = { srcFactor: 'one', dstFactor: 'one-minus-src-alpha' }; // premultiplied
const draw = device.createRenderPipeline({ layout: 'auto', vertex: { module },
fragment: { module, targets: [{ format, blend: { color: blend, alpha: blend } }] },
primitive: { topology: 'triangle-strip' } });
const group = (pipeline) => device.createBindGroup({ layout: pipeline.getBindGroupLayout(0),
entries: [pages, sim].map((resource, binding) => ({ binding, resource })) });
const [updateGroup, drawGroup] = [group(update), group(draw)]; // pages.wgsl: Section 4.18.3
let frame = 0;
function tick() {
const u = new Float32Array([1 / 60, frame / 60, sky.width / sky.height, 0]);
new Uint32Array(u.buffer)[3] = frame;
device.queue.writeBuffer(sim, 0, u);
const encoder = device.createCommandEncoder(), compute = encoder.beginComputePass();
compute.setPipeline(update), compute.setBindGroup(0, updateGroup);
compute.dispatchWorkgroups(COUNT / 64), compute.end(); // simulate: no read-back
const pass = encoder.beginRenderPass({ colorAttachments: [{ loadOp: 'clear',
storeOp: 'store', view: context.getCurrentTexture().createView(),
clearValue: [0.07, 0.16, 0.29, 1] }] });
pass.setPipeline(draw), pass.setBindGroup(0, drawGroup);
pass.draw(4, COUNT), pass.end(); // one instanced draw, 768 quads
device.queue.submit([encoder.finish()]);
if (++frame < 150) requestAnimationFrame(tick); // 2.5 simulated seconds, then stop
else device.queue.onSubmittedWorkDone().then(() => { window.__done = true; });
}
requestAnimationFrame(tick);
</script>
Pages stream up from the emitter at the bottom center, fan out, turn over and drift down as they fade. Per frame the CPU writes 16 bytes and records two passes, whatever the count; 768 keeps the figure readable.