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 :
- 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. - 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.
- 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.
- 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 | src — offset 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.py → RP3/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 groupe — papyDeck_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 |