Task 6 of 6
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.
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.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]3blurX's output into blurY (already wired up)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.
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].
This page is an interactive exercise — the editor, the GPU runner and your saved progress need JavaScript. The text above is the full brief.