# Ping-Pong the Whole Landscape

*Task 5 of 6 · [Hydraulic Erosion: Carving Terrain by Accumulation](https://gpu.rocks/learn/hydraulic-erosion-07165ca1.md) · GPU.js Learn*

Four kernels, three fields, one hundred steps. The loop lives in JavaScript, the
grids live on the GPU, and the discipline is exactly the one Reaction–Diffusion and
Cellular Automata already drilled — with one extra wrinkle worth naming.

Each step is *ordered*: `dropTotal` runs first, because the other
three read the grid it produces. Then `moveWater`, `moveSediment`
and `erode` all read the **same snapshot** of height, water and
sediment — plus that fresh drop grid — and only when all three have returned do you swap.
Overwrite `height` early and `moveWater` starts routing over
terrain that has already been cut, which is a different simulation and a worse one.

Nothing here is pipelined: each kernel hands JavaScript an ordinary array and gets one
back. That works, and it costs a round trip per pass — Pipelines & Textures shows how
to keep the whole thing resident on the card with `pipeline: true` and
`immutable: true`, which is the production answer once the grid stops being
64×64.

## Goal

**Goal:** run `STEPS` steps of the whole model, recording the
sediment in flight and the deepest water each step, then plot both.

## Requirements

- Call `dropTotal` first each step, then the other three with the *same* height / water / sediment
- Swap all three fields only after all four kernels have returned
- Push `totalOf(sediment)` and `maxOf(water)` onto the traces each step and `plot` them

## Hint 1 — the loop body

```js
const drop = await dropTotal(height, water);
const nextWater = await moveWater(height, water, drop);
const nextSediment = await moveSediment(height, water, sediment, drop);
const nextHeight = await erode(height, water, sediment, drop);
height = nextHeight;
water = nextWater;
sediment = nextSediment;
```

Four `await`s in order. Never `Promise.all` them: three of the
four need the first one's answer.

## Hint 2 — the traces

`totalOf` and `maxOf` are written for you. After the
swap:

```js
carried.push(totalOf(sediment));
deepest.push(maxOf(water));
```

and after the loop,
`plot({ 'sediment in flight': carried }, { title: '…' })`.

## Hint 3 — what the curves should look like

Sediment climbs, bends over around step 60 as pick-up and settling come into
balance — and then, around step 80, kicks upward again: that second climb is the two
hollows turning into lakes, and it shows up in the other plot at the same moment as a
step in the deepest water. From there the deepest cell keeps climbing, with a
sawtooth on it, because a lake surface trades a little water back and forth with its
rim every step. Drag the rainfall slider and watch both curves scale.

## Same idea elsewhere

Buying quality with time instead of work per frame is the same trade a
progressive path tracer makes, and the same one a physically based fluid sim makes: the
per-step kernel is cheap and stupid, and the result comes from running it enough times.
Every one of them ping-pongs two sets of buffers, on every API there is.

## Starter code

```js
// Rain, flow, cut, settle. A hundred times.
const gpu = new GPU({ mode });

const SIZE = 64;
const STEPS = 100;
// Rain per step, in units of 10⁻⁵ of depth. Drag it and the whole run redoes itself.
const rainfall = slider('rain', { min: 0.5, max: 6, value: 2, step: 0.5, label: 'rain per step (×10⁻⁵)' });
const SHARED = { size: SIZE, move: 0.6, soft: 0.0002, carry: 60, pickUp: 0.3, settle: 0.05, maxCut: 0.004 };

const dropTotal = gpu.createKernel(function (height, water) {
  const x = this.thread.x;
  const y = this.thread.y;
  let xl = x - 1; if (xl < 0) xl = this.constants.size - 1;
  let xr = x + 1; if (xr > this.constants.size - 1) xr = 0;
  let yd = y - 1; if (yd < 0) yd = this.constants.size - 1;
  let yu = y + 1; if (yu > this.constants.size - 1) yu = 0;
  const here = height[y][x] + water[y][x];
  let total = 0;
  let d = here - (height[y][xl] + water[y][xl]); if (d > 0) total += d * d;
  d = here - (height[y][xr] + water[y][xr]); if (d > 0) total += d * d;
  d = here - (height[yd][x] + water[yd][x]); if (d > 0) total += d * d;
  d = here - (height[yu][x] + water[yu][x]); if (d > 0) total += d * d;
  return total;
}, { output: [SIZE, SIZE], constants: SHARED });

const moveWater = gpu.createKernel(function (height, water, drop) {
  const x = this.thread.x;
  const y = this.thread.y;
  let xl = x - 1; if (xl < 0) xl = this.constants.size - 1;
  let xr = x + 1; if (xr > this.constants.size - 1) xr = 0;
  let yd = y - 1; if (yd < 0) yd = this.constants.size - 1;
  let yu = y + 1; if (yu > this.constants.size - 1) yu = 0;
  const here = height[y][x] + water[y][x];
  let taken = 0;
  let d = height[y][xl] + water[y][xl] - here;
  if (d > 0) taken += (d * d * water[y][xl]) / (drop[y][xl] + this.constants.soft);
  d = height[y][xr] + water[y][xr] - here;
  if (d > 0) taken += (d * d * water[y][xr]) / (drop[y][xr] + this.constants.soft);
  d = height[yd][x] + water[yd][x] - here;
  if (d > 0) taken += (d * d * water[yd][x]) / (drop[yd][x] + this.constants.soft);
  d = height[yu][x] + water[yu][x] - here;
  if (d > 0) taken += (d * d * water[yu][x]) / (drop[yu][x] + this.constants.soft);
  const given = (drop[y][x] * water[y][x]) / (drop[y][x] + this.constants.soft);
  return water[y][x] + this.constants.move * (taken - given) + this.constants.rain;
}, { output: [SIZE, SIZE], constants: { ...SHARED, rain: rainfall * 0.00001 } });

const moveSediment = gpu.createKernel(function (height, water, sediment, drop) {
  const x = this.thread.x;
  const y = this.thread.y;
  let xl = x - 1; if (xl < 0) xl = this.constants.size - 1;
  let xr = x + 1; if (xr > this.constants.size - 1) xr = 0;
  let yd = y - 1; if (yd < 0) yd = this.constants.size - 1;
  let yu = y + 1; if (yu > this.constants.size - 1) yu = 0;
  const here = height[y][x] + water[y][x];
  let taken = 0;
  let d = height[y][xl] + water[y][xl] - here;
  if (d > 0) taken += (d * d * sediment[y][xl]) / (drop[y][xl] + this.constants.soft);
  d = height[y][xr] + water[y][xr] - here;
  if (d > 0) taken += (d * d * sediment[y][xr]) / (drop[y][xr] + this.constants.soft);
  d = height[yd][x] + water[yd][x] - here;
  if (d > 0) taken += (d * d * sediment[yd][x]) / (drop[yd][x] + this.constants.soft);
  d = height[yu][x] + water[yu][x] - here;
  if (d > 0) taken += (d * d * sediment[yu][x]) / (drop[yu][x] + this.constants.soft);
  const given = (drop[y][x] * sediment[y][x]) / (drop[y][x] + this.constants.soft);
  const capacity = this.constants.carry * Math.sqrt(drop[y][x]) * water[y][x];
  const load = sediment[y][x];
  let delta = -this.constants.settle * (load - capacity);
  if (load < capacity) delta = Math.min(this.constants.pickUp * (capacity - load), this.constants.maxCut);
  return sediment[y][x] + this.constants.move * (taken - given) + delta;
}, { output: [SIZE, SIZE], constants: SHARED });

const erode = gpu.createKernel(function (height, water, sediment, drop) {
  const x = this.thread.x;
  const y = this.thread.y;
  const capacity = this.constants.carry * Math.sqrt(drop[y][x]) * water[y][x];
  const load = sediment[y][x];
  let delta = -this.constants.settle * (load - capacity);
  if (load < capacity) delta = Math.min(this.constants.pickUp * (capacity - load), this.constants.maxCut);
  return height[y][x] - delta;
}, { output: [SIZE, SIZE], constants: SHARED });

function blank() {
  const grid = [];
  for (let y = 0; y < SIZE; y++) grid.push(new Array(SIZE).fill(0));
  return grid;
}
function totalOf(grid) {
  let sum = 0;
  for (let y = 0; y < SIZE; y++) for (let x = 0; x < SIZE; x++) sum += grid[y][x];
  return sum;
}
function maxOf(grid) {
  let top = 0;
  for (let y = 0; y < SIZE; y++) for (let x = 0; x < SIZE; x++) if (grid[y][x] > top) top = grid[y][x];
  return top;
}

let height = terrain;
let water = blank();
let sediment = blank();
const carried = [];
const deepest = [];

// TODO: run STEPS steps. Each one is dropTotal FIRST, then the other three
// reading the same height / water / sediment, then a swap of all three.
// Push totalOf(sediment) and maxOf(water) after every swap.

plot({ 'sediment in flight': carried }, { title: 'sediment the streams are carrying' });
plot({ 'deepest water': deepest }, { title: 'deepest cell on the map' });
```

---

Interactive version: https://gpu.rocks/learn/hydraulic-erosion-07165ca1/5

[Previous task](https://gpu.rocks/learn/hydraulic-erosion-07165ca1/4.md) · [Next task](https://gpu.rocks/learn/hydraulic-erosion-07165ca1/6.md)
