# The Two-Pass Blur

*Task 6 of 6 · [Thinking in Parallel](https://gpu.rocks/learn/thinking-in-parallel-c3876efb.md) · GPU.js Learn*

The payoff. A 3×3 box blur of a 2D grid needs nine reads per cell — but the box
blur is **separable**: blurring horizontally and then blurring that result
vertically gives the *identical* answer with just three reads per cell per pass.
Bigger blurs win bigger: a 9×9 blur drops from 81 reads to 18.

This is also how you design around the no-communication rule at scale: since threads
can't share work *within* a pass, you split the algorithm into passes — each pass
a clean parallel gather, each handoff a finished grid. Kernel one blurs along
`x`; its output feeds kernel two, which blurs along `y`. Both are
3-tap clamped stencils — task 5, twice, at right angles.

## Goal

**Goal:** finish both kernels — `blurX` averages each cell
with its left/right neighbors, `blurY` with its up/down neighbors — edges
clamped, so the composition equals a full 3×3 box blur.

## Requirements

- `blurX`: 3-tap average along the row — clamp `x + d − 1`, read `grid[this.thread.y][j]`
- `blurY`: 3-tap average down the column — clamp `y + d − 1`, read `grid[j][this.thread.x]`
- Both kernels divide their sum by `3`
- Feed `blurX`'s output into `blurY` (already wired up)

## Hint 1 — task 5, rotated

Each kernel is the moving-average pattern with a 3-wide window. The only new
move: in 2D you clamp the coordinate along the blur axis and keep the other
coordinate fixed.

## Hint 2 — the x pass

```js
for (let d = 0; d < 3; d++) {
  const j = Math.max(0, Math.min(this.constants.n - 1, this.thread.x + d - 1));
  sum += grid[this.thread.y][j];
}
return sum / 3;
```

The y pass swaps which coordinate is clamped: `grid[j][this.thread.x]`.

## Same idea elsewhere

Separable filtering is a classic GPU optimization you'll meet everywhere: game
engines render Gaussian blurs as two fullscreen passes, WebGPU and Metal chain compute
encoder passes the same way, and CUDA image pipelines launch one kernel per axis. Two
cheap 1D passes beating one fat 2D pass — O(k) taps instead of O(k²) — never stops
being true.

## Starter code

```js
// Two passes at right angles = one 3×3 box blur, for 6 reads instead of 9.
const gpu = new GPU({ mode });

const blurX = gpu.createKernel(function (grid) {
  // TODO: average grid[y][x-1], grid[y][x], grid[y][x+1] — clamp x
  return grid[this.thread.y][this.thread.x];
}, { output: [48, 48], constants: { n: 48 } });

const blurY = gpu.createKernel(function (grid) {
  // TODO: average grid[y-1][x], grid[y][x], grid[y+1][x] — clamp y
  return grid[this.thread.y][this.thread.x];
}, { output: [48, 48], constants: { n: 48 } });

const pass1 = await blurX(heightmap);
const smooth = await blurY(pass1);
console.log('corner before → after:', heightmap[0][0], '→', smooth[0][0]);
```

---

Interactive version: https://gpu.rocks/learn/thinking-in-parallel-c3876efb/6

[Previous task](https://gpu.rocks/learn/thinking-in-parallel-c3876efb/5.md)
