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.

loading core…

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

OperationWhat it measures
__mulsi3 × 6Q8.8 position deltas squared for r²: (int16)dx × dx + (int16)dy × dy, 16×16→32
__udivsi3 × 2Gravitational acceleration: GRAV_K × mass / r² — a 32-bit unsigned divide per body-pair
rep / sep × 82Mode-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.