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.

loading core…

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

ItemWhat it exercises
Array sweepEach frame scans the full 896-byte heat array (flat-index read + variable-offset write) — lda/sta (zp),y traffic, the demo’s hottest loop
PRNGA 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-freeNo __mulsi3 / __udivmodsi4 in the per-cell loop — the stress is indexed array bandwidth + the PRNG, not the ALU libcalls
CGRAMHeat 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.