Shared Book Struct

BookNest's Shared Struct Layout for a Book

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:

demos/ch04/catalog-layout.html: one struct layout for WGSL and JavaScriptHTMLLive
<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>
Browser output of Listing 4.37
Browser output of 37

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.