Neo6502 › Articles › AsteroNeo
Porting: from Astéroric to AsteroNeo
Same game, same C, another machine: what changes when you move from a 1 MHz 6502 and a home-made bitmap to a 6.25 MHz 65C02 whose RP2040 draws the lines.
Astéroric is our study clone of Asteroids (Atari, 1979) for the 48K Oric-1: the arcade ROM rev 4 logic in cc65 C, a home-made XOR vector renderer, 25 Hz in 20,000 cycles per frame — the previous article tells the forty phases it took. AsteroNeo is its port to Olimex's Neo6502: a real W65C02S at 6.25 MHz, an RP2040 standing in for the chipset, and an API at $FF00. Framing decisions: a dedicated repository, upstream firmware in mode 0 (320×240, 256 colours), full-screen playfield, cc65 C for the logic and ca65 for the API layer, 30 Hz, XOR rendering through the API, Phosphoneo as the test oracle and the official neo emulator for smoke tests. Two sprints later the game is playable in emulation; here is what changed, and what pushed back.
Oric-1 → Neo6502, in one table
| Oric-1 (Astéroric) | Neo6502 (AsteroNeo) |
|---|---|
| HIRES 240×200 mono, home-made XOR Bresenham | mode 0 320×240, API XOR lines and pixels (5,1 / 5,2 / 5,5) |
| asteroids as pre-rendered bitmaps (blit) | polygons derived from the arcade shapes, smoothed (24/16/10 vertices), drawn by the RP2040 |
| keyboard: VIA/PSG matrix | Key Status by HID code (1,2), five remappable actions |
| AY-3-8912 under Timer 1 IRQ | 4-channel square/noise generator, note queues (8,7) |
| VSync/Timer 1, 25 Hz | 60 Hz frame counter (5,37), 30 Hz game step |
| 8-bit coordinates | int coordinates (320 > 255), 8.8 integration with carry |
1. What does not move
The game logic — waves, asteroid fragmentation, the two saucers' AI, hyperspace, high scores — is kept as is: it is the transposition of the arcade disassembly and depends on no hardware. Astéroric's seven C modules (game, asteroids, ufo, hud, font, title, keys) move from one machine to the other. Everything that touched the Oric hardware — HIRES, VIA, AY — is replaced by a four-file Neo6502 layer: neo_gfx.s (drawing), neo_time.c (timing), neo_input.c (keyboard), neo_sound.c (sound).
2. Drawing: from a home-made Bresenham to the firmware's lines
On the Oric a line cost ~97 cycles per pixel, and that is what dictated 25 Hz. On the Neo6502 the line is drawn by the RP2040: the 65C02 stores the coordinates and calls function 5,2. Measured in co-simulation on the real firmware (Phosphoneo + libemul), a line costs ≈ 22 65C02 cycles (544 ARM steps), a pixel ≈ 5 cycles — whatever the length. The budget changes nature: you no longer count pixels, you count calls.
The subtlety is elsewhere. The firmware's line (EFLA algorithm) is half-open: it draws [P0, P1[ and excludes its end point. In XOR, a polygon drawn segment by segment then loses every other vertex, or lights it twice. neo_gfx.s turns it into two primitives: a closed line (line + final pixel) and an "open" line ]P0, P1], drawn backwards, so that each vertex is XORed exactly once. The t_xor test program checks idempotence: drawing a polygon twice leaves the screen blank.
3. Coordinates: 320 does not fit in a byte
Astéroric lived in 8 bits: 240 columns, everything fitted in a register. At 320 pixels wide, positions become int, and the 8.8 fixed-point velocity integration is done with carry (phys.c). Toroidal collision and wraparound by duplication (an object straddling an edge is drawn twice) are rewritten on 320×240 — and the spawn, wrap and HUD constants adapted to the full-screen playfield.
4. Timing: 30 Hz on 60 Hz frames
No more VSync or Timer 1: the firmware exposes a 60 Hz frame counter (5,37). The game step is set to 30 Hz — two frames — and every constant calibrated for 25 Hz is recomputed. With the measured latency, sprint 1 held the pace at 3 late steps out of 255.
Then the playtest asked for rounder asteroids: the Atari silhouettes smoothed by gen_shapes.py go from 11-13 to 24/16/10 vertices. The C loop chaining the segments cost ~1,500 cycles per segment and the game fell to 200 late steps out of 255. The polygon drawing is rewritten in assembly (poly_xor: per-segment clipping, half-open segments): 2 late steps out of 255. On this machine the bottleneck is not the pixel, it is the C wrapping around each call.
5. Keyboard and sound
The Oric's VIA/PSG matrix gives way to the state of each key by HID code (1,2). The five actions — rotate, thrust, hyperspace, fire, quit — can be remapped to any named key from the CONTROLS screen; the mapping lives in RAM. Sound leaves the AY and its IRQ for the firmware's 4-channel generator (8,7): each Astéroric effect is recreated as a square/noise note queue, jingle included. With no hardware envelope, decays are volume staircases; validated by ear in neo on 16 September — on an emulator, not on a board.
6. Two points of attention in the toolchain
cc65 2.19 and OptStackOps. Asteroids showed up as a tangle of segments while the same logic compiled with gcc on the host was correct. The optimiser, merging operations on the C stack, indexed the vertex-count table with the low byte of a pointer instead of the shape id. Workaround: --disable-opt OptStackOps project-wide (a few cycles per call, no effect at 6.25 MHz) and reading n before the pointers. Lesson: any optimisation pass switched on must go through the reference captures again.
Quitting cleanly. A jmp ($FFFC) to return to NeoBASIC restarted the game: the reset vector is patched to the .neo's execution address. Exit now goes through the API (1,3: reload NeoBASIC) then jmp (0), as the kernel does at reset; the t_exit test types PRINT 6*7 after exiting and expects 42.
7. The tests
Three levels, all in make test. On the host, the portable logic is compiled with gcc against stubs that reproduce the firmware's EFLA (8.8 integration, collisions, shape tables, XOR idempotence, wraparound, fragmentation, key table). On target, Phosphoneo plays deterministic scenarios — title screen, a game, CONTROLS, the t_line, t_xor, t_exit programs — and the captures are compared bit for bit with the references; the pace is measured under the latency recorded in co-simulation. Finally a smoke test in the official emulator. That net is what caught the cc65 case, the missing vertices and the false return to NeoBASIC.
8. What we do not claim
No Neo6502 board has been plugged in: everything is checked in Phosphoneo, in co-simulation with the real firmware and in
neo. The real call latency on a board remains to be measured (plan B: hardware sprites). Sound is validated by ear on an emulator only. Sprint 2 is under way: USB gamepad, a 60 Hz option, persistence of high scores and key mapping on SD/USB. Theasteroneo.neobinary (v0.2.0) is published on GitHub and Framagit; the repository's CHANGELOG is authoritative.