First Shader Pair's covers now read the catalog: group 0 holds the 48-byte Book structs of Shared Book Struct as read-only storage visible to both stages, and group 1 a small uniform Shelf with a price ceiling and the id of a book to pull forward. Explicit layouts set minBindingSize from the WGSL sizes:
<canvas id="gpu" width="480" height="150"></canvas><pre id="log"></pre>
<script type="module">
const books = [ // id, price, rating, pages, year, inStock, tint (catalog.js, Section 4.6.11)
[1, 14.99, 4.6, 312, 2024, 1, [.08, .4, .75]], [2, 39.5, 4.3, 428, 2025, 1, [.18, .49, .2]],
[3, 24, 4.8, 256, 2023, 0, [.94, .42, 0]], [4, 18.75, 4.1, 198, 2026, 1, [.42, .11, .6]],
[5, 16.2, 4.5, 344, 2025, 1, [.22, .28, .31]],
[6, 21.3, 4.4, 176, 2024, 1, [.78, .16, .16]]];
const bytes = new ArrayBuffer(books.length * 48), view = new DataView(bytes);
books.forEach(([id, price, rating, pages, year, inStock, tint], i) => {
const o = i * 48, u32 = (at, v) => view.setUint32(o + at, v, true);
const f32 = (at, v) => view.setFloat32(o + at, v, true);
u32(0, id), f32(4, price), f32(8, rating), u32(12, pages);
tint.forEach((c, k) => f32(16 + 4 * k, c)), u32(28, year), u32(32, inStock);
});
const code = /* wgsl */ `
struct Book { id: u32, price: f32, rating: f32, pages: u32, tint: vec3f, year: u32,
inStock: u32 }
struct Shelf { maxPrice: f32, highlight: f32 } // highlight: a book id
@group(0) @binding(0) var<storage, read> catalog: array<Book>; // changes rarely
@group(1) @binding(0) var<uniform> shelf: Shelf; // changes per view
struct Cover { @builtin(position) pos: vec4f, @location(0) uv: vec2f,
@location(1) @interpolate(flat) book: u32 }
@vertex fn vs(@builtin(vertex_index) v: u32, @builtin(instance_index) b: u32) -> Cover {
let uv = array(vec2f(0, 0), vec2f(1, 0), vec2f(0, 1), vec2f(1, 0), vec2f(1, 1),
vec2f(0, 1))[v];
let book = catalog[b];
let lift = select(0.0, 0.15, f32(book.id) == shelf.highlight); // pull one book out
let size = vec2f(0.26, f32(book.pages) / 240);
return Cover(vec4f(vec2f(-0.93 + f32(b) * 0.31, -0.95 + lift) + uv * size, 0, 1), uv, b);
}
@fragment fn fs(in: Cover) -> @location(0) vec4f {
let book = catalog[in.book];
if (in.uv.y < 0.08) { // rating bar
return select(vec4f(1), vec4f(1, .8, .2, 1), in.uv.x < book.rating / 5);
}
let color = book.tint * (0.75 + 0.25 * in.uv.y) * select(1.0, 0.6, in.uv.x < 0.12);
let dim = book.inStock == 0 || book.price > shelf.maxPrice; // unavailable
return vec4f(select(color, vec3f(dot(color, vec3f(.3, .59, .11))), dim), 1);
}`;
const device = await (await navigator.gpu.requestAdapter()).requestDevice();
const { STORAGE, UNIFORM, COPY_DST } = GPUBufferUsage, { VERTEX, FRAGMENT } = GPUShaderStage;
const buffer = (label, data, usage) => {
const b = device.createBuffer({ label, size: data.byteLength, usage: usage | COPY_DST });
device.queue.writeBuffer(b, 0, data);
return b;
};
const catalog = buffer('catalog', bytes, STORAGE);
const shelf = buffer('shelf', new Float32Array([25, 4]), UNIFORM); // under $25, lift book 4
const layouts = [['read-only-storage', 48], ['uniform', 8]]
.map(([type, minBindingSize]) => device.createBindGroupLayout({ entries: [{ binding: 0,
visibility: VERTEX | FRAGMENT, buffer: { type, minBindingSize } }] }));
const groups = [catalog, shelf].map((resource, i) =>
device.createBindGroup({ layout: layouts[i], entries: [{ binding: 0, resource }] }));
const context = gpu.getContext('webgpu'), format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ device, format });
const module = device.createShaderModule({ label: 'shelf.wgsl', code });
const pipeline = await device.createRenderPipelineAsync({ label: 'shelf', vertex: { module },
layout: device.createPipelineLayout({ bindGroupLayouts: layouts }),
fragment: { module, targets: [{ format }] } });
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({ colorAttachments: [{ loadOp: 'clear', storeOp: 'store',
view: context.getCurrentTexture().createView(), clearValue: [0.96, 0.94, 0.90, 1] }] });
pass.setPipeline(pipeline), groups.forEach((g, i) => pass.setBindGroup(i, g));
pass.draw(6, books.length), pass.end();
device.queue.submit([encoder.finish()]);
await device.queue.onSubmittedWorkDone();
log.textContent = `catalog ${catalog.size} B (6 books x 48), shelf ${shelf.size} B`;
window.__done = true;
</script>
Heights, rating bars and colors all come from catalog[b]. "Patterns of the Deep Web" ($39.50) is over the $25 ceiling and "Salt and Saffron" is out of stock, so both turn grey, and "Small Steps to Big Summits" (id 4) sits raised. A new view is one 8-byte writeBuffer() to the shelf uniform.