Aller au contenu

papyDeck — Architecture système

Note

Technical reference — French only for now. / Référence technique, en français pour l'instant.

Console de jeux d'inspiration arcade construite autour de trois RP2350B genlockés et d'un module ESP32-MINI-1, sortant du 640×480@60 Hz, 8bpp palettisé 256 couleurs en HDMI/DVI via le HSTX.

Documents par puce : ArchitectureRP1.md · ArchitectureRP2.md · ArchitectureRP3.md Crédits du code tiers : THIRD_PARTY.md Matériel : ../papyDeck_HW/rpGPU_V1 — la carte V1 alpha, sa netlist et les board headers ; reçue et mise en service le 2026-08-26.


1. Vue d'ensemble

   µSD ──┐                                    ESP32-MINI-1
         │                                    (Wi-Fi + BT Classic + BLE)
         │                                          │
         │   SPI + UART + EN/IO0                    │
         └────────┐    ┌───────────────────────────-┘
                  ▼    ▼
              ┌───────────┐
              │    RP1    │  logique, USB, stockage, réseau
              └───────────┘
                │        │
      SPI1      │        │      SPI0/PIO
   assets +     │        │    assets + audio
  display list  │        │
                ▼        ▼
           ┌───────┐  ┌───────┐
           │  RP2  │  │  RP3  │──► HSTX ──► TPD12S521 ──► HDMI
           └───────┘  └───────┘
             │   ▲       ▲  │
        NOR 16Mo │       │  NOR 16Mo
        (atlas)  │       │  (fond)
                 │       │
                 └───────┘
       overlay 16 bits SDR @ 42 MHz  (RP2 → RP3)
       HREQ / HSYNC                  (RP3 → RP2)
       VSYNC                         (RP3 → RP1, RP2)

   oscillateur 12 MHz commun ──► XIN des trois RP2350B

Chaque étage possède sa propre NOR QSPI et ses propres 520 Ko de SRAM. C'est ce qui fait sauter le mur de bande passante d'une solution monopuce.

Rôles

Puce Rôle Ressources propres Contrainte temps réel
RP1 Logique de jeu, USB, µSD, réseau, audio analogique, mise à jour des autres puces. En service au 2026-08-26 : RUN/straps des deux autres puces, consoles, Lua + gfx, display list, palette ; µSD, réseau, audio et mise à jour sont de la conception, non implémentés NOR 16 Mo, µSD, ESP, DAC Aucune contrainte dure
RP2 Sprite engine : composition des scanlines d'overlay NOR 16 Mo (atlas), pool de 320 Kio possédé par RP1 (pages de 64 o, chargé blob par blob depuis la NOR sur ordre de RP1 ; 450 Ko : cible) Une boucle dure : le blit par scanline, 16 000 cycles par ligne et par cœur
RP3 Mixeur : overlay + fond, table de mélange, sortie HDMI, audio HDMI (conception) NOR 16 Mo (fond), table 128 Ko Composition mesurée le 2026-08-26 : 4 543 cycles/ligne (57 %) ; avec le fond tuilé, ≈ 6 700 / 8 000 par ligne (84 %)
ESP32 Bluetooth Classic + BLE (manettes, clavier, souris), Wi-Fi Aucune

2. Horloges et genlock

Un oscillateur CMOS 12 MHz unique attaque le XIN des trois RP2350B, XOUT laissé en l'air. Chaque puce multiplie la même référence par sa PLL_SYS : les trois clk_sys sortent à 252 MHz verrouillés en fréquence. Aucune configuration firmware particulière — set_sys_clock_khz(252000) suffit. C'est ce que font rp1_papydeck, RP2 et RP3 ; rp1_supervisor reste aux 150 MHz du SDK, sans conséquence — RP1 est hors genlock.

Paramètre Valeur Source
clk_sys 252 MHz overclock standard du RP2350
clk_hstx clk_sys / 2 = 126 MHz mode 480p : pixel clock 25,2 MHz, TMDS 252 Mb/s
Fréquence de référence 12 MHz imposée par le bootrom (boot USB et boot UART)

La référence des PLL est câblée sur le XOSC. pll_init() calcule ref_freq = XOSC_HZ / refdiv et le multiplexeur d'entrée des PLL n'a aucune entrée GPIN. Un clk_gpin ne peut alimenter que clk_ref ou directement clk_sys, en court-circuitant la PLL — ce qui imposerait de distribuer 252 MHz sur le circuit imprimé. Le partage du XOSC est la seule voie praticable.

12 MHz est imposé : le bootrom suppose cette valeur pour dériver le 48 MHz de l'USB PLL (DS RP2350 p. 17 et 376) ; le boot UART de secours en dépend également. Une autre fréquence casserait le boot USB tant que l'OTP (BOOTSEL_XOSC_CFG, BOOTSEL_PLL_CFG) n'est pas programmée.

Attaque externe de XIN — spécification

Point Valeur DS RP2350
Mode entrée CMOS asymétrique, XOUT déconnecté p. 17, 1336, 1338
Domaine d'alimentation IOVDD ; QFN-80 broche 30 (XOUT = 31) p. 1338
VIH ≥ 0,65 × IOVDD, soit 2,15 V à 3,3 V p. 1343
VIL ≤ 0,35 × IOVDD, soit 1,16 V à 3,3 V p. 1343
Fréquence max 50 MHz p. 557
CTRL.FREQ_RANGE ne pas configurer en entrée CMOS p. 557

Nomenclature de l'horloge

Fonction Référence Notes
Oscillateur Abracon ASDMB-12.000MHZ-LC-T MEMS, CMOS 3,3 V, ±50 ppm. Marge ×50 sur la tolérance USB (±2 500 ppm), ×100 sur le pixel clock HDMI (±5 000 ppm).
Buffer 1:3 74LVC3G34 (ou CDCLVC1104 1:4, la 4ᵉ sortie en point de test) 12 MHz ne demande rien de particulier.

Topologie en étoile, 22-33 Ω en série au pied de chaque sortie, retour de masse continu, à l'écart des paires HSTX. Pas d'appairage de longueurs : seule la fréquence est partagée, la phase de chaque PLL est arbitraire.

Phase

Les trois clk_sys sont verrouillés en fréquence, pas en phase : chaque PLL s'accroche avec un décalage arbitraire, stable pendant une mise sous tension, différent d'un démarrage à l'autre. Le protocole réaligne la phase à chaque ligne : c'est RP3 qui lève HREQ depuis sa machine PIO (side-set) et RP2 qui attend ce front par un wait avant d'émettre. RP3 échantillonne RP3_RX_PHASE_DELAY + 1 cycles après le front, puis tous les 6 cycles. Mesuré sur la carte le 2026-08-26 en balayant le délai : sans erreur de 10 à 13, soit une fenêtre de 4 cycles = 15,9 ns sur la période de 23,8 ns ; réglé à 12, 0 erreur sur 11 M de mots. Le décalage sub-cycle entre PLL (< 4 ns) tient dans cette fenêtre.


