Task 4 of 6
The moment a gather reads a neighbor, the edges bite. Take the forward
difference — out[i] = signal[i+1] − signal[i], "how much does the signal jump
here?". Thread 63 asks for signal[64], which does not exist.
What comes back is whatever the backend decides — and the three this
course runs on decide three different things. Run the starter, then switch
Mode and run it again: the CPU backend gives you NaN, WebGL
gives you a garbage texel from elsewhere in the texture, and WebGPU quietly
clamps the index and gives you signal[63] — a perfectly plausible
number that you never asked for. That last one is the dangerous one: nothing looks wrong,
so nothing gets fixed.
Reading off the end isn't wrong, it is undefined: every
platform is free to answer differently, and they do. So never rely on the read. Decide what
the edge means, and write that down. The usual convention — the one every image
filter uses — is replicate: the last cell repeats the last real
difference, signal[63] − signal[62]. You get it by clamping the index you
start from, so the pair you read is always a pair that exists.
signal[63] − signal[62] — instead of depending on what this backend happens to
do with a read past the end.Math.min(this.thread.x, this.constants.n - 2)signal[i+1] − signal[i]Only thread 63 misbehaves: this.thread.x + 1 is 64, one past the
end. Every other thread's pair is fine, so the fix has to leave 0 … 62 exactly as they
are and hand 63 a pair that exists.
Careful which index you pin. Clamping the neighbor —
Math.min(this.thread.x + 1, n - 1) — makes thread 63 read itself twice and
return 0, which is the answer WebGPU was already inventing for you. Clamp the
base instead.
const i = Math.min(this.thread.x, this.constants.n - 2);
return signal[i + 1] - signal[i];
For thread 63, i is 62, so the answer is
signal[63] - signal[62] — the last real jump, repeated. Cells 62 and 63
come back holding the same number, which is exactly what "replicate" means.
clamp-to-edge address mode in WebGPU and Metal,
cudaAddressModeClamp on CUDA texture objects, with repeat and
mirror sitting beside them as the alternatives. Reading a raw buffer instead
of a texture? Then you pick the convention by hand, exactly like here — and you do
pick one, because an unguarded read past the end is undefined everywhere: CUDA will happily
hand you another allocation's memory, and WGSL leaves out-of-range buffer access loose
enough that two implementations can disagree. The three answers you just got from three
backends are that fact, one level up.
This page is an interactive exercise — the editor, the GPU runner and your saved progress need JavaScript. The text above is the full brief.