From here on, BookNest's catalog lives on the GPU as a storage buffer of Book structs. Text stays in JavaScript; the numbers and the spine tint of No Global State are written with a DataView at WGSL's offsets. To prove both sides agree, a compute pass copies each book member by member, at a 15% sale price, into a buffer pre-filled with 0xeeeeeeee, and the page shows book 1's twelve words as the GPU wrote them:
<style>td { border: 1px solid #999; padding: 3px; font: 12px Consolas, monospace }</style>
<table id="grid"></table><pre id="log"></pre>
<script type="module">
const books = [ // id, price, rating, pages, year, inStock, tint: BookNest's six books
[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 code = /* wgsl */ `
struct Book {
id: u32, price: f32, rating: f32, pages: u32, // offsets 0, 4, 8, 12
tint: vec3f, // offset 16: a vec3f aligns to 16
year: u32, // offset 28: the vec3f's spare slot
inStock: u32, // offset 32: bool is not host-shareable
} // size 48: rounded up to a multiple of 16
@group(0) @binding(0) var<storage, read> catalog: array<Book>;
@group(0) @binding(1) var<storage, read_write> sale: array<Book>;
@compute @workgroup_size(6) fn main(@builtin(local_invocation_index) i: u32) {
let b = catalog[i]; // member by member: padding untouched
sale[i].id = b.id; sale[i].price = round(b.price * 85) / 100; sale[i].rating = b.rating;
sale[i].pages = b.pages; sale[i].tint = b.tint; sale[i].year = b.year;
sale[i].inStock = b.inStock;
}`;
const SIZE = 48, bytes = new ArrayBuffer(books.length * SIZE), view = new DataView(bytes);
books.forEach(([id, price, rating, pages, year, inStock, tint], i) => {
const o = i * SIZE, 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 device = await (await navigator.gpu.requestAdapter()).requestDevice();
const { STORAGE, COPY_SRC, COPY_DST, MAP_READ } = GPUBufferUsage, n = bytes.byteLength;
const catalog = device.createBuffer({ size: n, usage: STORAGE | COPY_DST });
device.queue.writeBuffer(catalog, 0, bytes);
const sale = device.createBuffer({ size: n, usage: STORAGE | COPY_SRC,
mappedAtCreation: true });
new Uint32Array(sale.getMappedRange()).fill(0xeeeeeeee), sale.unmap();
const read = device.createBuffer({ size: n, usage: COPY_DST | MAP_READ });
const module = device.createShaderModule({ code });
const pipeline = device.createComputePipeline({ layout: 'auto', compute: { module } });
const encoder = device.createCommandEncoder(), pass = encoder.beginComputePass();
pass.setPipeline(pipeline);
pass.setBindGroup(0, device.createBindGroup({ layout: pipeline.getBindGroupLayout(0),
entries: [{ binding: 0, resource: { buffer: catalog } },
{ binding: 1, resource: { buffer: sale } }] }));
pass.dispatchWorkgroups(1), pass.end();
encoder.copyBufferToBuffer(sale, 0, read, 0, n);
device.queue.submit([encoder.finish()]), await read.mapAsync(GPUMapMode.READ);
const out = new DataView(read.getMappedRange());
const hex = (k) => out.getUint32(k * 4, true).toString(16).padStart(8, '0');
const names = 'id price rating pages tint.r tint.g tint.b year inStock pad pad pad'.split(' ');
const cell = (k, t) => `<td style="background:${k > 8 ? '#eceff1' : '#e3f2fd'}">${t}</td>`;
for (const first of [0, 6]) { // book 1's twelve words, six per row
const ks = [0, 1, 2, 3, 4, 5].map((k) => k + first);
grid.insertRow().innerHTML = ks.map((k) => cell(k, `byte ${k * 4}: ${names[k]}`)).join('');
grid.insertRow().innerHTML = ks.map((k) => cell(k, hex(k))).join('');
}
log.textContent = 'sale prices: ' + books.map((b, i) => out.getFloat32(i * SIZE + 4, true)
.toFixed(2)).join(', ');
window.__done = true;
</script>
Every member landed where JavaScript put it (0x414bd70a is the f32 12.74, 0x7e8 is 2024), and the sentinel survives in bytes 36-47, the padding that rounds the struct up to 48 bytes, a multiple of its 16-byte alignment. demos/ch04/catalog.js keeps this code.