Neo6502 › Artikel › AsteroNeo
Portierung: von Astéroric zu AsteroNeo
Dasselbe Spiel, dasselbe C, eine andere Maschine: was sich ändert, wenn man von einem 6502 mit 1 MHz und einer selbstgebauten Bitmap zu einem 65C02 mit 6,25 MHz wechselt, dessen RP2040 die Linien zeichnet.
Astéroric ist unser Studienklon von Asteroids (Atari, 1979) für den Oric-1 48K: die Logik des Arcade-ROM rev 4 in cc65-C, ein selbstgebauter XOR-Vektorrenderer, 25 Hz in 20.000 Zyklen pro Frame — der vorige Artikel erzählt die vierzig Phasen, die es brauchte. AsteroNeo ist seine Portierung auf den Neo6502 von Olimex: ein echter W65C02S mit 6,25 MHz, ein RP2040 als Chipsatz und eine API bei $FF00. Rahmenentscheidungen: eigenes Repository, Upstream-Firmware im Modus 0 (320×240, 256 Farben), Spielfeld im Vollbild, cc65-C für die Logik und ca65 für die API-Schicht, 30 Hz, XOR-Rendering über die API, Phosphoneo als Testorakel und der offizielle Emulator neo für den Rauchtest. Zwei Sprints später ist das Spiel in Emulation spielbar; hier ist, was sich geändert hat — und was sich gewehrt hat.
Oric-1 → Neo6502, in einer Tabelle
| Oric-1 (Astéroric) | Neo6502 (AsteroNeo) |
|---|---|
| HIRES 240×200 mono, eigener XOR-Bresenham | Modus 0 320×240, XOR-Linien und -Pixel der API (5,1 / 5,2 / 5,5) |
| Asteroiden als vorgerenderte Bitmaps (Blit) | aus den Arcade-Formen abgeleitete, geglättete Polygone (24/16/10 Ecken), vom RP2040 gezeichnet |
| Tastatur: VIA/PSG-Matrix | Key Status per HID-Code (1,2), fünf umbelegbare Aktionen |
| AY-3-8912 unter Timer-1-IRQ | 4-Kanal-Generator Rechteck/Rauschen, Notenwarteschlangen (8,7) |
| VSync/Timer 1, 25 Hz | Frame-Zähler mit 60 Hz (5,37), Spielschritt mit 30 Hz |
| 8-Bit-Koordinaten | int-Koordinaten (320 > 255), 8.8-Integration mit Übertrag |
1. Was sich nicht bewegt
Die Spiellogik — Wellen, Fragmentierung der Asteroiden, KI der beiden Untertassen, Hyperraum, Highscores — bleibt unverändert: sie ist die Übertragung des Arcade-Disassemblys und hängt von keiner Hardware ab. Die sieben C-Module von Astéroric (game, asteroids, ufo, hud, font, title, keys) wandern von einer Maschine zur anderen. Alles, was die Oric-Hardware berührte — HIRES, VIA, AY — wird durch eine Neo6502-Schicht aus vier Dateien ersetzt: neo_gfx.s (Zeichnen), neo_time.c (Takt), neo_input.c (Tastatur), neo_sound.c (Ton).
2. Das Zeichnen: vom eigenen Bresenham zu den Linien der Firmware
Auf dem Oric kostete eine Linie ~97 Zyklen pro Pixel, und das diktierte die 25 Hz. Auf dem Neo6502 zeichnet der RP2040 die Linie: der 65C02 legt die Koordinaten ab und ruft Funktion 5,2 auf. In Co-Simulation auf der echten Firmware gemessen (Phosphoneo + libemul) kostet eine Linie ≈ 22 65C02-Zyklen (544 ARM-Schritte), ein Pixel ≈ 5 Zyklen — unabhängig von der Länge. Das Budget ändert seine Natur: man zählt keine Pixel mehr, man zählt Aufrufe.
Die Feinheit liegt woanders. Die Linie der Firmware (EFLA-Algorithmus) ist halboffen: sie zeichnet [P0, P1[ und lässt den Endpunkt aus. In XOR verliert ein Segment für Segment gezeichnetes Polygon dann jede zweite Ecke oder zündet sie zweimal. neo_gfx.s macht daraus zwei Primitive: eine geschlossene Linie (Linie + Endpixel) und eine „offene“ Linie ]P0, P1], rückwärts gezeichnet, damit jede Ecke genau einmal ge-XOR-t wird. Das Testprogramm t_xor prüft die Idempotenz: ein Polygon zweimal zeichnen lässt den Bildschirm leer.
3. Die Koordinaten: 320 passt nicht in ein Byte
Astéroric lebte in 8 Bit: 240 Spalten, alles passte in ein Register. Bei 320 Pixeln Breite werden die Positionen zu int, und die 8.8-Festkomma-Integration der Geschwindigkeiten erfolgt mit Übertrag (phys.c). Toroidale Kollision und Wraparound durch Duplikation (ein Objekt auf einer Kante wird zweimal gezeichnet) sind auf 320×240 neu geschrieben — und die Spawn-, Wrap- und HUD-Konstanten an das Vollbild angepasst.
4. Der Takt: 30 Hz auf 60-Hz-Frames
Kein VSync und kein Timer 1 mehr: die Firmware bietet einen Frame-Zähler mit 60 Hz (5,37). Der Spielschritt liegt bei 30 Hz — zwei Frames — und alle für 25 Hz kalibrierten Konstanten werden neu berechnet. Mit der gemessenen Latenz hielt Sprint 1 den Takt mit 3 verspäteten Schritten von 255.
Dann verlangte der Playtest rundere Asteroiden: die von gen_shapes.py geglätteten Atari-Silhouetten gehen von 11-13 auf 24/16/10 Ecken. Die C-Schleife, die die Segmente aneinanderreihte, kostete ~1.500 Zyklen pro Segment, und das Spiel fiel auf 200 verspätete Schritte von 255. Das Polygonzeichnen ist in Assembler neu geschrieben (poly_xor: Clipping pro Segment, halboffene Segmente): 2 verspätete Schritte von 255. Auf dieser Maschine ist der Engpass nicht das Pixel, sondern die C-Hülle um jeden Aufruf.
5. Tastatur und Ton
Die VIA/PSG-Matrix des Oric weicht dem Zustand jeder Taste per HID-Code (1,2). Die fünf Aktionen — Drehen, Schub, Hyperraum, Feuer, Beenden — lassen sich im CONTROLS-Bildschirm auf jede benannte Taste umbelegen; die Belegung lebt im RAM. Der Ton verlässt den AY und seinen IRQ für den 4-Kanal-Generator der Firmware (8,7): jeder Effekt von Astéroric wird als Warteschlange von Rechteck-/Rauschnoten nachgebaut, Jingle inklusive. Ohne Hardware-Hüllkurve sind die Ausklänge Lautstärketreppen; am 16. September in neo nach Gehör bestätigt — im Emulator, nicht auf der Platine.
6. Zwei Aufmerksamkeitspunkte in der Toolchain
cc65 2.19 und OptStackOps. Die Asteroiden erschienen als Gewirr von Segmenten, während dieselbe Logik, mit gcc auf dem Host kompiliert, korrekt war. Der Optimierer, der Operationen auf dem C-Stack verschmilzt, indizierte die Tabelle der Eckenzahlen mit dem niederwertigen Byte eines Zeigers statt mit der Form-ID. Abhilfe: --disable-opt OptStackOps im ganzen Projekt (ein paar Zyklen pro Aufruf, ohne Wirkung bei 6,25 MHz) und n vor den Zeigern lesen. Lehre: jeder eingeschaltete Optimierungsdurchlauf muss erneut durch die Referenzaufnahmen.
Sauber beenden. Ein jmp ($FFFC) zur Rückkehr nach NeoBASIC startete das Spiel neu: der Reset-Vektor ist auf die Ausführungsadresse der .neo gepatcht. Der Ausstieg läuft jetzt über die API (1,3: NeoBASIC neu laden) und dann jmp (0), wie der Kernel beim Reset; der Test t_exit tippt nach dem Beenden PRINT 6*7 und erwartet 42.
7. Die Tests
Drei Ebenen, alle in make test. Auf dem Host wird die portable Logik mit gcc gegen Stubs kompiliert, die die EFLA der Firmware nachbilden (8.8-Integration, Kollisionen, Formtabellen, XOR-Idempotenz, Wraparound, Fragmentierung, Tastentabelle). Auf dem Ziel spielt Phosphoneo deterministische Szenarien — Titelbild, eine Partie, CONTROLS, die Programme t_line, t_xor, t_exit — und die Aufnahmen werden bitgenau mit den Referenzen verglichen; der Takt wird unter der in Co-Simulation erfassten Latenz gemessen. Schließlich ein Rauchtest im offiziellen Emulator. Dieses Netz hat den cc65-Fall, die fehlenden Ecken und die falsche Rückkehr nach NeoBASIC gefangen.
8. Was wir nicht behaupten
Keine Neo6502-Platine wurde angeschlossen: alles ist in Phosphoneo, in Co-Simulation mit der echten Firmware und in
neogeprüft. Die echte Aufruflatenz auf der Platine ist noch zu messen (Plan B: Hardware-Sprites). Der Ton ist nur im Emulator nach Gehör bestätigt. Sprint 2 läuft: USB-Gamepad, 60-Hz-Option, Speichern von Highscores und Tastenbelegung auf SD/USB. Die Binaryasteroneo.neo(v0.2.0) ist auf GitHub und Framagit veröffentlicht; das CHANGELOG des Repositorys ist maßgeblich.