Doom Fire
Cellular Automata
The classic PSX-Doom fire effect
running live on a Super Nintendo. A 32×28 grid of heat values is anchored by a constant
white-hot source row along the bottom; every frame each cell inherits the cell below it
minus a random decay, with a random horizontal drift — so flames rise and flicker.
The heat (0–15) indexes a black→red→orange→yellow→white palette. Written in C and compiled
with the llvm-mos-based 65816 toolchain
(+mos-a16, 16-bit accumulator). Runs automatically.
Self-running demo — flames rise automatically from the source row.
Click the screen to focus, then watch. Tab away and it pauses.
What it is
The Doom fire (popularised by Fabien Sanglard's write-up of the PlayStation port's title screen) is a wonderfully cheap fire simulation. There is no physics — just a heat grid and a pseudo-random sweep:
for src in W .. W*H-1: # every cell above the source row
pixel = fire[src]
if pixel == 0: fire[src-W] = 0
else:
rnd = xorshift16() & 3 # 0..3
dst = src - W + 1 - rnd # row above, drift {+1,0,-1,-2}
fire[dst] = pixel - (rnd & 1)
The bottom row is pinned at maximum heat. Cooling is the random − (rnd & 1)
decay; the random horizontal dst offset gives the flames their lateral
flicker. The whole effect is a single in-place pass over an 896-byte uint8_t
array — no buffers, no multiplies, no divides.
Why it stresses the compiler
Most demos in this battery lean on 32-bit multiply/divide libcalls. Doom-fire is the
opposite: it is deliberately multiply- and divide-free in the hot loop, so it
exercises the parts of the backend the arithmetic demos don't —
indexed 8-bit array traffic (lda/sta (zp),y over a variable write
offset) and a 16-bit xorshift PRNG that lowers to native eor/
asl/lsr inside a single rep/sep mode
bracket under +mos-a16.
Compiler stress-test
| Item | What it exercises |
|---|---|
| Array sweep | Each frame scans the full 896-byte heat array (flat-index read + variable-offset write) — lda/sta (zp),y traffic, the demo’s hottest loop |
| PRNG | A 16-bit xorshift per non-zero cell drives the decay + horizontal drift — native eor/asl/lsr inside one rep/sep bracket under +mos-a16 |
| Multiply-free | No __mulsi3 / __udivmodsi4 in the per-cell loop — the stress is indexed array bandwidth + the PRNG, not the ALU libcalls |
| CGRAM | Heat 0..15 indexes a 16-colour black→red→orange→yellow→white ramp, uploaded to CGRAM; the palette is the picture |
A separate 16×16 gate grid runs 30 sweeps and folds the whole field into a rotating-XOR
CRC after each step. The gate asserts corpus_result (0x3C59) is identical on
host, default 8-bit SNES, +mos-a16, and +mos-xy16. The
Verify fidelity button above reproduces that gate live in this tab via the same
cycle-accurate bsnes-jg core used in CI.
Written in C with the llvm-mos 65816 toolchain. Hit Verify fidelity
to reproduce the build gate's WRAM assert (corpus_result = 0x3C59) live in
this tab. No far pointers — the same logic checked four ways: host == default ==
+mos-a16 == +mos-xy16, with bsnes-jg as the in-browser
confirmation.