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:
<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>
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.