These ten questions cover WebGPU behaviors that surprise developers who know WebGL or other GPU APIs: limits you did not ask for, errors that never throw, a canvas texture that changes, mapped memory that vanishes, gray that is not 128, the order of select(), arithmetic that never faults, bounds that clamp, and a workgroup size the hardware allows but the device refuses. Every snippet ran in Chrome 154 1 on the GTX 1650 through the check.mjs script of Test Yourself!, which prints a page's console. Write down what each line prints and why, then check Appendix H for the real results and the reasons.
Questions
<canvas id="view" width="64" height="64"></canvas>
<script type="module">
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
// 1. The adapter offers 1,024 invocations per workgroup and 2 GiB storage bindings. What
// does a device requested with no limits get? (Sections 4.3.4 and 4.3.5)
const L = 'maxComputeInvocationsPerWorkgroup', S = 'maxStorageBufferBindingSize';
console.log('1:', device.limits[L], device.limits[S]);
// 2. A buffer with an invalid usage. Does the catch block run? (Sections 4.4.1 and 4.4.3)
device.pushErrorScope('validation');
try {
device.createBuffer({ size: 16, usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.STORAGE });
console.log('2: no exception');
} catch (e) { console.log('2: caught', e.name); }
console.log('2:', (await device.popErrorScope())?.constructor.name);
// 3. Is the canvas texture the same object twice in a task, and after a frame? (4.5.1)
const context = view.getContext('webgpu');
context.configure({ device, format: navigator.gpu.getPreferredCanvasFormat() });
const first = context.getCurrentTexture();
const again = context.getCurrentTexture() === first;
await new Promise(requestAnimationFrame);
console.log('3:', again, context.getCurrentTexture() === first);
// 4. What is left in an array taken from a mapped buffer after unmap()? (Section 4.9.3)
const mapped = device.createBuffer({ size: 16, usage: GPUBufferUsage.MAP_WRITE
| GPUBufferUsage.COPY_SRC, mappedAtCreation: true });
const floats = new Float32Array(mapped.getMappedRange()).fill(1);
mapped.unmap();
console.log('4:', floats.length, floats[0]);
// 5. Two textures cleared to 0.5 gray. Which bytes come back? (Sections 4.12.8 and 4.12.10)
for (const format of ['rgba8unorm', 'rgba8unorm-srgb']) {
const texture = device.createTexture({ size: [1, 1], format,
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC });
const read = device.createBuffer({ size: 4, usage: GPUBufferUsage.COPY_DST
| GPUBufferUsage.MAP_READ });
const encoder = device.createCommandEncoder();
encoder.beginRenderPass({ colorAttachments: [{ view: texture.createView(), loadOp: 'clear',
clearValue: [0.5, 0.5, 0.5, 1], storeOp: 'store' }] }).end();
encoder.copyTextureToBuffer({ texture }, { buffer: read, bytesPerRow: 256 }, [1, 1]);
device.queue.submit([encoder.finish()]);
await read.mapAsync(GPUMapMode.READ);
console.log('5:', format, [...new Uint8Array(read.getMappedRange())].join(','));
}
</script><script type="module">
const device = await (await navigator.gpu.requestAdapter()).requestDevice();
async function run(label, body, type = 'i32', size = 1) { // prints out[] or the first error
const code = `@group(0) @binding(0) var<storage, read_write> out: array<${type}, 4>;
@compute @workgroup_size(${size}) fn main(@builtin(local_invocation_index) i: u32) {
let zero = i32(i); // 0, but only known at run time
${body} }`;
device.pushErrorScope('validation');
const pipeline = device.createComputePipeline({ layout: 'auto',
compute: { module: device.createShaderModule({ code }) } });
const out = device.createBuffer({ size: 16, usage: GPUBufferUsage.STORAGE
| GPUBufferUsage.COPY_SRC });
const read = device.createBuffer({ size: 16, usage: GPUBufferUsage.COPY_DST
| GPUBufferUsage.MAP_READ });
const encoder = device.createCommandEncoder(), pass = encoder.beginComputePass();
pass.setPipeline(pipeline), pass.setBindGroup(0, device.createBindGroup({
layout: pipeline.getBindGroupLayout(0), entries: [{ binding: 0, resource: out }] }));
pass.dispatchWorkgroups(1), pass.end();
encoder.copyBufferToBuffer(out, 0, read, 0, 16), device.queue.submit([encoder.finish()]);
const error = await device.popErrorScope();
if (error) return console.log(label, error.message.split('\n')[0]);
await read.mapAsync(GPUMapMode.READ);
const View = type === 'f32' ? Float32Array : Int32Array;
console.log(label, [...new View(read.getMappedRange())].slice(0, 3).join(', '));
}
// 6. The argument order of select(), and % on a negative float. (Sections 4.7.8, 4.19.5)
await run('6:', 'out[0] = select(1.0, 2.0, true); out[1] = -7.0 % 4.0;', 'f32');
// 7. Division by zero and overflow on i32 values computed at run time. (Section 4.6.8)
await run('7:', 'out[0] = 7 / zero; out[1] = 7 % zero; out[2] = 2147483647 + (1 + zero);');
// 8. The same division, with both operands constant. (Sections 4.6.7 and 4.8.8)
await run('8:', 'out[0] = 7 / 0;');
// 9. Reading past the end of a four-element array. (Sections 4.2.7 and 4.8.7)
await run('9:', 'var a = array(10, 20, 30, 40); out[0] = a[4 + zero]; out[1] = a[9 + zero];');
// 10. A workgroup of 512 invocations on this adapter, which allows 1,024. (Section 4.16.2)
await run('10:', 'out[i % 4] = 1;', 'i32', 512);
</script>