Tuiles et lutins : le graphisme
Des décors en tuiles, des lutins qui se déplacent, jusqu’à 256 couleurs : sans prendre de mémoire à la 6502, et en dessinant chaque ligne en moins de 63,5 microsecondes. Le chantier le plus long du projet.
Dans cette page
Pourquoi des tuiles
Une image de 320 × 240 en 256 couleurs pèse 75 Kio : plus que toute la mémoire de la 6502. Les consoles de jeux ont résolu ce problème depuis longtemps avec les tuiles : on dessine une fois un petit jeu de motifs (8 × 8 ou 16 × 16 pixels), et le décor n’est qu’une carte qui dit quel motif mettre à quel endroit. Par-dessus, des lutins (sprites) se déplacent librement.
Avec deux plans, 256 tuiles et 128 lutins, il faut environ 22 Kio en 320 × 240. La mémoire n’est plus le problème ; le problème devient le temps de dessin.
Le modèle retenu
Le modèle s’inspire de deux machines connues, sans chercher la compatibilité :
- de la VERA (la puce vidéo du Commander X16), des plans souples : tuiles ou image, de 1 à 8 bits par pixel, tuiles de 8 × 8 ou 16 × 16 ;
- de la SNES, les effets par ligne et les priorités.
| Élément | Capacité |
|---|---|
| Résolution | 320 × 240 |
| Plans | 2, en tuiles ou en image, défilement indépendant |
| Lutins | 128, avec profondeur et miroirs |
| Palette | 256 couleurs choisies parmi 4 096 |
| Effets par ligne | défilement et banque de palette, ligne par ligne |
| Mémoire vidéo | 32 Kio, extensible à 64 Kio |
L’idée propre à Eliott65 : lire sans copier
Comme pour l’écran texte, les données graphiques peuvent rester dans la mémoire du programme 6502 : cartes, images, dessins des lutins, table des lutins. Le moteur les lit là, sans copie. Le programme fait bouger un lutin en changeant deux octets dans sa propre mémoire, sans appel système.
À côté, une mémoire vidéo de 32 Kio dans le RP2040, chargée par les services du noyau (y compris directement depuis la clé, sans passer par la 6502). Elle peut s’étendre à 64 Kio en empruntant des cadres libres à la réserve du multitâche, rendus à la fin.

