Task 1 of 5
On a CPU, filtering is four words: data.filter(v => v >= 50).
Underneath it is a loop with a moving write cursor — every element that
passes gets pushed at wherever the last one left off:
const kept = [];
for (const v of data) {
if (v >= 50) kept.push(v); // ← push() knows where the cursor is
}
That cursor is the problem. Thread 7 can tell you instantly whether
data[7] survives. It cannot tell you where it goes, because that
depends on how many of elements 0…6 survived — six other threads' business, none of which
thread 7 is allowed to ask about. An output position that depends on other threads is not
something a single kernel can compute, which is why there is no filter
kernel and never will be.
So compaction gets built out of pieces, and the first piece is the one part that is perfectly independent: the flag pass. Turn the predicate into a mask of 1s and 0s — a plain map, one thread per element, nobody talking to anybody. Run it and look at what you get: the ones and zeros line up under the input, holes and all. Flags say who survives. They move nothing.
1 when this thread's sample is at or above
this.constants.threshold, and 0 when it is not.output: [64], no loops1 or 0, never the sample value50 survivesRead your own element and compare it — nothing else. The comparison
samples[this.thread.x] >= this.constants.threshold is the whole
decision; all that is left is turning it into a number.
return samples[this.thread.x] >= this.constants.threshold ? 1 : 0;DeviceSelect::Flagged takes a flags array
beside the data, Thrust's copy_if takes the predicate and builds the same
mask internally, and a WebGPU pipeline writes it to a storage buffer with one dispatch.
The predicate pass is the cheap, embarrassingly parallel part — everything after it is
the interesting problem.
This page is an interactive exercise — the editor, the GPU runner and your saved progress need JavaScript. The text above is the full brief.