N-body Orbits
Physics & Simulation
Sun, Earth, and Jupiter orbit each other under Newtonian gravity on a Super Nintendo.
Each pair computes a 1/r² force via Q8.8 fixed-point arithmetic; Symplectic Euler
integrates the velocities each frame. Orbital trails accumulate into a 128×128 2bpp
bitmap canvas; the palette fades periodically to dim old trails. Written in C and
compiled to a real .sfc ROM with the
llvm-mos-based 65816 toolchain
(+mos-a16, 16-bit accumulator mode). Self-running — no joypad needed.
Self-running — orbits build up automatically. Trails fade every ~4 seconds.
Click the screen to focus. Tab away and it pauses.
What it is
Three point masses — Sun (mass 255), Earth (mass 1), Jupiter (mass 3) — interact under Newtonian gravity. For each body-pair the simulator computes:
dx = (x_j − x_i) >> 8 // Q8.8 → pixel units dy = (y_j − y_i) >> 8 r² = dx² + dy² + GRAV_SOFT // softening avoids 1/0 a = GRAV_K × mass / r² // 32-bit divide → __udivsi3 v += a × dt // Symplectic Euler integration x += v × dt
Positions and velocities are int16_t in Q8.8 fixed-point (1 unit = 1/256
pixel). A softening constant (GRAV_SOFT = 16) prevents singularities at
close approach. Four substeps per V-blank frame keep the integration stable.
Display
Each body plots a 3×3 pixel dot into a 128×128 BitmapCanvas (BG3, Mode 1, 2bpp) centred in an 8×6-tile box on screen. The canvas accumulates dots by OR-ing tile data — old marks stay visible as trails. Every 120 frames the CGRAM palette entry for the trail colour dims by one level, creating a fade effect without touching VRAM. A HUD row above and below the canvas shows the physics parameters.
Codegen under test
| Operation | What it measures |
|---|---|
| __mulsi3 × 6 | Q8.8 position deltas squared for r²: (int16)dx × dx + (int16)dy × dy, 16×16→32 |
| __udivsi3 × 2 | Gravitational acceleration: GRAV_K × mass / r² — a 32-bit unsigned divide per body-pair |
| rep / sep × 82 | Mode-switch brackets under +mos-a16 marking the 16-bit accumulator sections |
The Verify fidelity button above powers on the ROM, runs 500 frames,
and reads corpus_result from WRAM — it must equal 0xCC65,
the same hash the build gate asserts against a host C oracle. Six __mulsi3
calls (three body-pairs × two axes) and two __udivsi3 calls (one r²
divide per pair) are the primary compiler stress surfaces.
Technical notes
The force-pair inner function is marked __attribute__((noinline)) to
cap register pressure — the 65816 backend's register allocator cannot handle six
simultaneous live int32_t values in a single inlined scope without
emitting undefined-register errors in the machine-verifier pass. Extracted as a
separate function, each body-pair's six live values fall within the allocatable set.
Compiler stress-test #13 in the llvm-mos-65816 battery. 5-way differential gate: host == default@MAME == +mos-a16@MAME == +mos-xy16@MAME == a16@bsnes-jg.