# The Payoff: Photo to Screen, Zero Readbacks

*Task 5 of 5 · [Pipelines & Textures](https://gpu.rocks/learn/pipelines-and-textures-9f4aeaa5.md) · GPU.js Learn*

Time to cash in the whole module. In the finale of **Data In, Data Out**,
a two-kernel chain
hauled the luminance map down to JavaScript and back up again — two transfers it didn't
need. This pipeline does more work with *fewer* transfers: photo →
**luminance** → **3×3 blur** → **painted canvas**,
and after the photo is uploaded, nothing comes back. The graphical kernel eats the blur
texture and writes pixels; readbacks: zero.

The missing piece is the blur. Each cell averages its 3×3 neighbourhood — two little
loops over `dy`/`dx`, indices clamped to 0…63 so the edges don't
read out of bounds. When it works, hit **Benchmark** and watch what
keeping data on the card does to the gap.

**Array layout in gpu.js**
Image data comes in row-major: `image[y][x]` is the pixel in row *y*,
column *x*, and each pixel is an `[r, g, b, a]` array with channels from
0 to 1. Mind the inversion that catches everyone — sizes are given width-first
(`output: [width, height]`), but indexing runs row-first, so this thread's own
pixel is `image[this.thread.y][this.thread.x]`. Swap those two and you read the
transpose of your image. Three-dimensional data follows the same rule:
`output: [w, h, d]` is indexed `[z][y][x]`.

## Goal

**Goal:** implement the 3×3 box blur so the full three-pass pipeline —
two texture passes and a graphical finale — runs with zero readbacks.

## Requirements

- Blur: average the 3×3 neighbourhood, clamping indices to 0…63 at the edges
- Both `luminance` and `blur` stay `pipeline: true`
- The graphical pass is fed the blur *texture* — nothing is downloaded
- Render the result with `render(paint.canvas)`

## Hint 1 — the neighbourhood loops

Two nested loops with fixed bounds are fine in a kernel:
`for (let dy = -1; dy <= 1; dy++)` and the same for `dx`.
Accumulate into a `sum`, return `sum / 9`.

## Hint 2 — clamping the edges

Compute `let yy = this.thread.y + dy;` then push it back in
range:

```js
if (yy < 0) yy = 0;
if (yy > 63) yy = 63;
```

Same for `xx`. Corner cells just count some neighbours twice.

## Hint 3 — the whole body

`let sum = 0;` then inside the loops
`sum += map[yy][xx];` and finally `return sum / 9;` —
the clamped `yy`/`xx` from hint 2 do the rest.

## Same idea elsewhere

You just built what engine programmers call a render graph: named passes,
explicit dependencies, all resources resident on the GPU — the architecture behind
Frostbite's frame graph, CUDA Graphs' pre-recorded launch chains, and a Metal command
buffer full of encoder passes. Real engines are this task with more boxes.

## Starter code

```js
const gpu = new GPU({ mode });

// Pass 1 — luminance map. You've written this one twice already.
const luminance = gpu.createKernel(function (photo) {
  const pixel = photo[this.thread.y][this.thread.x];
  return 0.299 * pixel[0] + 0.587 * pixel[1] + 0.114 * pixel[2];
}, { output: [64, 64], pipeline: true });

// Pass 2 — 3×3 box blur. Currently a do-nothing passthrough.
const blur = gpu.createKernel(function (map) {
  // TODO: average the 3×3 neighbourhood around this cell.
  // Clamp indices to 0…63 so edges don't read out of bounds.
  return map[this.thread.y][this.thread.x];
}, { output: [64, 64], pipeline: true });

// Pass 3 — paint the blurred map. Texture in, pixels out.
const paint = gpu.createKernel(function (map) {
  const l = map[this.thread.y][this.thread.x];
  this.color(l, l, l, 1);
}, { output: [64, 64], graphical: true });

// The whole pipeline: after `photo` goes up, nothing comes back down.
await paint(await blur(await luminance(photo)));
render(paint.canvas);
```

---

Interactive version: https://gpu.rocks/learn/pipelines-and-textures-9f4aeaa5/5

[Previous task](https://gpu.rocks/learn/pipelines-and-textures-9f4aeaa5/4.md)
