Neo6502 › Articles › AsteroNeo
Portage : d'Astéroric à AsteroNeo
Le même jeu, le même C, une autre machine : ce qui change quand on passe d'un 6502 à 1 MHz et d'un bitmap maison à un 65C02 à 6,25 MHz dont le RP2040 trace les lignes.
Astéroric est notre clone d'étude d'Asteroids (Atari, 1979) pour l'Oric-1 48 Ko : la logique de la ROM arcade rev 4 reprise en C cc65, un tracé vectoriel XOR maison, 25 Hz dans 20 000 cycles par trame — l'article précédent raconte les quarante phases qu'il a fallu. AsteroNeo est son portage sur le Neo6502 d'Olimex : un W65C02S réel à 6,25 MHz, un RP2040 qui tient lieu de chipset et une API en $FF00. Décisions de cadrage : dépôt dédié, firmware amont en mode 0 (320×240, 256 couleurs), terrain plein écran, C cc65 pour la logique et ca65 pour la couche API, 30 Hz, rendu XOR par l'API, Phosphoneo comme oracle des tests et l'émulateur officiel neo pour la fumée. Deux sprints plus tard, le jeu est jouable en émulation ; voici ce qui a changé, et ce qui a résisté.
Oric-1 → Neo6502, en une table
| Oric-1 (Astéroric) | Neo6502 (AsteroNeo) |
|---|---|
| HIRES 240×200 mono, Bresenham XOR maison | mode 0 320×240, lignes et pixels XOR de l'API (5,1 / 5,2 / 5,5) |
| astéroïdes en bitmaps pré-rendus (blit) | polygones dérivés des formes arcade, lissés (24/16/10 sommets), tracés par le RP2040 |
| clavier : matrice VIA/PSG | Key Status par code HID (1,2), cinq actions remappables |
| AY-3-8912 sous IRQ Timer 1 | générateur 4 canaux carré/bruit, files de notes (8,7) |
| VSync/Timer 1, 25 Hz | compteur de trames 60 Hz (5,37), pas de jeu à 30 Hz |
| coordonnées 8 bits | coordonnées int (320 > 255), intégration 8.8 avec retenue |
1. Ce qui ne bouge pas
La logique de jeu — vagues, fragmentation des astéroïdes, IA des deux soucoupes, hyperespace, high scores — est reprise telle quelle : c'est la transposition du désassemblage arcade, et elle ne dépend d'aucun matériel. Les sept modules C d'Astéroric (game, asteroids, ufo, hud, font, title, keys) passent d'une machine à l'autre. Tout ce qui touchait au matériel Oric — HIRES, VIA, AY — est remplacé par une couche Neo6502 de quatre fichiers : neo_gfx.s (tracé), neo_time.c (cadence), neo_input.c (clavier), neo_sound.c (son).
2. Le tracé : du Bresenham maison aux lignes du firmware
Sur l'Oric, la ligne coûtait ~97 cycles par pixel et c'est elle qui a dicté le 25 Hz. Sur le Neo6502, la ligne est tracée par le RP2040 : le 65C02 dépose les coordonnées et appelle la fonction 5,2. Mesurée en co-simulation sur le vrai firmware (Phosphoneo + libemul), une ligne coûte ≈ 22 cycles 65C02 (544 pas ARM), un pixel ≈ 5 cycles — quelle que soit la longueur. Le budget change de nature : on ne compte plus les pixels, on compte les appels.
La subtilité est ailleurs. La ligne du firmware (algorithme EFLA) est semi-ouverte : elle trace [P0, P1[ et exclut son point d'arrivée. En XOR, un polygone tracé segment par segment perd alors un sommet sur deux, ou l'allume deux fois. neo_gfx.s en fait deux primitives : une ligne fermée (ligne + pixel final) et une ligne « ouverte » ]P0, P1], tracée à l'envers, pour que chaque sommet soit XOR-é exactement une fois. Le programme de test t_xor vérifie l'idempotence : tracer deux fois un polygone rend l'écran vierge.
3. Les coordonnées : 320 ne tient pas dans un octet
Astéroric vivait en 8 bits : 240 colonnes, tout tenait dans un registre. À 320 pixels de large, les positions passent en int, et l'intégration des vitesses en virgule fixe 8.8 se fait avec retenue (phys.c). La collision torique et le wraparound par duplication (un objet à cheval sur un bord est tracé deux fois) sont réécrits sur 320×240 — et les constantes de spawn, de wrap et de HUD adaptées au terrain plein écran.
4. La cadence : 30 Hz sur des trames à 60 Hz
Plus de VSync ni de Timer 1 : le firmware expose un compteur de trames à 60 Hz (5,37). Le pas de jeu est fixé à 30 Hz — deux trames — et toutes les constantes calibrées pour 25 Hz sont recalculées. Avec la latence mesurée, le sprint 1 tenait la cadence à 3 pas en retard sur 255.
Puis le playtest a demandé des astéroïdes arrondis : les silhouettes Atari lissées par gen_shapes.py passent de 11-13 à 24/16/10 sommets. La boucle C qui enchaînait les segments coûtait ~1 500 cycles par segment et le jeu tombait à 200 pas en retard sur 255. Le tracé de polygone est réécrit en assembleur (poly_xor : clip par segment, segments semi-ouverts) : 2 pas en retard sur 255. Sur cette machine, le goulot n'est pas le pixel, c'est l'enrobage C autour de chaque appel.
5. Clavier et son
La matrice VIA/PSG de l'Oric laisse place à l'état de chaque touche par code HID (1,2). Les cinq actions — rotation, poussée, hyperespace, tir, quitter — sont remappables sur n'importe quelle touche nommée depuis l'écran CONTROLS ; le mapping vit en RAM. Le son quitte l'AY et son IRQ pour le générateur 4 canaux du firmware (8,7) : chaque effet d'Astéroric est recréé en file de notes carré/bruit, le jingle compris. Faute d'enveloppe matérielle, les décroissances sont des escaliers de volume ; validées à l'oreille dans neo le 16 septembre — sur émulateur, pas sur carte.
6. Deux points d'attention dans la chaîne d'outils
cc65 2.19 et OptStackOps. Les astéroïdes s'affichaient en enchevêtrement de segments alors que la même logique compilée avec gcc sur l'hôte était correcte. L'optimiseur, en fusionnant des opérations sur la pile C, indexait la table du nombre de sommets avec l'octet bas d'un pointeur au lieu de l'identifiant de forme. Contournement : --disable-opt OptStackOps sur tout le projet (quelques cycles par appel, sans effet à 6,25 MHz) et lecture de n avant les pointeurs. Leçon : toute passe d'optimisation activée doit repasser les captures de référence.
Quitter proprement. Un jmp ($FFFC) pour revenir à NeoBASIC relançait le jeu : le vecteur de reset est patché sur l'adresse d'exécution du .neo. La sortie passe désormais par l'API (1,3 : recharger NeoBASIC) puis jmp (0), comme le noyau au reset ; le test t_exit tape PRINT 6*7 après la sortie et attend 42.
7. Les tests
Trois niveaux, tous dans make test. Sur l'hôte, la logique portable est compilée avec gcc contre des stubs qui reproduisent l'EFLA du firmware (intégration 8.8, collisions, tables des formes, idempotence XOR, wraparound, fragmentation, table des touches). Sur cible, Phosphoneo joue des scénarios déterministes — écran titre, partie, CONTROLS, programmes t_line, t_xor, t_exit — et les captures sont comparées bit à bit aux références ; la cadence est mesurée sous la latence relevée en co-simulation. Enfin un test de fumée dans l'émulateur officiel. C'est ce filet qui a attrapé le cas cc65, les sommets manquants et le faux retour à NeoBASIC.
8. Ce qu'on ne dit pas
Aucune carte Neo6502 n'a été branchée : tout est vérifié dans Phosphoneo, en co-simulation avec le vrai firmware et dans
neo. La latence réelle des appels sur carte reste à mesurer (plan B : sprites matériels). Le son est validé à l'oreille sur émulateur seulement. Le sprint 2 est en cours : manette USB, option 60 Hz, persistance des high scores et du mapping sur SD/USB. Le binaireasteroneo.neo(v0.2.0) est publié sur GitHub et Framagit ; le CHANGELOG du dépôt fait foi.