GRAPHE.E65 (994 octets) : un plan de tuiles en 16 couleurs qui défile en diagonale, sa carte dans
la mémoire du programme, et 8 lutins de 16 × 16 qui rebondissent. Capture de l’émulateur.
VAGUE.E65 : une image en 16 couleurs, mais une banque de palette différente à chaque ligne donne
241 couleurs à l’écran ; un défilement par ligne fait onduler l’image. Capture de l’émulateur.Une référence pour ne pas se tromper
Avant d’optimiser quoi que ce soit, une référence a été écrite en Python, d’après la spécification, pixel par pixel. Le moteur du firmware doit produire exactement la même image. Trois erreurs introduites exprès (ordre des priorités, miroir vertical, repli de la carte) sont toutes détectées.
Deux fois, moteur et référence étaient d’accord contre le cas de test écrit à la main : c’était l’attente qui était fausse.
Le mur des 63,5 µs
Chaque ligne logique doit être prête en 63,5 µs. Premier passage sur la carte :
| Programme | Pire ligne | Budget |
|---|---|---|
| texte (shell) | 61 µs | 63,5 µs |
| VAGUE | 140 µs | 63,5 µs |
| GRAPHE | 161 µs | 63,5 µs |
Deux fois et demie trop lent. Ce que l’utilisateur voyait : un écran brouillé. Le rendu était juste, mais en retard, et l’écran ressortait des lignes à moitié écrites.
« Il ne faut pas augmenter la fréquence des cœurs ? » Non : l’horloge de 252 MHz est celle des bits HDMI. L’augmenter obligerait à envoyer les pixels plus vite d’autant : le nombre de cycles par ligne resterait le même. (Le signal 720 × 480 adopté plus tard, à 270 MHz, donne bien 7 % de cycles en plus par ligne : sa ligne est plus longue, pas plus rapide.)
La descente, mesure par mesure
| Étape | GRAPHE | VAGUE |
|---|---|---|
| départ | 161 µs | 140 µs |
| moteur réorganisé (pages, boucles par format, liste des lutins) | 123 | 86 |
| tables d’encodage en C | 123 | 86 |
| encodage en assembleur | 100 | 63 |
| carte lue une fois par ligne | 91 | — |
| tuiles 4 bits en C dédié | 85 | — |
| tuiles 4 bits en assembleur | 61 | 63 |
Le coupable principal : le Cortex-M0+ a peu de registres, et le compilateur rangeait les variables sur la pile, environ 28 instructions par pixel pour l’encodage. L’assembleur écrit à la main en fait 7 à 12.
« Je vois les losanges qui rebondissent, c’est bon » : GRAPHE sur la carte, en v0.36.
Les limites, mesurées
Quatorze scénarios ont ensuite été mesurés sur la carte, avec une mémoire vidéo remplie au hasard et un contrôle de chaque image contre la référence. Le rendu est juste partout ; seuls quelques scénarios tenaient dans le budget : un plan de tuiles 8 × 8 en 4 bits, une image seule, quelques lutins par ligne. Deux plans, des tuiles 16 × 16, beaucoup de lutins sur une même ligne dépassaient.
Deux cœurs qui collaborent (v0.39 – v0.40)
Jusque-là, le cœur 1 faisait tout : dessiner la ligne, puis l’encoder en signal HDMI. L’idée retenue : le cœur 0 dessine, le cœur 1 encode.
| Scénario | Cœur 1 seul | Avec la collaboration | Lignes manquées |
|---|---|---|---|
| tuiles 8 × 8, 4 bits | 52 µs | 30 µs | 0 |
| deux plans | 71 µs | 31 µs | 0 (201 avant) |
| tuiles 16 × 16 | 78 µs | 31 µs | 0 |
| image + tuiles | 82 µs | 31 µs | 0 |
| tuiles + 16 lutins 16 × 16 | 93 µs | 31 µs | 0 |
| tuiles + 32 lutins 32 × 32 | 192 µs | 30 µs | 30 |
(Dans la colonne « avec la collaboration », le temps du cœur 1 ; le cœur 0 dispose de la réserve de huit lignes pour lisser ses étapes plus longues.)
Dix scénarios sur quatorze tiennent désormais, contre quatre avant.
Les lutins en assembleur (v0.41)
Les deux petites banques de mémoire rapide du RP2040 étaient pleines. Puisque seul le cœur 0 exécute désormais le moteur, et qu’il exécute déjà le noyau depuis la flash, une expérience : laisser le moteur en flash. Mesuré sur la carte, il va aussi vite qu’en RAM dans les quatorze scénarios (0 à 2 µs d’écart) : le cache de la flash suffit. La place libérée accueille le dessin des lutins en assembleur.
| Scénario | Étape la plus longue du cœur 0 | Lignes manquées par image |
|---|---|---|
| 32 lutins 16 × 16 | 103 → 85 µs | 7 → 0 |
| 32 lutins 32 × 32 | 157 → 118 µs | 30 → 26 |
| 32 lutins 64 × 64 | 172 → 132 µs | 63 → 61 |
| deux plans + 32 lutins | 122 → 104 µs | 13 → 10 |
Onze scénarios sur quatorze tiennent. Restent 32 grands lutins sur les mêmes lignes, et deux plans avec 32 lutins. Contrepartie : le graphisme a besoin d’une page libre pour la réserve de lignes ; sans elle, le noyau le refuse.
Les boucles des lutins réécrites (v0.49)
Une mesure au cycle près a montré que la boucle de pixels des lutins coûtait encore 17 à 18 cycles par pixel : l’octet était relu, et la parité de la colonne testée, à chaque pixel. Réécrite (pointeurs qui avancent, deux pixels par octet en 4 bits, quatre pixels à la fois en 8 bits), elle tombe à environ 11 cycles par pixel. Deux scénarios ont été ajoutés pour les miroirs, que les quatorze premiers ne couvraient pas.
| Scénario | Moteur | Lignes en retard par image |
|---|---|---|
| 32 lutins 16 × 16 | 85 → 73 µs | 0 |
| 32 lutins 32 × 32 | 118 → 93 µs | 26 → 19 |
| 32 lutins 64 × 64 en 8 bits | 132 → 116 µs | 61 → 59 |
| deux plans + 32 lutins 16 × 16 | 104 → 92 µs | 10 → 5 |
Treize scénarios sur seize tiennent, tous justes. Le chantier est clos avec ces limites, en accord avec l’utilisateur.
Le prix : les appels système
Le cœur 0 sert aussi les appels système. Une étape de dessin peut donc retarder un appel. Mesuré du côté de la 6502 pendant l’affichage de deux plans : +0,7 µs en moyenne, +34 µs au pire. Un prix jugé acceptable : la collaboration est activée par défaut depuis la v0.40.
Restait une étape longue : 104 µs avec GRAPHE, au début de chaque image. L’hypothèse : la relecture de la table des 128 lutins (1 Kio) d’un seul coup. Depuis la v0.42, elle se fait par tranches de 16 lutins, une par tour du cœur 0, la réserve de lignes couvrant ces tours. Étape la plus longue : 104 → 30 µs, hypothèse confirmée. C’est aussi le pire retard qu’un appel système subit à cause du dessin.
Cette mesure a d’ailleurs révélé un vieux défaut du noyau, sans rapport avec le graphisme : un programme qui enchaînait les appels ne pouvait plus être préempté. Corrigé.
Ce qui est propre à Eliott65
Interrogé sur l’état de l’art, le projet a regardé PicoDVI et PicoVGA, deux bibliothèques vidéo connues pour le RP2040 : l’encodage HDMI converge vers les mêmes solutions, la physique laissant peu de choix. Ce qui distingue Eliott65 est ailleurs : le noyau hors de la 6502, le graphisme lu sans copie dans la mémoire des programmes, et une mémoire vidéo empruntée aux processus quand on en a besoin.
Et ensuite
- une piste pour les trois scénarios qui dépassent encore : le cœur 1, libre environ 29 µs par ligne, pourrait dessiner les lutins de devant ;
- le mode Hercules (720 × 350, texte 80 × 25) : le signal 720 × 480 à 270 MHz est le défaut depuis la v0.52, la partie graphique existe depuis la v0.53 et le texte depuis la v0.54, avec sa propre police 9 × 14 depuis la v0.58 (voir La vidéo) ;
- un arrêt de GRAPHE au bout de quelques minutes à 270 MHz, avec une correction candidate à vérifier sur la carte (voir Le bus).
Le son, lui, a fait son chemin : voir Le son.