3. Liens inter-puces

État au 2026-08-26 ; ce qui est « conception » n'est pas câblé dans le firmware.

Lien Nature Débit Usage
RP2 → RP3 16 bits SDR @ 42 MHz, PIO, GP26-41 → GP45-30 84 Mo/s overlay par scanline, 20,2 Mo/s utiles ; trame bus de 484 lignes = 480 pixels + 4 blocs de contrôle (palette et caméra, modes, écritures de carte, réservé) — un compte pair, obligatoirement (§10.3)
RP3 → RP2 HREQ (GP28 → GP43), HSYNC (GP27 → GP44) cadencement ligne ; HSYNC inutilisé par RP2
RP3 → RP1, RP2 VSYNC (GP26 → RP1 GP39, RP2 GP45) cadencement trame, sur le front montant
RP1 → RP2 SPI1 maître (GP26-29) → PIO esclave pio1 (GP2/⅘) 25 MHz display list + blocs de contrôle (palette, caméra et carte du fond) + ordres de chargement du pool, paquet de 6 928 o au VSYNC. Installation des assets et mise à jour : conception
RP2 → RP1 UART0 (RP2 GP0/1 → RP1 GP0/1) 115 200 console de RP2, lecture seule, préfixée [rp2]
RP1 ↔ RP3 UART1 (RP1 GP⅘ ↔ RP3 GP0/1) 115 200 console de RP3, bidirectionnelle — le seul lien RP1 ↔ RP3 existant. SPI0/PIO (assets, audio) et mise à jour : conception
RP1 → RP2, RP3 RUN (GP2, GP7), straps QSPI (GP3/6/12/13, GP8/9/10/11) RUN tenu haut par l'entrée + pull-up de RP1 (pas de pull-up externe : sans firmware sur RP1, RP2 et RP3 restent en reset) ; reset = impulsion basse 10 ms ; straps en Hi-Z
RP1 ↔ ESP SPI + UART + EN/IO0 ≥ 6 MHz manettes, réseau, flashage — conception, non implémenté côté RP1
RP1 ↔ µSD PIO, SD 4 bits stockage des jeux — conception, non implémenté

Bus overlay RP2 → RP3

16 bits SDR à 42 MHz. La fréquence est un diviseur entier de clk_sys (252 / 6) : c'est la condition pour que la phase d'échantillonnage reste fixe malgré le genlock en fréquence seule.

Format : un index de palette 8 bits par pixel, sans octet d'attribut — donc deux pixels par transfert de 16 bits. Charge utile 640 o/ligne en 31,75 µs = 20,2 Mo/s sur les 84 disponibles, et la rafale occupe 24 % du temps-ligne (320 transferts × 6 cycles = 1 920 des 8 000 cycles), entièrement sur DMA.

Priorité devant/derrière le fond et effets de composition sont portés par l'index et résolus par la table de mélange de RP3. Une seule palette de 256 couleurs simultanées, sans banques — voir ArchitectureRP3.md §3.

Le bus est câblé en ordre inverse — RP3 reçoit OVERLAY_15 sur GP30 et OVERLAY_0 sur GP45, les deux puces se faisant face. RP3 capture donc chaque transfert inversé bit à bit, ce qui échange les deux pixels et inverse chaque index ; les deux permutations sont absorbées sans coût d'exécution. Détail dans ArchitectureRP3.md §4. Après ses 480 lignes de pixels, chaque trame bus porte quatre lignes de contrôle (480 à 483), demandées par HREQ exactement comme des lignes de 640 octets, dès que la ligne 479 est composée : le bloc A PDPA (génération, backdrop, drapeaux et caméra du fond, 256 couleurs RGB565), le bloc B PDPB (génération, 256 modes de mélange), le bloc C PDPC (écritures de carte du fond, depuis le 2026-08-27) et le bloc D, réservé à zéro. RP1 les écrit, RP2 les relaie tels quels, RP3 les applique avant la ligne 0 suivante. Aucune autre « configuration » ne circule. Le compte de lignes de contrôle doit être pair — RP2 répartit les lignes entre ses cœurs par parité et adopte sur celui de la ligne 480 ; avec 483 lignes les deux cœurs s'attendaient pour toujours (2026-08-27). Formats en §10.4.

Granularité : par burst. RP3 lève HREQ une fois par ligne depuis sa machine PIO ; RP2 attend le front et débite ses 640 octets à la cadence imposée par le récepteur — un transfert tous les 6 cycles, sans contrôle de flux, latence fixe — avec une horloge en side-set qui ne sert qu'à l'oscilloscope. La réception est système-synchrone : phase calibrée une fois (RP3_RX_PHASE_DELAY = 12, mesurée), pas de poursuite de front. Transmission en SDR par burst.

Contraintes PIO

Deux contraintes indépendantes gouvernent le brochage du bus.

Largeur du bus : 16 broches contiguës. in pins, 16 lit N broches à partir d'une base ; le PIO ne rassemble pas des broches éparses.

Fenêtre du bloc PIO : base 0 ou 16. Un bloc PIO n'adresse que 32 GPIO alors que le RP2350B en a 48. GPIOBASE fixe où se pose la broche « 0 » du bloc :

GPIOBASE Le bloc voit
0 GPIO 0 à 31
16 GPIO 16 à 47

Le bus overlay et ses signaux occupent GPIO26-45 sur RP2 et RP3 → GPIOBASE = 16 pour ce bloc. Tout autre usage PIO en dessous de GPIO16 passe donc par un autre bloc PIO — le RP2350 en a trois : l'esclave SPI de la display list de RP2 est sur pio1 avec GPIOBASE = 0 (GP2-5).


4. Budgets vidéo

Cible 640×480@60 : 18,4 Mpx/s actifs, temps ligne 31,75 µs ≈ 8 000 cycles à 252 MHz, vblank 45 lignes = 1,43 ms.

Grandeur Valeur
Flux 8bpp 18,4 Mo/s
Flux RGB565 vers HSTX (2 px par mot de 32 bits) 36,9 Mo/s
Débit NOR QSPI mesuré 33 Mo/s
Budget de rechargement NOR 550 Ko/trame, ou 47 Ko en vblank seul
Trafic bus interne, tout confondu ~260 Mo/s, < 10 % du crossbar AHB

