The camera stands in BookNest's aisle, 2.5 units from a wall of 1,200 books and turned 0.6 radians along it. One dispatch of 19 workgroups tests every book, and one drawIndirect() draws the survivors; the page reads back the arguments and the list to report what the GPU decided:
<canvas id="aisle" width="480" height="220"></canvas>
<script type="module">
const device = await (await navigator.gpu.requestAdapter()).requestDevice();
const { UNIFORM, STORAGE, INDIRECT, COPY_DST, COPY_SRC, MAP_READ } = GPUBufferUsage;
const buffer = (size, usage) => device.createBuffer({ size, usage });
const camera = buffer(32, UNIFORM | COPY_DST), visible = buffer(1200 * 4, STORAGE | COPY_SRC);
const args = buffer(16, INDIRECT | STORAGE | COPY_DST | COPY_SRC);
const read = buffer(16 + 1200 * 4, COPY_DST | MAP_READ);
device.queue.writeBuffer(camera, 0, new Float32Array([0, 0.3, 2.5, 0.6, // eye, yaw
aisle.width / aisle.height])); // aspect
device.queue.writeBuffer(args, 0, new Uint32Array([4, 0, 0, 0])); // 4 vertices, 0 books
const code = await (await fetch('shelf-cull.wgsl')).text(); // Section 4.18.8
const module = device.createShaderModule({ code });
const cull = device.createComputePipeline({ layout: 'auto', compute: { module } });
const format = navigator.gpu.getPreferredCanvasFormat(), context = aisle.getContext('webgpu');
context.configure({ device, format });
const draw = device.createRenderPipeline({ layout: 'auto', vertex: { module },
fragment: { module, targets: [{ format }] }, primitive: { topology: 'triangle-strip' } });
const group = (pipeline, entries) => device.createBindGroup({
layout: pipeline.getBindGroupLayout(0), entries });
const encoder = device.createCommandEncoder(), compute = encoder.beginComputePass();
compute.setPipeline(cull), compute.setBindGroup(0, group(cull, [{ binding: 0, resource:
camera }, { binding: 1, resource: args }, { binding: 2, resource: visible }]));
compute.dispatchWorkgroups(Math.ceil(1200 / 64)), compute.end(); // one test per book
const pass = encoder.beginRenderPass({ colorAttachments: [{ loadOp: 'clear', storeOp: 'store',
view: context.getCurrentTexture().createView(), clearValue: [0.36, 0.25, 0.17, 1] }] });
pass.setPipeline(draw), pass.setBindGroup(0, group(draw, [{ binding: 0, resource: camera },
{ binding: 2, resource: visible }]));
pass.drawIndirect(args, 0), pass.end(); // the instance count comes from the GPU
encoder.copyBufferToBuffer(args, 0, read, 0, 16);
encoder.copyBufferToBuffer(visible, 0, read, 16, 1200 * 4);
device.queue.submit([encoder.finish()]), await read.mapAsync(GPUMapMode.READ);
const [vertexCount, count] = new Uint32Array(read.getMappedRange(0, 16));
const kept = new Uint32Array(read.getMappedRange(16, count * 4)).map((id) => id % 200);
console.log(`drawIndirect: ${vertexCount} vertices x ${count} of 1200 books`);
console.log(`slots drawn: ${Math.min(...kept)} to ${Math.max(...kept)} of 0 to 199`);
window.__done = true;
</script>
drawIndirect: 4 vertices x 637 of 1200 books slots drawn: 92 to 199 of 0 to 199
The first 92 slots of each shelf lie behind or beside the camera and were never drawn, and drawing all 1,200 books without the compute pass produced a pixel-identical image, so nothing visible was lost.