Aller au contenu
Eliott65wiki
Sommaire

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émentCapacité
Résolution320 × 240
Plans2, en tuiles ou en image, défilement indépendant
Lutins128, avec profondeur et miroirs
Palette256 couleurs choisies parmi 4 096
Effets par lignedéfilement et banque de palette, ligne par ligne
Mémoire vidéo32 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.

Démonstration GRAPHE : damier de tuiles colorées en diagonale et huit losanges de couleur
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.
Démonstration VAGUE : bandes verticales dégradées de toutes les couleurs, ondulées comme une vague
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 :

ProgrammePire ligneBudget
texte (shell)61 µs63,5 µs
VAGUE140 µs63,5 µs
GRAPHE161 µs63,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

ÉtapeGRAPHEVAGUE
départ161 µs140 µs
moteur réorganisé (pages, boucles par format, liste des lutins)12386
tables d’encodage en C12386
encodage en assembleur10063
carte lue une fois par ligne91—
tuiles 4 bits en C dédié85—
tuiles 4 bits en assembleur6163

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.

Le trajet d'une ligne d'imageL'image part de la mémoire de la 6502, lue sans copie. Le cœur 0 dessine la ligne avec une avance de huit lignes au plus ; le cœur 1 l'encode en signaux TMDS ; la DMA et la PIO l'envoient sur la prise HDMI, un bit par cycle à 252 MHz. Chaque ligne doit être prête en 63,5 microsecondes, et aucune image complète n'est gardée en mémoire.Mémoire 6502texte, tuiles,lutinsCœur 0dessine la ligne(entre deux appels)Réserve8 lignesd'avanceCœur 1encode en TMDS(signal HDMI)DMA + PIO→ prise HDMI1 bit / cyclelue sans copieBudget : 63,5 µs par ligne logique (2 lignes de l'écran 640 × 480 à 60 Hz)Pas d'image complète en mémoire : seulement des tampons de ligne.Depuis la v0.40.0 les deux cœurs collaborent ; avant, le cœur 1 faisait tout.
Le cœur 0, entre deux appels système, dessine les lignes avec jusqu’à huit lignes d’avance ; le cœur 1 ne fait plus que les encoder.
ScénarioCœur 1 seulAvec la collaborationLignes manquées
tuiles 8 × 8, 4 bits52 µs30 µs0
deux plans71 µs31 µs0 (201 avant)
tuiles 16 × 1678 µs31 µs0
image + tuiles82 µs31 µs0
tuiles + 16 lutins 16 × 1693 µs31 µs0
tuiles + 32 lutins 32 × 32192 µs30 µs30

(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 0Lignes manquées par image
32 lutins 16 × 16103 → 85 µs7 → 0
32 lutins 32 × 32157 → 118 µs30 → 26
32 lutins 64 × 64172 → 132 µs63 → 61
deux plans + 32 lutins122 → 104 µs13 → 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énarioMoteurLignes en retard par image
32 lutins 16 × 1685 → 73 µs0
32 lutins 32 × 32118 → 93 µs26 → 19
32 lutins 64 × 64 en 8 bits132 → 116 µs61 → 59
deux plans + 32 lutins 16 × 16104 → 92 µs10 → 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.

Sources dans le dépôt : docs/journal/13-graphisme.md, docs/adr/ADR-0025-graphisme-tuiles-lutins.md, docs/adr/ADR-0026-collaboration-des-coeurs.md.