Ni la NOR, ni le bus interne, ni le CPU ne sont limitants. Les deux points durs étaient la capture du bus inter-puces (§3) et la cohabitation streaming NOR / exécution du code ; les deux sont tranchés au bring-up du 2026-08-26 : phase 12, 0 erreur sur 11 M de mots ; copy_to_ram imposé et mesuré.

Contention XIP. Sur RP2 et RP3, tout le code tourne en SRAM (copy_to_ram) : exécuté depuis la flash, RP3 dépassait le temps-ligne (~39 µs). RP1 reste en XIP. Un DMA qui lit la flash par l'alias XIP bloque le port de lecture du DMA (~1 µs par mot) et affame les canaux temps réel — l'émetteur du bus de RP2 a produit des lignes parasites ainsi (2026-08-27) : les chargements du pool de RP2 passent donc par la XIP stream FIFO (ArchitectureRP2 §6). Les copies uniques au boot (atlas de test de RP2, tileset de RP3) restent des DMA sur l'alias non-caché, rien ne tournant encore ; RP3 lit la carte du monde par lectures XIP cachées du CPU pendant le vblank (ArchitectureRP3 §5) — jamais par DMA pendant qu'HSTX émet.


5. Budget d'alimentation 3,3 V

Ce qui est spécifié et ce qui est estimé

Grandeur Statut
Régulateur interne RP2350 : 200 mA sur DVDD, rendement 72 % à 3,3 V d'entrée spécifié — DS p. 1346, guide de conception p. 7
W25Q128JV : 25 mA max en programmation/effacement, 20 mA en lecture à 104 MHz spécifié — DS p. 62
ESP32-MINI-1 : 379 mA crête en émission 802.11b @19,5 dBm ; 112 mA en réception spécifié — DS Table 16
Courant IOVDD des RP2350 estimé — la fiche ne donne aucun maximum, seulement des mesures typiques à 150 MHz un cœur
Carte µSD estimé — dépend de la carte de l'utilisateur

La fiche RP2350 ne spécifie pas de courant maximal. Elle donne des cas d'usage typiques (CoreMark un cœur à 150 MHz : 11 mA sur VREG_VIN, 455 µA sur IOVDD) et un graphe DVDD en fonction de la fréquence. Le plafond retenu ici est celui du régulateur interne — 200 mA sur DVDD — qui borne physiquement ce que le cœur peut consommer.

Consommation par composant

Composant Rail Calcul Max
RP2350B — cœur VREG_VIN 200 mA × 1,1 V ÷ 0,72 ÷ 3,3 V 93 mA
RP2350B — E/S IOVDD + QSPI_IOVDD estimation, voir ci-dessous 20 à 30 mA
RP2350B — total, ×3 ≈ 355 mA
W25Q128JV ×3 3V3 25 + 25 + 20 mA 70 mA
ESP32-MINI-1 3V3 crête TX 802.11b 379 mA
Carte µSD 3V3 en écriture, variable ~100 mA
TLV320DAC3100 — logique 3V3 + 1V8 ~10 mA
TLV320DAC3100 — ampli classe D SPKVDD 2,5 W sur 4 Ω à 5,5 V jusqu'à ~500 mA sur son rail
Oscillateur + buffer 3V3 ~5 mA
TPD12S521 3V3 110 µA + 1 µA négligeable
Pull-ups (SD, DDC, straps) 3V3 négligeable

Estimation des E/S

Le courant IOVDD dépend du commutation des broches, en C × V × f.

RP2 est le plus chargé : 17 lignes du bus overlay à 42 MHz. Avec 15 pF de charge par ligne et 50 % d'activité, 15 pF × 3,3 V × 42 MHz × 0,5 ≈ 1 mA par ligne, soit ~18 mA, plus les lectures QSPI.

RP3 commute 8 lignes HSTX à 252 Mb/s vers le TPD12S521 : pistes courtes, ~5 pF, ≈ 2 mA par ligne, soit ~17 mA. Le courant continu des lignes TMDS ne charge pas ce rail : il vient de la terminaison 50 Ω vers 3,3 V du récepteur et est absorbé vers la masse par le pad, il ne transite pas par IOVDD.

RP1 commute des liens SPI et UART à quelques mégahertz plus la µSD : ~20 mA.

Cas d'usage

Situation Détail Total
Partie en cours + rafale Wi-Fi 3 RP2350 pleins (355) + 3 flash en lecture (60) + ESP en TX (379) + SD au repos ≈ 795 mA
Installation d'un jeu 3 RP2350 (355) + 2 flash en programmation et 1 en lecture (70) + SD en lecture (50) + ESP en RX (112) ≈ 590 mA
Partie en cours, radio au repos 3 RP2350 (355) + 3 flash (60) + ESP en RX (112) ≈ 530 mA

Conséquences

