# Dice Every Thread Can Roll

*Task 1 of 5 · [Progressive Path Tracing: Noise Melting Into an Image](https://gpu.rocks/learn/path-tracing-c99efc67.md) · GPU.js Learn*

A path tracer needs random numbers by the million — every bounce picks a direction
out of a hat. On a CPU you call `Math.random()` and never think about it again.
On a GPU you can't, for two separate reasons. gpu.js's WebGPU backend simply refuses to
compile a kernel containing `Math.random` (it is one of exactly three things that
make a kernel decline the upgrade and stay on WebGL). And even where it compiles, 4,096
threads drawing from *one* generator is a fiction: there is no shared state on a GPU
and no defined order in which the threads would take their turns.

So the whole industry does the same thing instead: **the thread's own coordinates
are the seed**. Hash `this.thread.x`, `this.thread.y` and a
frame counter into a starting state, then walk that state forward. Every thread gets a
private stream, nothing is shared, and — because a hash is a pure function — the same run
produces the same picture every single time.

The generator is written for you. Notice how small the numbers stay:

```js
gpu.addFunction(function nextRandom(state) {
  let p = state * 78.233 + 0.7213;
  p = p - Math.floor(p);   // wrap into 0…1
  p = p * (p + 61.7);
  return p - Math.floor(p);
});
```

That restraint is deliberate. A GPU float has 24 bits of mantissa, and taking the
fractional part of a big number throws away exactly the bits the integer part is using — so
the famous `fract(sin(x) * 43758.5453)` one-liner, whose intermediate reaches
40,000, has about eight bits of randomness left by the time you see it. Keep every value you
wrap under a hundred and you keep seventeen.

## Figures

- **no shared dice on a GPU — so the thread hashes its own address into its own**

## Goal

**Goal:** build this thread's seed out of `frame`,
`this.thread.x` and `this.thread.y`, so that no two threads and no two
frames ever share a stream.

## Requirements

- The seed must depend on all three of `frame`, `this.thread.x` and `this.thread.y`
- Run the mix through `nextRandom` — a raw sum of coordinates is not a random number
- No `Math.random`: pressing ▶ Run twice must produce the identical static

## Hint 1 — what is wrong with the starter

Run it. Every pixel is the same shade, because every thread computed the same
seed from the same constant. A seed that does not mention `this.thread.x` is
a seed 4,096 threads agree on.

## Hint 2 — stir one coordinate at a time

Add a coordinate, hash, add the next, hash again. Each hash smears whatever
went in across the whole 0…1 range, so two seeds that started 0.0713 apart end up with
nothing in common:

```js
let seed = 0.1237 + frame * 0.0173;
seed = nextRandom(seed + this.thread.x * 0.0713);
seed = nextRandom(seed + this.thread.y * 0.0917);
```

The constants are arbitrary; what matters is that all three numbers get in.

## Hint 3 — why the frame belongs in there

Leave `frame` out and every frame draws the same numbers. The next
tasks average frames together to kill noise — and averaging a value with a perfect copy
of itself removes nothing at all.

## Same idea elsewhere

Counter-based, per-thread generators are how every production GPU renderer does
this: CUDA's cuRAND is initialised as `curand_init(seed, thread_id, 0, &state)`,
and WebGPU and Metal shaders hash `global_invocation_id` with a frame index using
exactly this shape. The reason is always the same one — a shared stream needs shared state
and an ordering, and a GPU offers neither.

## Starter code

```js
// Static, on purpose: 4,096 threads each rolling their own die.
const gpu = new GPU({ mode });

// The whole random-number generator. It is a pure function: same state in,
// same number out, on every backend and every run.
gpu.addFunction(function nextRandom(state) {
  let p = state * 78.233 + 0.7213;
  p = p - Math.floor(p);
  p = p * (p + 61.7);
  return p - Math.floor(p);
});

const dice = gpu.createKernel(function (frame) {
  // TODO: build THIS thread's seed. It has to depend on the frame, on
  // this.thread.x and on this.thread.y — otherwise threads share a stream.
  let seed = 0.1237;
  return nextRandom(seed);
}, { output: [64, 64] });

const paint = gpu.createKernel(function (values) {
  const v = values[this.thread.y][this.thread.x];
  this.color(v, v, v, 1);
}, { output: [64, 64], graphical: true });

// Four frames of static. Consecutive render() calls collapse into one
// scrubber, so you can drag between them and watch the dice re-roll.
for (let f = 0; f < 4; f++) {
  await paint(await dice(f));
  render(paint.canvas);
}

const first = await dice(0);
let mean = 0;
for (let y = 0; y < 64; y++) {
  for (let x = 0; x < 64; x++) mean += first[y][x];
}
console.log('mean of 4,096 draws:', mean / 4096, '— should sit near 0.5');
```

---

Interactive version: https://gpu.rocks/learn/path-tracing-c99efc67/1

[Next task](https://gpu.rocks/learn/path-tracing-c99efc67/2.md)
