# toArray() Is a Tollbooth

*Task 3 of 5 · [Pipelines & Textures](https://gpu.rocks/learn/pipelines-and-textures-9f4aeaa5.md) · GPU.js Learn*

Here's the mental model that makes GPU code fast: computation on the card is
nearly free — it's the **transfers** that cost. Every kernel that is
*not* `pipeline: true` ends with an implicit download, and passing
that array to the next kernel triggers a re-upload. A three-stage chain without
pipelines pays the toll **four times** for one result.

The starter below is a fully working three-stage audio chain — normalize, gamma,
smooth — and every hop goes through JavaScript. Your job isn't to fix the math.
It's to fix the traffic: intermediates become pipeline kernels, and only the
*final* stage returns plain numbers. The chain call itself shouldn't change
by a single character.

## Goal

**Goal:** refactor the chain so stages 1 and 2 keep their results on
the GPU, the final stage returns numbers, and the output is bit-for-bit the same idea —
just without the round trips.

## Requirements

- Make `normalize` and `gamma` pipeline kernels
- Leave `smooth` as a plain kernel — the one download you actually want
- Do not change the chain: `await smooth(await gamma(await normalize(signal)))` stays as-is

## Hint 1 — where is the readback hiding?

There's no `.toArray()` in the starter, but the readbacks are
still there: a non-pipeline kernel's *awaited return value* is the readback.
Count them: normalize downloads, gamma re-uploads and downloads, smooth re-uploads.

## Hint 2 — a two-line diff

Add `pipeline: true` to the settings of `normalize`
and `gamma`. That's the entire refactor — the chain line already does
the right thing once textures flow through it.

## Same idea elsewhere

Profile any real CUDA or ROCm app and the widest bars are often
`cudaMemcpy` DtoH/HtoD, not kernels; in WebGPU the same toll is
`mapAsync` plus staging-buffer copies. "Keep data resident, read back once
at the end" is performance rule number one on every GPU platform.

## Starter code

```js
const gpu = new GPU({ mode });

// Stage 1 — scale the raw 0–10 signal down to 0–1.
const normalize = gpu.createKernel(function (signal) {
  return signal[this.thread.x] / 10;
}, { output: [256] }); // TODO: this intermediate should stay on the GPU

// Stage 2 — gamma curve to tame the loud parts.
const gamma = gpu.createKernel(function (v) {
  return v[this.thread.x] * v[this.thread.x];
}, { output: [256] }); // TODO: so should this one

// Stage 3 — 3-tap smoothing. Final stage: plain numbers out, on purpose.
const smooth = gpu.createKernel(function (v) {
  let left = this.thread.x - 1;
  let right = this.thread.x + 1;
  if (left < 0) left = 0;
  if (right > 255) right = 255;
  return (v[left] + v[this.thread.x] + v[right]) / 3;
}, { output: [256] });

// This chain is CORRECT — and slow. Each non-pipeline return is a full
// GPU → JS download, and the next call re-uploads it. Four transfers.
const out = await smooth(await gamma(await normalize(signal)));
console.log('smoothed[0]:', out[0]);
```

---

Interactive version: https://gpu.rocks/learn/pipelines-and-textures-9f4aeaa5/3

[Previous task](https://gpu.rocks/learn/pipelines-and-textures-9f4aeaa5/2.md) · [Next task](https://gpu.rocks/learn/pipelines-and-textures-9f4aeaa5/4.md)
