Task 5 of 5
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.
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].
luminance and blur stay pipeline: truerender(paint.canvas)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.
Compute let yy = this.thread.y + dy; then push it back in
range:
if (yy < 0) yy = 0;
if (yy > 63) yy = 63;
Same for xx. Corner cells just count some neighbours twice.
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.
This page is an interactive exercise — the editor, the GPU runner and your saved progress need JavaScript. The text above is the full brief.