# Randomness Without a Random Number Generator

*Task 2 of 6 · [The Ising Model: Colour to Break the Race](https://gpu.rocks/learn/ising-model-1f12d841.md) · GPU.js Learn*

The rule that makes this a *model of temperature* rather than a downhill slide
needs a coin toss per spin per sweep. On a CPU you reach for `Math.random()` without
thinking. Think about it here: an ordinary generator is a **stream** — one hidden
state, advanced once per call, so call number 900 depends on call number 899. That is a
sequential dependency, and it is the same one Thinking in Parallel rules out. There is no
ordering between 16,384 threads and nothing they can share. gpu.js is blunt about it too: the
WebGPU backend *refuses* `Math.random` at compile time, and on WebGL a kernel
that calls it is no longer reproducible.

So stop asking for a stream and ask for a **function**:
`u = hash(x, y, seed)`. Same thread, same sweep, same number — every time, on every
backend. Different thread, unrelated number. No state, no ordering, and a run you can replay
exactly, which is what makes a stochastic simulation debuggable at all. This is not a
compromise for gpu.js's benefit; it is what production GPU Monte Carlo does.

A hash has one job: make the output look nothing like the input. Three moves do it. The
*fold* multiplies `x`, `y` and `seed` by unrelated odd
constants and adds them, which spreads the three inputs across sixteen bits. The
*squaring* step is the only nonlinear one, and without it a hash is just an affine map:
neighbouring cells would come out a fixed distance apart, which is not randomness, it is a
ramp. The *byte swap* moves the high eight bits down where the next multiply can reach
them. Every intermediate stays under `2²⁴` — the largest integer a 32-bit float
holds exactly — so WebGPU, WebGL and the CPU compute the identical value and nothing about your
run depends on which one you got.

## Goal

**Goal:** fill a 128×128 grid with `hash(x, y, seed)` in
`[0, 1)`, using no `Math.random` anywhere.

## Requirements

- Fold `x`, `y` and `seed` into one value below `65536`
- Mix it twice: square a low slice, swap the two bytes, one multiply-and-add round
- Return the mixed value divided by `65536` so it lands in `[0, 1)`
- No `Math.random` — the same seed must give the same field every run

## Hint 1 — the fold, and why the seed belongs in it

Three odd constants, one modulo:

```js
let h = (x * 1103 + y * 2749 + seed * 3571) % 65536;
```

Leave `seed` out and the field is the same 16,384 numbers on every sweep — the
simulation would take one step and then repeat it forever. The seed is what makes this a
*sequence* of fields rather than one field.

## Hint 2 — the two mixing moves

Squaring is the nonlinear part. `h % 2048` keeps the value small enough
that `q * q` stays under `2²⁴` and the arithmetic is still exact:

```js
let q = h % 2048;
h = (h + q * q) % 65536;
```

The swap exchanges the high and low bytes. Note which side gets the multiply —
`h % 256` is the LOW byte, so it is the one that has to move UP:

```js
h = (h % 256) * 256 + Math.floor(h / 256);
```

## Hint 3 — the whole body

```js
let h = (x * 1103 + y * 2749 + seed * 3571) % 65536;
let q = h % 2048;
h = (h + q * q) % 65536;
h = (h % 256) * 256 + Math.floor(h / 256);
h = (h * 253 + 30011) % 65536;
q = h % 2048;
h = (h + q * q) % 65536;
return h / 65536;
```

Two rounds, not one — and the reason is not where you would look for it. Inside a
single field one round is already fine: neighbouring cells measure `0.021`
against the two-round hash's `0.032`, which is the same noise. The damage is
*between consecutive seeds*. One squaring plus one multiply is still so nearly
affine that `hash(x, y, k)` and `hash(x+1, y+1, k+1)` correlate at
`0.30`, against `0.007`–`0.021` with two rounds — and that is precisely the
pair the next task puts side by side, a red cell drawing at seed `2k` and the
black neighbour that reads it drawing at `2k + 1`.

## Same idea elsewhere

Counter-based randomness — hash a coordinate instead of advancing a stream — is the
standard on every parallel platform: Random123 / Philox in CUDA, `curand`'s
counter-based generators, and essentially every shader that needs noise. The reason is the same
everywhere: a stream forces an order on things that have none, while a hash gives every thread
an independent draw for free and makes the whole run reproducible. Keeping the arithmetic
inside the exactly-representable integer range is the other half of the trick, and it is why
real GPU hashes are written in integer types rather than floats.

## Starter code

```js
// 16,384 threads, 16,384 random numbers, and no random number generator.
const gpu = new GPU({ mode });

const noise = gpu.createKernel(function (seed) {
  const x = this.thread.x;
  const y = this.thread.y;
  // TODO 1: fold x, y and seed into one value below 65536.
  // TODO 2: mix it twice — square a low slice, swap the two bytes,
  //         one multiply-and-add round, then square again.
  // TODO 3: return the mixed value / 65536.
  return 0;
}, { output: [128, 128] });

const field = await noise(1);

// A histogram of all 16,384 values, in plain JavaScript.
const bins = new Array(32).fill(0);
let total = 0;
for (let y = 0; y < 128; y++) {
  for (let x = 0; x < 128; x++) {
    bins[Math.min(31, Math.floor(field[y][x] * 32))]++;
    total += field[y][x];
  }
}
console.log('mean of the field:', total / (128 * 128));
console.log('counts per bin:', bins);
plot({ 'cells per bin': bins }, { title: 'hash output, 32 equal bins of [0, 1)' });
```

---

Interactive version: https://gpu.rocks/learn/ising-model-1f12d841/2

[Previous task](https://gpu.rocks/learn/ising-model-1f12d841/1.md) · [Next task](https://gpu.rocks/learn/ising-model-1f12d841/3.md)