Dimensionner le régulateur 3,3 V pour au moins 1 A. Le pic de 795 mA laisse peu de marge à 1 A ; 1,5 A donne une réserve saine et coûte peu. Le TPS62132 retenu (3 V à 17 V d'entrée, 3 A) couvre largement, y compris les transitoires de l'ESP32.

L'ampli classe D est sur son propre rail. Ses 2,5 W crête ne transitent pas par le 3,3 V : à pleine puissance sur 4 Ω il tire de l'ordre de 500 mA sur SPKVDD, à prendre sur le 5 V. Un rail 1,8 V dédié alimente par ailleurs le DVDD du DAC.

L'alimentation par USB seule ne suffit pas au cas nominal. Un port USB annonce 500 mA par défaut. Le connecteur USB-C de la carte porte 5,1 kΩ sur CC1 et CC2, ce qui identifie un consommateur mais n'annonce aucun besoin : le courant disponible dépend de ce que la source propose sur ses propres résistances Rp — 500 mA sans annonce, 1,5 A ou 3 A si la source les signale. Le firmware doit mesurer les lignes CC pour savoir ce dont il dispose, ou la carte doit prévoir une entrée d'alimentation séparée.

Le pic de l'ESP32 est bref. Les 379 mA correspondent à des rafales d'émission de quelques centaines de microsecondes. Un bulk de 100 à 220 µF sur le rail 3,3 V, plus 10 à 22 µF au pied du module, absorbent ces transitoires sans solliciter le régulateur en régime établi.

Réduire le pic radio est possible. Les 379 mA correspondent au mode 802.11b à 19,5 dBm. En 802.11n à 13 dBm, le pic tombe à 258 mA. Une limitation de la puissance d'émission dans le firmware de l'ESP réduit d'autant le dimensionnement.


6. Latence et synchronisation

Étape Quand
RP1 décide la trame N pendant la trame N−2 (contrat actuel) ou N−1 (envoi anticipé)
RP1 envoie la display list au VSYNC (contrat actuel, 6 928 o à 25 MHz = 2,22 ms de transfert) ou dès que _draw() a fini
RP2 adopte la liste à sa ligne 480 de la trame en cours, comme liste de la trame suivante
RP2 compose la ligne jusqu'à 9 lignes d'avance, anneau de 12, sans s'arrêter entre les trames
RP3 affiche temps réel, 6 lignes d'avance sur le faisceau

Latence totale : deux trames plus le balayage avec le contrat de trame actuel — la liste envoyée au VSYNC arrive quand RP2 a déjà entamé la trame qui commence, elle sert donc à la suivante. Elle descend à une trame plus le balayage si RP1 envoie la liste dès qu'elle est prête, pendant la trame active, au prix d'une échéance sur la logique de jeu (≈ 13 ms au lieu de 16,7) ; RP2 double-bufferise la liste et le permet déjà (ArchitectureRP2 §5). Décidé le 2026-08-26 : envoi au VSYNC, deux trames. La logique de jeu garde la trame entière ; l'envoi anticipé reste possible sans rien changer côté RP2 si un jeu le demande un jour. Dans les deux cas la latence est entièrement déterministe.

Concrètement, RP2 adopte le paquet en attente quand son cœur 0 compose la ligne 480 de la trame en cours (la première ligne de contrôle), comme liste et blocs de contrôle de la trame suivante — et non à la ligne 0 de celle-ci. Le paquet parti au VSYNC est arrivé 2,22 ms après le front (6 928 o depuis le 2026-08-27 ; 1,81 ms pour les 5 648 o d'avant), l'adoption a lieu ≈ 14 ms plus tard : ~14 ms de marge, et c'est cette adoption anticipée qui fait deux trames et non trois. La contrainte historique « tenir dans le vblank » est caduque. Établi au bring-up du 2026-08-26 (ArchitectureRP2 §3, « Cadence de service » ; récapitulatif §10).

Zéro tearing par construction : RP3 possède le temps vidéo (il a le HSTX), demande chaque ligne par HREQ et exporte VSYNC ; RP2 est cadencé par ces demandes et vérifie sur VSYNC qu'il est en pas. HSYNC est câblé mais inutilisé.

Il n'existe pas de relation maître/esclave entre les puces, seulement un genlock.


7. Répartition des GPIO

RP1 RP2 RP3
Utilisés 48 26 35
Libres 0 22 13
Détail ArchitectureRP1.md ArchitectureRP2.md ArchitectureRP3.md

La NOR QSPI de chaque puce est sur le port XIP dédié et ne consomme aucun GPIO.

RP2350B (QFN80, 48 GPIO) obligatoire pour les trois. Sur un RP2350A (30 GPIO), le HSTX occupe GPIO12-19 et coupe la banque en deux : aucune plage de 16 broches consécutives ne subsiste pour le bus overlay.


8. Firmware

Les répertoires RP1/, RP2/ et RP3/ du dépôt papyDeck_FW portent chacun le firmware de la puce correspondante : trois binaires distincts, trois projets CMake séparés, pas un exécutable unique paramétré.

Le firmware s'écrit en C. Pas de C++. L'assembleur PIO (.pio) est incontournable ; l'assembleur ARM, en ligne ou en module, est autorisé là où la mesure le justifie — en pratique la boucle de blit de RP2.

Le CMake de chaque projet déclare néanmoins LANGUAGES C CXX ASM : le pico-sdk déclare des cibles C++ en interne et CMake refuse de générer sans. Aucune ligne de C++ n'est écrite.

L'étage de sortie HSTX de RP3 s'appuie sur pico_hdmi de fliperama86, sous Unlicense, vendoré sans modification sous third_party/.


9. Ordre de prototypage

Chaque étage est testable isolément. État au 2026-08-26, jour de réception de la carte :

  1. RP3 seul — fond NOR, table de mélange (le color-key initial a été remplacé), HSTX. Fait : première image, copy_to_ram, fond tuilé défilant, late = 0.
  2. Bus RP2 → RP3 genlocké — XOSC partagé, HREQ par ligne, capture PIO, calibration de phase. Fait : délai 12, fenêtre 10-13, 0 erreur sur 11 M de mots.
  3. Sprite engine RP2 — boucle de blit mesurée en isolation avant intégration. Fait : blitter asm à intervalles opaques, 3 000 px opaques par ligne dans le budget, display list reçue de RP1.
  4. RP1 — Lua, display list, palette : faits (jeu de test à 220 sprites sur la carte). Pool possédé par RP1, µSD, ESP : ouverts. Aucune contrainte temps réel.

10. Contrats inter-puces — ce que le simulateur doit reproduire

Tout ce qui franchit une frontière de puce, tel que le code le fait au 2026-08-27 (branches rp1-pool, puis rp1-fond le soir : caméra et carte du fond pilotées par RP1). Petit-boutiste partout, offsets en octets, image mémoire des structs C. Le détail reste dans le document de chaque puce : ArchitectureRP1.md §10 (l'API gfx et la boucle de trame vues par un jeu), ArchitectureRP2.md §5 (paquet) et §8 (rendu pixel exact : ordre, clipping, flips, dépassement), ArchitectureRP3.md §4 (blocs de contrôle et leur application) et §5 (scène). Ce qui est dit « conception » ici n'est pas sur le fil.

10.1 Horloges et temps vidéo — propriétaire : RP3

Grandeur Valeur
clk_sys 252 MHz sur RP1 (rp1_papydeck), RP2 et RP3 ; clk_hstx 126 MHz ; pixel 25,2 MHz
Mode VIC 1, 640×480@60 : 800 × 525 ; ligne = 8 000 cycles = 31,746 µs ; vblank 45 lignes (front porch 10, sync 2, back porch 33). Mode DVI forcé, pas d'audio
VSYNC exporté RP3 GP26 → RP2 GP45, RP1 GP39. Actif haut : monte à la première ligne de l'impulsion V (après le front porch), retombe à la ligne active 0 — ≈ 35 lignes ≈ 1,11 ms. Le front montant est la référence de tout le monde. Mesuré 59,99 Hz
HSYNC exporté RP3 GP27 → RP2 GP44 : impulsion GPIO par ligne active depuis l'IRQ, 480 par trame ; inutilisé par RP2 (passage en PIO : ouvert)
Budgets RP1 : la trame, 16,7 ms ; RP2 : 16 000 cycles par ligne et par cœur (falaise mesurée) ; RP3 : 8 000 cycles par ligne

10.2 Lien 1 — RP1 → RP2, la display list — propriétaire : RP1

Élément Valeur
Transport SPI1 maître sur RP1 (SCK GP26, MOSI GP27, MISO GP28 inutilisé, CS GP29 en GPIO manuel) → esclave PIO pio1 SM0 sur RP2 (SCLK GP2, MOSI GP4, MISO GP3 inutilisé, CS GP5), GPIOBASE = 0
Cadence de bit 25 MHz, mode 0, MSB first, 8 bits, DMA ; CS bas sur tout le paquet
Récepteur cadré par CS : attend CS haut puis bas, puis compte exactement 6 928 × 8 bits ; autopush 32 à gauche ⇒ bswap32 de chaque mot à l'adoption ; DMA de 1 732 mots vers deux tampons alternés ; l'IRQ ne vérifie que le magic (bad++ sinon, paquet ignoré) et lance les ordres de chargement du paquet
Paquet 6 928 octets = 2,22 ms (5 648 o et 1,81 ms jusqu'au 2026-08-27 après-midi, avec deux blocs de contrôle) ; envoyé depuis l'IRQ du front montant de VSYNC, donc à cheval sur la trame active

Paquet PDDL :

Offset Type Contenu
0 u32 magic 0x4C444450 (octets P D D L) — seul champ vérifié
4 u16 count, 0..256 (> 256 tronqué à 256 par RP2)
6 u16 seq, incrémenté par RP1, non vérifié
8 u16 n_loads, 0..16 (> 16 tronqué à 16 par RP2)
10 u16 réservé, 0
12 u32 réservé, 0
16 256 × 16 entrées ; seules les count premières sont lues, pas de terminateur ; RP1 met les suivantes à zéro
4 112 640 bloc de contrôle A (PDPA : palette, backdrop, drapeaux et caméra du fond), relayé tel quel en ligne 480 du bus
4 752 640 bloc de contrôle B (PDPB : modes), relayé tel quel en ligne 481
5 392 640 bloc de contrôle C (PDPC : écritures de carte du fond), relayé tel quel en ligne 482
6 032 640 bloc de contrôle D (réservé, zéro), relayé tel quel en ligne 483
6 672 16 × 16 ordres de chargement (RP2_DL_CTRL_END) ; seuls les n_loads premiers sont lus ; RP1 met les suivants à zéro

Entrée (16 octets) :

Offset Type Contenu
0 u32 srcoffset des pixels du bitmap dans le pool de RP2, alloué par RP1 ; multiple de 4 ; intervalles à src − 4·h
4 i16 x, coin haut-gauche
6 i16 y
8 u16 w — rempli par RP1 depuis l'annuaire de l'atlas
10 u16 h — idem
12 u16 stride — idem, (w + 3) & ~3
14 u8 flags : bit 0 FLIP_H, bit 1 FLIP_V, autres ignorés (RP1 masque à 3)
15 u8 pad, 0

Ordre de chargement (16 octets) :

Offset Type Contenu
0 u32 nor_off — offset du blob (intervalles + pixels) dans la flash de RP2, depuis son début ; multiple de 4
4 u32 pool_off — offset de destination dans le pool ; multiple de 4
8 u32 len — octets, ≠ 0, multiple de 4
12 u32 pad, 0

Règles :

Règle Valeur
Clipping RP1 (exact depuis le 2026-08-27) entrée écartée ssi x + w ≤ 0, y + h ≤ 0, x ≥ 640 ou y ≥ 480 (clipped++), w, h venant de l'annuaire ; id inconnu ⇒ unknown++ ; bitmap pas dans le pool à temps ⇒ sprite sauté, missing++ ; liste pleine (256) ⇒ dropped++ — tout silencieux
Clipping RP2 (exact) entrée écartée ssi y + h ≤ 0, y ≥ 480, x + w ≤ 0 ou x ≥ 640, sinon coupée aux quatre bords ; y0 clampé à 0 pour le tri par première ligne visible
Validation des entrées RP2 n'a aucune table : w = 0, h = 0, w > stride, src non multiple de 4, src < 4·h ou src + stride·h > 327 680 ⇒ entrée écartée, liste compactée dans l'ordre, rejetees++, invisible pour RP1 ; sinon src + base du pool ; count = 0 ⇒ trame vide
Ordres de chargement exécutés par RP2 dès la réception du paquet (IRQ DMA), un DMA 32 bits par ordre alimenté par le stream FIFO du XIP (jamais un DMA direct sur la flash : il affame l'émetteur du bus), enchaînés (file de 32) ; len ≠ 0, offsets et longueur multiples de 4, pool_off + len ≤ 327 680, nor_off + len ≤ taille de la flash, sinon bad++ ; file pleine ⇒ dropped++ ; ordre encore en vol à l'adoption ⇒ late++ (la liste est adoptée quand même)
Même paquet référençable une liste peut référencer un bitmap chargé par son propre paquet : les ordres s'exécutent ~13 ms avant l'adoption. RP1 tient l'anti-tearing : pages d'un bitmap déchargé réutilisables à la trame f + 3 seulement ; ≤ 16 ordres et ≤ 96 Kio par paquet
Flips V : rangée source h−1−(Y−y) ; H : colonne w−1−cx, l'intervalle opaque [x0, x1) devient [w−x1, w−x0) ; (x, y) reste le coin haut-gauche de la boîte
Pixel écrit ssi dans l'intervalle opaque de sa rangée et index non nul (l'intervalle fait autorité) ; rangée vide (x1 ≤ x0) sautée
Génération des blocs gen part à 1 sur RP1, rp1_gfx_palette_init() la monte (2 au démarrage), +1 à chaque modification effective (valeur identique ⇒ pas de montée) ; blocs A et B reconstruits ssi gen a changé — A aussi quand la caméra ou les drapeaux du fond changent, sans monter gen — mais envoyés à chaque paquet ; bloc C reconstruit à chaque trame, seq +1 ; ctrl == NULL (superviseur) ⇒ blocs à zéro = gen 0 = RP3 garde sa palette, sa caméra de bring-up et sa carte NOR
Cadence RP1 un paquet par front VSYNC, depuis l'IRQ : la liste soumise pendant la trame précédente. board_dl_submit() renvoie toujours true ; liste remplacée avant envoi ⇒ replaced++ ; DMA précédent encore en vol ⇒ skipped++ ; pas de liste prête ⇒ rien n'est envoyé
Adoption RP2 par le cœur 0, quand il compose la ligne 480 de la trame f : le paquet en attente devient la liste et les blocs de contrôle de f+1 ; deux paquets reçus entre deux adoptions ⇒ le premier est écrasé. Sans paquet en attente, RP2 construit sa scène de test locale (RP2_TEST_SCENE), pas la liste précédente — comportement de bring-up, à remplacer par « répéter la trame précédente » (§10.8)
Contrainte réelle le paquet doit être reçu avant cette ligne 480 : arrivée à VSYNC + 2,22 ms, adoption ≈ 14 ms plus tard, ~14 ms de marge. « Tenir dans le vblank / ≥ 23 MHz » est caduc ; le PIO se justifie par la marge et la simplicité (le PL022 esclave plafonne à 12,5 MHz, soit 4,4 ms, qui tiendraient aussi)

10.3 Lien 2 — RP2 → RP3, le bus overlay — cadence : RP3

Élément Valeur
Câblage RP2 GP26-41 = OVERLAY_0..15, CLK GP42 (oscilloscope seulement), HREQ GP43 (entrée), HSYNC GP44 (non configuré), VSYNC GP45 ; RP3 VSYNC GP26, HSYNC GP27, HREQ GP28, CLK GP29, bus GP30-45 avec GP30 = OVERLAY_15 (miroir : bit capturé n = OVERLAY_(15−n)). GPIO26-45 des deux côtés, GPIOBASE = 16
Convention normative pixel 2k sur OVERLAY_0..7, pixel 2k+1 sur OVERLAY_8..15 (autopull à droite depuis un tampon 8 bpp en ordre naturel)
Transaction ligne RP3 : pull count (319), pull delay (12), lève HREQ (side-set), attend delay + 1 cycles, puis 320 × in pins, 16 toutes les 6 cycles clk_sys (42 MHz), autopush 32 à droite ⇒ 160 mots ; HREQ retombe au retour au pull. RP2 : out x = count, attend HREQ bas puis haut, puis 320 × (out pins, 16, 3 cycles haut, 3 bas). Aucun contrôle de flux, latence fixe : RP2 pas prêt ⇒ RP3 capture du vide et RP2 rate le front
Bloc DMA RP2 161 mots : [319] puis 160 mots de 4 pixels (644 o)
Mot reçu RP3 depuis le LSB : rev8(px 4i+1), rev8(px 4i), rev8(px 4i+3), rev8(px 4i+2) ; l'inversion de bits est absorbée par la table stockée à rp3_blend[(rev8(ov) << 8) \| bg], l'échange par le placement des décalages dans compose.c
Phase RP3_RX_PHASE_DELAY = 12 ; fenêtre sans erreur 10-13 (4 cycles = 15,9 ns) ; 0 erreur sur 11 M de mots
Trame bus 484 lignes : 0-479 pixels, 480 = bloc A, 481 = bloc B, 482 = bloc C, 483 = bloc D — ceux du paquet adopté pour la trame suivante ; demandés par HREQ exactement comme des lignes, dès que la ligne 479 est composée (RP3 a jusqu'à 6 lignes d'avance : pendant les dernières lignes actives, pas « pendant le vblank ») ; reçus dans ctrl_raw, dé-mirrorés octet à octet. Le nombre de lignes de contrôle est pair, obligatoirement : voir l'anneau
Anneau RP2 12 slots de 161 mots (7 728 o) ; numérotation absolue L, trame = L / 484, ligne = L % 484 ; cœur 0 = lignes paires (dont 480 et 482), cœur 1 = impaires (481, 483), amorçage lignes 0..9 ; l'adoption du paquet se fait sur le cœur qui compose la ligne 480 — avec un compte impair (483, essayé le 2026-08-27) elle tombait une trame sur deux sur le cœur 1 et les deux cœurs s'attendaient pour toujours (RP2 muet, RP3 comptant un bloc invalide par trame) ; garde L − next_arm ≤ 9 ; l'IRQ DMA arme toujours la ligne suivante (late++ si le slot n'était pas composé : la ligne sort avec le contenu de L−12) ; au VSYNC, (next_arm − 1) % 484 == 0 sinon desync++ + resync. Mesuré 2026-08-26 : late = 0, desync = 0, lead_min = 8 ; 2026-08-27 (484 lignes) : late = 1, desync = 1 au démarrage, txstall = 2, 0 rejet
Cadence RP3 après VSYNC : 20 µs de grâce, caméra (celle de RP1 si le bit 0 du bloc A est levé, sinon le rebond de bring-up), puis lignes 0-479 : fond, attente de l'overlay, armement de la suivante, throttle à 6 lignes d'avance sur le faisceau (RP3_LINE_BUFFERS − 2), compose, publie ; puis les 4 lignes de contrôle ; ctrl_apply(). En début de trame RP3 demande 6 lignes en rafale (~27 µs) : RP2 doit tenir cette cadence, pas celle du balayage
Repli RP3 palette de la scène NOR (PDSC, palette_off ∉ {0, 0xFFFFFFFF}, 256 entrées index 0 compris), sinon rampe RGB332 ; backdrop noir ; ensuite seuls les blocs de contrôle changent la palette

10.4 Blocs de contrôle — RP1 écrit, RP2 relaie, RP3 applique

Bloc A (640 octets, ligne 480) :

Offset Type Contenu
0 u32 magic 0x41504450 (P D P A)
4 u32 gen
8 u16 backdrop, RGB565
10 u16 flags : bit 0 RP3_CTRL_FLAG_CAMERA — RP1 pilote la caméra ; bit 1 RP3_CTRL_FLAG_LOCAL — carte locale ; autres bits 0, ignorés
12 i16 cam_x, caméra en pixels monde, lu ssi le bit 0 est levé
14 i16 cam_y, idem
16 256 × u16 palette RGB565 ; l'entrée 0 est ignorée par RP3
528 112 zéro

flags, cam_x, cam_y sont lus par RP3 à chaque paquet, indépendamment de gen ; le reste du bloc A ne s'applique qu'avec une génération nouvelle.

Bloc B (640 octets, ligne 481) :

Offset Type Contenu
0 u32 magic 0x42504450 (P D P B)
4 u32 gen
8 u32[2] pad
16 256 × u8 mode par index ; mode[0] ignoré
272 368 zéro

Bloc C (640 octets, ligne 482) — écritures de carte du fond, depuis le 2026-08-27 :

Offset Type Contenu
0 u32 magic 0x43504450 (P D P C)
4 u32 seq, +1 par trame côté RP1 ; RP3 n'applique qu'un seq différent du dernier (RP2 re-relaie le même paquet quand aucun nouveau n'arrive)
8 u16 n_runs
10 u16 bytes, octets de runs depuis l'offset 16 (informatif, non lu par RP3)
12 u32 pad
16 ≤ 624 runs à la suite : {u8 x, u8 y, u8 n, u8 flags = 0} puis n × u16 entrées de carte (format de la carte de scène : id bits 0-9, flip H bit 10, flip V bit 11), chaque run arrondi à 4 octets ; x, y en tuiles modulo la fenêtre 128 × 64 ; n ≤ 255

Bloc D (640 octets, ligne 483) : réservé, zéro, ignoré par RP3 — présent pour que le compte de lignes de contrôle soit pair (§10.3).

Règle Valeur
Modes 0 TRANSPARENT, 1 OPAQUE, 2 TRANSLUCENT, 3 ADDITIVE, 4 SHADOW, 5 BEHIND ; inconnu ⇒ TRANSPARENT
Partition par défaut 0 transparent ; 1-191 opaque (1-190 l'art, 191 l'encre système) ; 192-223 translucide ; 224-239 additif ; 240-247 ombre ; 248-255 derrière (identique dans RP1/src/gfx.c et RP3/src/blend.c)
Arithmétique (par canal, RGB565) OPAQUE = couleur du sprite ; TRANSLUCENT = moyenne exacte sprite/fond ; ADDITIVE = addition saturée ; SHADOW = fond >> 1 (couleur du sprite ignorée) ; BEHIND = couleur du sprite si l'index de fond vaut 0, sinon fond ; TRANSPARENT = fond. Toute cellule dont l'index de fond vaut 0 prend le backdrop à la place de palette[0] — le trou du fond, c'est « rien de dessiné »
Application RP3 — palette et modes ssi les deux magics sont bons, gen ≠ 0, gen ≠ dernière appliquée, gen A = gen B (sinon ignoré ; « différente », pas nécessairement croissante). Diff contre l'état courant (index 1..255, couleur ou mode ; backdrop = index 0) ; ≤ 16 index changés (RP3_CTRL_PARTIAL_MAX) ⇒ rp3_blend_update_index (rangée + colonne) chacun, sinon rp3_blend_build complet (1,2-5 ms). Tourne après la ligne 483, avant la ligne 0 suivante : jamais de trame partiellement mise à jour
Application RP3 — caméra et drapeaux (bloc A) à chaque paquet dont les magics A et B sont bons, indépendamment de gen : bit 0 ⇒ cam_x/cam_y deviennent la caméra de la trame suivante (camera_step), sinon le rebond de bring-up continue ; bit 1 ⇒ rp3_background_set_local : la fenêtre 128 × 64 est le monde, torique (1 024 × 512 px), jamais rechargée depuis la NOR ; retour à 0 ⇒ rechargement complet de la fenêtre. Caméra au pixel entier, non bornée par RP3
Application RP3 — carte (bloc C) avant A et B, indépendamment d'eux : magic faux ⇒ map_bad++ ; seq déjà appliqué ⇒ rien ; sinon chaque run est écrit dans la fenêtre (rp3_background_map_write, torique) ; run débordant du bloc ⇒ map_bad++ et arrêt du bloc. En mode local les écritures sont le monde ; en mode NOR ce sont des sur-impressions effacées par la prochaine recharge de la rangée/colonne. Rapport RP3 : map seq= runs= ent= bad= [cam=rp1] [local]
Coût mesuré (2026-08-26) 56 258 cycles pour 5 index + backdrop ; 160 000 cycles pour 12 index — ≈ 10 000-13 000 cycles par index selon la mesure, 0,63 ms de vblank ; late = 0
Palette partagée scripts/pdpal.pyRP3/build/palette.bin : 256 × u16 RGB565 LE ; 0 = noir réservé ; 1..190 couleurs opaques (median-cut) ; 191 = blanc, encre système (fonte, dashboard — jamais quantifié) ; 192..255 = (0,0,0), réservées aux effets ; quantification : alpha ≥ 128 ⇒ plus proche des 1..190, sinon 0. RP1 l'embarque (generated/scene_palette.h) et la possède au démarrage ; RP3 la lit aussi dans le blob de scène

10.5 Latence bout en bout — décidée le 2026-08-26

_draw() pendant la trame k (après VSYNC k) → paquet au VSYNC k+1 (2,22 ms) → adoption par RP2 à sa ligne 480 de k+1 → composée et affichée à la trame k+2 : deux trames plus le balayage. Sprites, palette, caméra et écritures de carte d'un même paquet atterrissent sur la même trame, atomiquement. Seuls les gfx.sprite faits dans _draw sont affichés (ArchitectureRP1 §10). Mesure au chronomètre : ouverte.

10.6 Propriétaires d'état

État Propriétaire
Palette, modes, backdrop (gen) RP1
Display list RP1
Annuaire de l'atlas (w, h, stride, offset NOR par bitmap) RP1 — embarqué à la compilation depuis RP2/build/sprites.bin (interim : viendra de la µSD) ; RP2 n'a plus de table pour les listes de RP1
Carte du pool, allocation, ordres de chargement RP1 — implémenté le 2026-08-27 (RP1/src/pool.c, pages de 64 o, quarantaine 3 trames) ; RP2 exécute et vérifie les bornes
Scène de test RP2 (repli)
Caméra du fond RP1 — implémenté le 2026-08-27 : gfx.camera(x, y), pixels monde entiers, bloc A (bit 0, cam_x, cam_y), appliquée par RP3 à la trame suivante ; tant que le bit 0 n'est pas levé, RP3 garde son rebond de bring-up (départ (0,0), vitesse (3,1) px/trame, rebond dans [0, monde − écran])
Carte du fond RP1 en mode local (gfx.bg_local(true), bit 1 du bloc A) : la fenêtre 128 × 64 de RP3 est le monde, écrite par les runs du bloc C (gfx.tile, gfx.tiles, gfx.text), torique, jamais rechargée — le mode texte. Sinon : la carte du monde dans la NOR de RP3, streamée par fenêtre ; les écritures de RP1 n'y sont que des sur-impressions non persistantes
Scène tuilée, tileset NOR de RP3 ; le tileset ne se change pas à chaud (pas d'API) ; tuiles 704-1023 réservées au système : deux fontes pour 32..95 — 16×16 fight16.png (devurandom, OpenGameArt ; 4 tuiles par glyphe) à 704 + 4·(code − 32), 8×8 font8x8arcade.png à 960 + (code − 32), bloc plein = case de # du 8×8 (RP3/assets/, mkscene.py) —, encre 191
Temps vidéo, VSYNC, HSYNC, HREQ RP3
RUN, straps, reset RP1

10.7 Formats en NOR

Blob Résumé Détail
Atlas PDSP NOR RP2 à 0x10100000 ; en-tête 16 o (magic 0x50534450, n_anims ≤ 32, n_bitmaps ≤ 512, pool_bytes — peut dépasser le pool), puis anims 8 o, bitmaps 12 o, pool ; animations 0 et 1 = fontes système (bitmaps 0..63 en 16×16, 64..127 en 8×8, caractères 32..95, encre 191), l'art à partir de 128 ; intervalles opaques h × {u16 x0, u16 x1} à off − 4·h ; stride = (w + 3) & ~3 ; 0 = transparent. Le pool de 320 Kio de RP2 est à RP1, qui embarque l'annuaire (en-tête + tables) et ordonne les chargements blob par blob (nor_off = 0x100000 + tables + off − 4·h) ; la copie entière au boot ne sert plus qu'à la scène de test locale de RP2 (repli) ArchitectureRP2.md §4, §5 et §8, ArchitectureRP1.md §7, RP2/tools/mksprites.py
Scène PDSC NOR RP3 à 0x10100000 ; en-tête 32 o (magic 0x43534450, u16 world_w, u16 world_h, u32 tileset_off, u32 map_off, u32 palette_off, 12 o zéro — offsets absolus depuis le début de la flash) ; tileset 1 024 tuiles 8×8 (64 Kio) ; carte u16 par blocs 16 × 16, id bits 0-9, flip H bit 10, flip V bit 11 ArchitectureRP3.md §4 et §5, RP3/tools/mkscene.py
Palette RP3/build/palette.bin, 256 × u16 RGB565 LE ; embarquée dans rp1_papydeck et dans PDSC §10.4, ArchitectureRP1.md §10, scripts/pdpal.py

10.9 Le dossier d'un jeu — groupes de palette, variantes, fonds — décidé le 2026-08-28

Le dossier du jeu, celui que l'App publie et que la carte porte (Notes-uSD §2, §14), est ce que gfx.bundle() compile d'un coup. (Il s'est appelé « un bundle = un jeu » jusqu'au 2026-09-01 ; dans l'App, un bundle est un groupepapyDeck_App/MODELE.md.) Il contient trois sortes de choses, et le mot « palette » en recouvre deux qu'il faut séparer :

Chose Fichiers Ce que c'est
Groupe de palette <g> <g>.pal (768 o : 256 RGB565 + 256 modes de rangée), <g>.atlas (Kivy, multi-pages), les PNG indexés que l'atlas nomme (<g>-<asset>.png) une unité de compilation : la palette et les images quantifiées avec elle. Un pixel indexé n'a de sens que dans la palette qui l'a produit. Un jeu en a un ou plusieurs (commun, un par niveau…)
Variante <g>.<v> <g>.<v>.pal (768 o) les mêmes index, d'autres couleurs : jour/nuit, flash, ambiance. Aucune image. Transférable en temps réel
Fond <g>-<f> <g>-<f>.map (texte, §13 de Notes-uSD), <g>-<f>-tiles.png (≤ 704 tuiles) un tileset + une carte, quantifiés dans le groupe <g>

Le fondu enchaîné n'est pas dans le bundle : RP1 interpole en RGB565 entre la palette courante et une autre, pas à pas, et RP3 reconstruit ses rangées de mélange par différence. Rien à précalculer.

Sur la console, tout est installé une fois et choisi en jeu — la NOR de RP2 (16 Mo) reçoit un seul PDSP avec les bitmaps de tous les groupes (ids uniques, régions nommées <g>/<nom>), celle de RP3 tous les fonds côte à côte (un PDSC de 128 Kio chacun, à des offsets successifs), RP1 garde toutes les palettes et variantes en RAM (768 o chacune) et l'annuaire.

API (Lua, identique sur la console et dans le simulateur) Effet
gfx.bundle() compile et installe le jeu entier — des secondes la première fois, rien avec le cache (papydeck/cache/<empreinte>/)
gfx.palette("niveau1"), gfx.palette("niveau1.nuit") la palette active, en temps réel (blocs A/B, ~21 trames pour 256 index changés) ; le jeu ne dessine que des images du groupe actif — c'est sa responsabilité, comme dans le simulateur
gfx.fade("niveau2", trames) interpolation vers une autre palette ou variante, en trames (60/s). Se lance UNE fois — chaque appel repart de zéro. Dans le simulateur aussi (« firmware seulement » jusqu'au 2026-09-03 : périmé)
gfx.background("niveau2-grotte") RP3 charge une autre scène de sa NOR : ~2 ms, sans écran noir
gfx.id("niveau1/rock_0") l'id de bitmap d'une région, pour gfx.sprite

Limites : 2 048 bitmaps par jeu (annuaire de RP1, 12 o l'entrée ; le pool d'un PDSP commence à 32 Kio pour loger les tables), 704 tuiles par fond, 256×128 tuiles par carte, images ≤ 65 536 px décodés. Le manifeste ne dit rien de tout ça : le dossier est la liste, les préfixes font les liens.

10.8 Écarts de contrat — connus, corrigés ou ouverts

Point Contrat voulu État
Ordre de peinture RP2 l'ordre de la display list est l'ordre de peinture, le dernier est devant Conforme depuis le 2026-08-27 (liste active triée par index de liste ; autotest : 7 120 pixels faux avec l'ancienne marche, 0 avec la nouvelle). Jusque-là, tri par première ligne visible + swap-remove : un sprite déclaré après mais commençant plus haut passait derrière — ArchitectureRP2 §8
gfx.stats().repeats compte les trames parties sans nouvelle liste Compté depuis le 2026-08-27 par les fronts VSYNC écoulés entre deux tours de boucle (vérifié : +1 par raté provoqué). Jusque-là jamais incrémenté
Repli sans paquet RP2 répète la trame précédente Ouvert : RP2 construit sa scène de test locale (RP2_TEST_SCENE) — bring-up, à remplacer
Caméra du fond locale à RP3 la caméra et la carte du fond sont au jeu, sur RP1 Conforme depuis le 2026-08-27 (branche rp1-fond) : bloc A étendu (drapeaux, cam_x, cam_y), bloc C d'écritures de carte, trame bus de 484 lignes, mode local = mode texte ; late = 0, 12 runs / 144 entrées par 120 trames comptés identiques sur RP1 et RP3. Restent ouverts : fonte système et écran de démarrage, écritures persistantes en mode NOR, tileset à chaud
src = id de bitmap, résolu par RP2 RP1 possède le pool, src = offset dans le pool, RP2 sans table Conforme depuis le 2026-08-27 : annuaire chez RP1, ordres de chargement dans le paquet, rejetees = 0, late = 0 mesurés ; éviction LRU côté RP1 le même jour (pool réduit à 64 Kio : full = 0, 0 rejet). Ouvert : compaction, annuaire depuis la µSD