Cover Blur

Applying a Compute Blur to BookNest's Cover Art

The page loads The Quiet Harbor at 160 x 240, runs the three dispatches in one compute pass (horizontal blur, vertical blur, Sobel on the blurred image), and copies the three results into a canvas configured only as a copy destination; no render pipeline is involved. It fetches the shader file of Blur and Edge Detection:

demos/ch04/cover-blur.html: blurring and edge-detecting a cover with computeHTMLLive
<canvas id="out" width="480" height="240"></canvas>
<script type="module">
const device = await (await navigator.gpu.requestAdapter()).requestDevice();
const tex = GPUTextureUsage, [W, H] = [160, 240];
const blob = await (await fetch('booknest-cover-1.png')).blob();
const source = await createImageBitmap(blob, { resizeWidth: W, resizeHeight: H });
const texture = (usage) => device.createTexture({ size: [W, H], format: 'rgba8unorm',
  usage: usage | tex.TEXTURE_BINDING | tex.COPY_SRC });
const cover = texture(tex.COPY_DST | tex.RENDER_ATTACHMENT);
const [across, blurred, edges] = [0, 1, 2].map(() => texture(tex.STORAGE_BINDING));
device.queue.copyExternalImageToTexture({ source }, { texture: cover }, [W, H]);
const code = await (await fetch('image-filters.wgsl')).text();   // Section 4.17.10
const module = device.createShaderModule({ code });
const pipeline = (entryPoint, constants) => device.createComputePipeline({ layout: 'auto',
  compute: { module, entryPoint, constants } });
const encoder = device.createCommandEncoder(), pass = encoder.beginComputePass();
for (const [p, from, to, groups] of [
  [pipeline('blur', { horizontal: 1 }), cover, across, [Math.ceil(W / 64), H]],
  [pipeline('blur', { horizontal: 0 }), across, blurred, [Math.ceil(H / 64), W]],
  [pipeline('sobel'), blurred, edges, [W / 8, H / 8]]]) {
  pass.setPipeline(p), pass.setBindGroup(0, device.createBindGroup({
    layout: p.getBindGroupLayout(0), entries: [from.createView(), to.createView()]
      .map((resource, binding) => ({ binding, resource })) }));
  pass.dispatchWorkgroups(...groups);                // three dispatches, one pass
}
pass.end();
const context = out.getContext('webgpu');
context.configure({ device, format: 'rgba8unorm', usage: tex.COPY_DST });
const canvas = context.getCurrentTexture();
[cover, blurred, edges].forEach((texture, i) => encoder.copyTextureToTexture(
  { texture }, { texture: canvas, origin: [i * W, 0] }, [W, H]));
device.queue.submit([encoder.finish()]);
await device.queue.onSubmittedWorkDone();
window.__done = true;
</script>
Browser output of Listing 4.97
Browser output of 97

A read-back of the blurred texture differed from the same two-pass Gaussian computed in JavaScript by at most 1/255 per channel, the rounding of the intermediate texture. All six covers would take one texture_2d_array and a dispatch depth of 6.