Skip to content

RP3 — mixeur vidéo

Note

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

Troisième étage du pipeline papyDeck. Mixe l'overlay reçu de RP2 avec le fond tuilé lu depuis sa propre NOR, applique la table de mélange, sort en HDMI par le HSTX. L'insertion de l'audio dans le flux est une conception, non implémentée (§6) : le firmware force le mode DVI.

Vue système : ArchitectureRoot.md — la vue d'ensemble des contrats inter-puces (transport, formats, cadences, propriétaire de chaque état) est son §10 ; ce document garde le détail côté RP3 (§3, §4, §5). Firmware : RP3/ (dépôt papyDeck_FW). Tout ce qui est marqué « mesuré » l'a été sur la carte V1 alpha (../papyDeck_HW/rpGPU_V1) le 2026-08-26.


1. Brochage

GPIO Fonction Sens
0-1 Console vers RP1 (son UART1, GP4/GP5), relayée sur l'USB de RP1 — seul lien RP1↔RP3 existant. Mise à jour A/B et stub UART : conception, non implémentés UART0
2-5 Réservés : assets et audio depuis RP1 en SPI0 esclave — conception, non implémenté : aucun code RP3 ne touche GP2-5
12-19 HSTX / HDMI — figé au silicium sortie
20 HPD depuis le TPD12S521 — câblé, non lu par le firmware (le HSTX tourne sans condition) entrée
26 VSYNC vers RP1 et RP2 sortie
27 HSYNC vers RP2 sortie
28 HREQ vers RP2 sortie
29 Horloge du bus overlay entrée
30-45 Bus overlay 16 bits depuis RP2 — GP30 porte OVERLAY_15 entrée, PIO
6-11, 21-25, 46-47 libres — 13 broches (le commentaire de config.h compte aussi 2-5 libres tant que le SPI n'existe pas)

Le port QSPI dédié porte la NOR de 16 Mo contenant le fond ; il ne consomme aucun GPIO.

Broches à fonction d'horloge

Les fonctions clk_gpin* et clk_gpout* du RP2350 sont confinées à GPIO12-25, y compris sur la variante B (io_bank0.h, FUNCSEL 0x09) :

Fonction GPIO possibles Disponible après le HSTX
clk_gpin0 12, 20 20
clk_gpin1 14, 22 22
clk_gpout0 13, 21 21
clk_gpout1 15, 23 23
clk_gpout2 24 24
clk_gpout3 25 25

Le genlock n'en utilise aucune : il passe par XIN, hors matrice d'E/S (les trois puces partagent le même oscillateur 12 MHz, rien à configurer). GP21 est réservé comme sortie de mesure du verrouillage (RP3_PIN_CLK_MEASURE, clk_gpout0) : défini dans config.h, jamais configuré par le firmware — le verrouillage a été constaté autrement, par la fenêtre de phase du bus (§4).

Contraintes PIO

Le bus overlay et ses signaux de service occupent GPIO26-45, ce qui impose GPIOBASE = 16 à son bloc PIO — celui-ci voit alors GPIO16-47 et perd l'accès à GPIO0-15. Sans conséquence : le HSTX n'est pas du PIO et la console non plus.

Lien SPI depuis RP1 — conception, non implémenté

Le lien SPI depuis RP1, sur GP2-5, utiliserait soit le SPI0 matériel, soit un second bloc PIO à GPIOBASE = 0. Aucun firmware ne le configure aujourd'hui, ni côté RP3 ni côté RP1 (RP1/src/board.c : SPI1 GP26-29 va vers RP2, l'UART GP4/GP5 vers RP3) ; le tableau ci-dessous est le plan de câblage, à confronter à la netlist, pas au code.

Ici le choix est réellement ouvert, contrairement à RP2. En mode esclave le PL022 plafonne à clk_peri/12 = 12,5 MHz (fiche technique §12.3.4.4 : SSPCLK doit être 12 fois plus rapide que le SCK entrant), contre ~clk_sys/8 ≈ 30 MHz en PIO. Ce lien ne porte que des assets hors temps réel et l'audio — 48 kHz stéréo 16 bits = 1,5 Mb/s — donc les deux conviennent largement. À rouvrir si RP3 devait un jour recevoir un flux soutenu : c'est ce plafond qui a imposé le PIO côté RP2, voir ArchitectureRP2.md §1.

Correspondance prévue des broches en esclave :

Signal RP1 (maître) RP3 (esclave)
SCLK GP34 SPI0_SCLK — GP2
MOSI GP35 SPI0_RX — GP4
MISO GP36 SPI0_TX — GP3
CS GP37 SPI0_SS_N — GP5

2. Sortie HDMI

Brochage TMDS

Le HSTX sort sur GPIO12-19, figé par le silicium. Son crossbar permettrait de permuter voies et polarités ; il n'est pas configuré. La carte suit le défaut de pico_hdmi :

Signal Positif Négatif
Horloge GP13 GP12
D0 — bleu GP15 GP14
D1 — vert GP17 GP16
D2 — rouge GP19 GP18

La broche impaire de chaque paire porte le positif.

Aucun appel à video_output_set_hstx_pinout() dans le firmware, et il ne faut pas en ajouter.

Les adaptateurs du commerce — Adafruit DVI Sock, DVI Breakout, PiCowBell HSTX DVI — suivent la convention PicoDVI : polarité inverse et horloge à la place de D0. Ils ne sont pas compatibles avec ce câblage.

Résistances série

270 Ω sur chacune des 8 lignes, placées entre le RP2350 et le TPD12S521.

Le récepteur TMDS termine par 50 Ω vers +3,3 V. Un CMOS 3,3 V à travers 270 Ω donne un niveau bas de 3,3 × 270/320 = 2,78 V, soit un swing de 516 mV — centré dans la plage TMDS de 400 à 600 mV — pour 10,3 mA par ligne. À 220 Ω le swing monterait à 611 mV et le courant à 12,2 mA, soit la limite du calibre 12 mA des pads.

Le TPD12S521 n'ajoute que des clamps ESD sur des lignes traversantes : il ne termine ni ne régénère rien.

TPD12S521

Composant de port émetteur HDMI : ESD IEC 61000-4-2 niveau 4, décalage de niveau des lignes lentes, régulateur 5 V limité en courant pour la broche 18 du connecteur.

En HDMI, la source fournit le +5 V sur la broche 18 et le puits renvoie le HPD sur la broche 19. C'est la fonction du 5V_OUT du TPD12S521 — limité à 55 mA, court-circuit 90-175 mA — qui permet la détection du récepteur même éteint.

Ligne Côté logique (LV_SUPPLY = 3,3 V) Côté connecteur (5 V)
HPD HOTPLUG_DET_IN, broche 19 HOTPLUG_DET_OUT, broche 20 → HDMI 19
DDC SCL DDC_CLK_IN, broche 17 DDC_CLK_OUT, broche 22 → HDMI 15
DDC SDA DDC_DAT_IN, broche 18 DDC_DAT_OUT, broche 21 → HDMI 16
CEC CE_REMOTE_IN, broche 16 — non utilisé CE_REMOTE_OUT, broche 23 → HDMI 13
+5 V vers l'écran 5V_OUT, broche 38 → HDMI 18

HPD est câblé à la fois sur RP3 GP20 — prévu pour démarrer ou arrêter le HSTX, non lu par le firmware aujourd'hui (§1) — et sur RP1 GP16, qui pilotera la lecture de l'EDID (DDC/EDID : ouvert).

Composants externes, d'après le schéma d'application TI :

Composant Entre Rôle
100 kΩ HPD_IN ↔ V_LV pull-up côté logique
47 kΩ ×2 DCLK_IN, DDAT_IN ↔ V_LV pull-ups côté logique
10 kΩ V_LV ↔ LV_SUPPLY alimentation du bias des décaleurs
10 kΩ HPD_OUT ↔ GND pull-down : c'est lui qui fait lire « pas d'écran »
1,5 kΩ ×2 DCLK_OUT, DDAT_OUT ↔ 5 V pull-ups DDC, spec HDMI
100 nF ESD_BYP ↔ GND requis
100 nF 5V_OUT ↔ GND découplage de la sortie 5 V

LV_SUPPLY à 3,3 V : plage recommandée 1 à 5,5 V, typique 3,3 V.

Le CEC n'étant pas utilisé, CE_REMOTE_IN reste libre et le 27 kΩ + Schottky anti-backdrive de la note d'application est omis.

Les TMDS traversent le composant. Chaque signal a deux broches — TMDS_D2+ en 4 et 35 par exemple : la piste entre par l'une et ressort par l'autre, la protection ESD étant au milieu. Le pas du boîtier (0,5 mm) et l'ordre des broches suivent ceux du connecteur HDMI, pour un tracé traversant sans croisement. Ajout : 0,8 pF par ligne.

Alimentations : 5V_SUPPLY broche 1, LV_SUPPLY broche 2. Consommation 110 µA + 1 µA. Sans alimentation, le composant reste une protection ESD purement passive.


3. Mixage et CLUT

Réception DMA du bus overlay, zéro CPU → une seule lecture de table par pixel vers un line buffer → HSTX.

rgb565 = table_de_mélange[(index_fond << 8) | index_overlay]

C'est tout le compositeur : pas de test de transparence, pas d'indirection de palette, aucun branchement. La table est fond-majeure : les 256 entrées d'un même index de fond sont contiguës, ce qui permet à gfx.sky de donner au vide (bg == 0) la couleur de la ligne en recopiant UNE rangée de 512 octets par ligne composée (rp3_blend_ciel_ligne) — tout pixel de fond à 0, case vide ou pixel transparent d'une tuile, rend alors le ciel local, exactement.

Index physique, pour le simulateur. La formule ci-dessus est logique ; l'entrée overlay est rangée à rev8(index_overlay) parce que le bus livre chaque index avec ses bits inversés (§4). L'adresse réelle est donc rp3_blend[(bg << 8) | rev8(ov)], où ov est l'octet tel que reçu du bus et bg l'index de fond en ordre naturel (blend.c, compose.c). rev8(0) = 0 : l'entrée transparente reste en tête de chaque rangée.

La table est une fonction arbitraire f(overlay, fond) → RGB565 de 65 536 entrées, 128 Ko. Elle est construite au démarrage par calcul direct (rp3_blend_build) et reconstruite en entier seulement quand un bloc de contrôle change plus de RP3_CTRL_PARTIAL_MAX = 16 index (§4) ; en deçà, seules la rangée et la colonne de chaque index modifié sont recalculées, et c'est le cas normal, par trame. Coût de la reconstruction complète, compté sur les boucles internes du binaire :

mode d'une rangée cycles par entrée
opaque ~2,5 — GCC empile deux entrées par str de 32 bits
transparent, derrière < 1 — un memcpy de 512 o par rangée
ombre ~8
translucide ~11
additif ~19

Soit ~1,2 ms avec le partitionnement par défaut ci-dessous, et ~5 ms dans le pire cas où les 256 rangées seraient en additif. La reconstruction complète est une opération de changement de scène, à masquer derrière un fondu ; la mise à jour partielle (rp3_blend_update_index, rangée + colonne, 512 cellules, ≈ 10 000-13 000 cycles par index selon la mesure — §4) est l'opération par trame.

Espace d'index

Une rangée est la seule chose que la lecture sache sélectionner : tout attribut devant se résoudre contre le fond coûte soit une table entière, soit des rangées sur les 256 disponibles. Ajouter un effet ne coûte donc ni mémoire ni cycle — ça coûte des rangées.

rangées rôle
0 transparent — obligatoire, c'est la clé de couleur du blitter de RP2
1-190 opaques, l'art du jeu
191 opaque, encre système (blanc) : la fonte des tuiles système et le dashboard ; l'art n'y est jamais quantifié
192-255 effets : translucide, additif, ombre, « derrière le fond »

Un index dépensé pour la version à 50 % d'une couleur n'est plus disponible comme couleur opaque. La priorité devant/derrière est une rangée, pas un bit d'attribut.

Modes — valeurs et arithmétique

Chaque index porte une couleur (rp3_palette) et un mode (rp3_blend_modes). Les six modes sont numérotés de la même façon dans RP3/include/rp3/blend.h et dans l'API gfx de RP1 (RP1/src/gfx.c) ; l'arithmétique est celle de RP3/src/blend.c, par canal sur le RGB565 (R 5 bits, G 6 bits, B 5 bits), sans retenue entre canaux :

valeur mode cellule (sprite s sur fond f)
0 TRANSPARENT f
1 OPAQUE couleur de s
2 TRANSLUCENT moyenne de s et f, par canal, partie entière
3 ADDITIVE s + f par canal, saturé au maximum du canal
4 SHADOW f >> 1 par canal — la couleur de s est ignorée
5 BEHIND couleur de s si l'index du fond vaut 0, sinon f
autre traité comme TRANSPARENT

f est palette[index_fond], sauf pour l'index de fond 0, remplacé par le backdrop dans toute la colonne 0 (voir ci-dessous). Le mode de l'index 0 est toujours TRANSPARENT : c'est la clé de couleur du blitter, et ni les blocs de contrôle ni l'API RP1 ne peuvent le changer.

Partition par défaut (rp3_blend_default_modes, identique dans RP1/src/gfx.c) — celle du démarrage, et celle que RP1 renvoie tant qu'un jeu n'appelle pas gfx.effect :

index mode par défaut
0 transparent
1-191 opaque (191 = encre système)
192-223 translucide
224-239 additif
240-247 ombre
248-255 derrière

La palette partagée des outils d'assets (scripts/pdpal.py, RP3/build/palette.bin : 256 × u16 RGB565 petit-boutiste) suit la même partition : 0 = noir réservé, 1..190 = couleurs opaques par median-cut, 191 = blanc, l'encre système (pdpal.SYSTEM_INK, depuis le 2026-08-27), 192..255 = (0,0,0) réservées aux effets ; le quantificateur envoie un pixel d'alpha ≥ 128 sur le plus proche des 1..190, les autres sur 0 — jamais sur 191, qu'un jeu peut repeindre s'il renonce au texte système.

L'index 0 du fond est réservé, et impose un registre de backdrop

L'effet « derrière le fond » doit pouvoir distinguer un fond opaque d'un trou. Cette information n'existe nulle part ailleurs que dans l'index du fond lui-même :

L'index 0 du fond signifie « rien de dessiné ». Une rangée « derrière » rend la couleur du sprite là où le fond vaut 0, et le fond partout ailleurs.

Conséquence à respecter dans la production des assets : un fond ne doit pas utiliser l'index 0 comme couleur visible. Un fond qui veut du noir prend un autre index.

Corollaire, et il ne se voit pas au premier regard. L'index 0 étant désormais un marqueur des deux côtés — clé de couleur dans l'overlay, vide dans le fond — plus aucune entrée de palette ne décrit le pixel affiché quand rien n'est dessiné. table[0][0], à l'intersection de la rangée transparente et du fond vide, deviendrait ce que palette[0] contient par accident.

Il faut donc une couleur explicite :

rp3_backdrop — registre distinct de la palette, poussé par RP1 avec elle dans le bloc de contrôle A (§4). Dans le diff des blocs de contrôle il compte comme l'index 0 : un changement de backdrop recalcule la rangée et la colonne 0 (rp3_blend_update_index(0)), pas toute la table.

Il ne concerne pas que table[0][0] : toute cellule dont le fond vaut 0 le mélange. Un sprite translucide, additif ou d'ombre au-dessus du vide se compose contre le backdrop, pas contre palette[0]. C'est ce que vérifie test_blend.c, qui met du magenta dans palette[0] et exige qu'il n'atteigne jamais l'écran.

Une seule palette de 256 couleurs simultanées, sans banques : une banque par pixel imposerait une table de mélange par banque, et 4 × 128 Ko ne tient pas dans 520 Ko. Un jeu partitionne les 256 index à la place.

Format de sortie

L'expandeur de champs de bits du HSTX est configuré par pico_hdmi pour du RGB565 : deux pixels par mot de 32 bits, étendus en RGB888 sur le fil à coût nul.

Grandeur Valeur
Line buffer 1 280 octets
Scanout vers le HSTX 36,9 Mo/s
Palette 256 entrées × 16 bits = 512 o
Table de mélange 65 536 entrées × 16 bits = 128 Ko
Sortie 2 pixels par store de 32 bits

Le HSTX ne sait pas faire de lookup, et le DMA non plus proprement en 8→32 bits : l'expansion est faite par le CPU, scanline par scanline, dans un anneau de 8 line buffers (RP3_LINE_BUFFERS, composed[8][320], 10 240 o) dont l'overlay reçu a son jumeau (overlay[8][160], 5 120 o).

Budget

La rafale overlay est faite par DMA et ne consomme aucun temps CPU : la ligne N+1 est demandée à RP2 avant que la ligne N ne soit composée, si bien que les deux se recouvrent. Le cœur 0 dispose donc du temps-ligne entier.

cycles par ligne
Rafale overlay — 320 transferts × 6 cycles, sur DMA 1 920
Budget CPU de la composition 8 000, soit 12,5 cycles/px
Composition — 640 px × 6,5 à 8,5 cycles 4 160 à 5 440
Composition mesurée (DWT, carte V1 alpha, 2026-08-26, code C -O3 en SRAM) 4 543

Soit 52 à 68 % du budget estimé, 57 % mesuré — 7,1 cycles par pixel.

Les 6,5 à 8,5 cycles/px sont un compte d'instructions sur le code compilé à -O3 : 25 instructions pour quatre pixels, la borne basse supposant les ldr pipelinés à un cycle et la haute à deux. La boucle traite quatre pixels par mot de 32 bits, les deux chargements, le strd et le contrôle de boucle s'amortissant d'autant.

Le cœur 0 n'est cadencé par aucune interruption de ligne : c'est lui qui demande chaque ligne par HREQ, et il tourne donc aussi vite que le pipeline le permet — plus vite que le balayage. Un garde-fou l'empêche de dépasser RP3_LINE_BUFFERS - 2 lignes d'avance, faute de quoi il composerait dans le tampon que le DMA HSTX est en train de lire.

Pourquoi N-2 et pas N-1. pico_hdmi demande le pointeur de la ligne L quand la liste d'en-tête de L-1 se termine — c'est l'instant où le DMA commence à lire les pixels de L-1. Quand beam_abs vaut L, deux slots sont donc vivants : celui de L (demandé) et celui de L-1 (en cours de lecture). Une garde à N-1 laissait le cœur 0 écraser L-1 en pleine lecture ; vu à l'écran le 2026-08-26 comme une bande de 10 px à gauche décalée de 8 lignes. Le rapport porte un compteur collide qui lit l'adresse courante des deux canaux DMA avant chaque composition.

Trois compteurs encadrent le résultat, et ils se lisent ensemble :

  • throttled_lines — le cœur 0 a dû attendre le balayage. Élevé, c'est le signe que la marge est réelle.
  • late_lines — le cœur 0 est arrivé après le balayage ; la dernière ligne terminée est réaffichée. Toute valeur non nulle signifie que le budget ci-dessus ne tient pas.
  • skipped — une trame entière est passée sans être composée. Calculé en delta du tic de vsync, pas en inégalité : une inégalité masquerait exactement le cas qu'on cherche.

Numérotation des lignes — un invariant, pas un détail

Tous les compteurs de ligne sont absolus et monotones depuis le démarrage, jamais remis à zéro au vsync. C'est ce qui fait de la frontière de trame un cas ordinaire : au sommet de la trame N, composed_abs porte déjà la dernière ligne de N−1, et le chemin habituel « réafficher la dernière ligne terminée » couvre la transition.

Une numérotation par trame obligeait le cœur 0 à remettre le compteur à zéro pendant que le cœur 1 le lisait, ce qui se déchirait dans les deux sens — remise à zéro pas encore faite, et le callback rendait une ligne du bas de la trame précédente ; déjà faite, et il rendait le tampon que le cœur 0 était en train d'écrire. Ajouter une remise à zéro réintroduit ce défaut.

Les compteurs sont non signés et comparés en différence signée, (int32_t)(a - b), jamais avec < ou >. À 28 800 lignes/s un compteur atteint 2³¹ en 20 h 42, et le débordement signé est un comportement indéfini en C. L'idiome de la différence reste juste à travers le bouclage tant que l'écart est petit devant 2³¹ — ici moins de 500.

Deux barrières mémoire ordonnent les publications, volatile n'ordonnant que le compilateur et non le tampon d'écriture du M33 :

Ce qu'elle garantit
cœur 0, après rp3_compose_line() les pixels visibles avant le compteur qui les annonce
cœur 1, dans vsync_cb() la nouvelle base visible avant le tic qui la signale

Sans la seconde, le cœur 0 composerait la trame N avec la base de N−1 : toutes les lignes décalées de 480, donc toutes déclarées en retard, donc une trame entière figée.

Mesuré sur la carte le 2026-08-26, au compteur de cycles DWT du M33 : 4 543 cycles par ligne pour rp3_compose_line, late = 0 et skipped = 0 en régime établi, throttled ≈ 470 par trame — le cœur 0 passe l'essentiel de son temps à attendre le balayage. Deux conditions, apprises en les violant : le binaire en SRAM (copy_to_ram — en XIP la passe dépassait le temps-trame), et rien de bloquant sur le cœur 0 — un seul printf du rapport coûtait 657 lignes en retard, il est imprimé par le cœur 1.

Rapport console

Toutes les 60 trames, le cœur 0 formate une ligne dans un tampon et le cœur 1 l'imprime sur UART0 (relayée par RP1 sur son USB). La NOR est programmée par RP1 sur cet UART (common/flasher_target.c, flasher_target_poll(uart_default) à chaque trame) : réveil \x01PDFL, cœur 1, IRQ et DMA coupés — la vidéo s'arrête —, 4 Mbauds, secteurs de 4 Kio comparés avant effacement, redémarrage à la fin (ArchitectureRP1.md §10, sys.install). scene.bin sur la carte (papydeck/install/rp3.bin) et l'item MISE A JOUR du dashboard remplacent program … 0x10100000 par la sonde. Au tout début de main(), un démarrage qui n'est pas un redémarrage par le watchdog en déclenche un (watchdog_reboot()), une fois : la parade au rail 3V3 pré-chargé par le moniteur à travers le DDC du TPD12S521 (ArchitectureRP1.md §10, Supervision). Avant la boucle, une fois, [boot] rp3 reset|watchdog : RP1 y reconnaît un redémarrage (la carte du fond est vide) et le programme repeint (sys.bg_lost(), ArchitectureRP1.md §10, Supervision). Un watchdog de 2 s, nourri à chaque trame composée, redémarre la puce sur blocage ou HardFault (en pause sous débogueur) ; RP1 la resette aussi après 500 ms sans VSYNC. Une seule ligne, dans cet ordre (main.c). Bloc A, offset 528 : u32 scene_off (2026-08-28) — le fond que RP1 veut, un offset de NOR parmi les PDSC que le bundle du jeu a installés (0x100000 + k·0x21000) ; quand il change, la scène est chargée par tranches (rp3_background_scene_begin/step) : deux tampons de tileset, le nouveau rempli par le CPU à raison de 8 Kio par vblank depuis l'alias XIP caché, puis au neuvième vblank la bascule du pointeur et le rechargement de la fenêtre de carte (CPU, ~1 ms). Jamais de DMA qui lit la flash pendant la vidéo : il monopolise le port de lecture du DMA (~16 ms pour 64 Kio), le DMA HSTX ne peut plus lire ses lignes, la sortie s'interrompt et le moniteur perd la synchro — l'écran noir d'une seconde à chaque gfx.background avant le 2026-08-28 (+208 lignes late par bascule ; 0 depuis). Coût : 64 Kio de SRAM de plus (RP3 à 383 Ko), ~9 trames par changement de fond, image continue. scene= et sw= dans le rapport. Rapport, une seule ligne, dans cet ordre :

frame N  late=  throttled=  skipped=  rx_underrun=  collide=  bg_refill= cam=x,y [ (pas de scene)]
  | cyc/line: compose=  bg=  wait=  pass=  (budget 8000)
  | bus: delay=D[ (calibre| (balayage)] err=n/m
  | pal gen=  applied=  partial=  full=  bad=  last=  cyc
  | map seq=  runs=  ent=  bad= [ cam=rp1] [ local]
champ sens
frame tics de vsync depuis le démarrage
late, throttled, skipped, collide compteurs cumulés ci-dessus (collide : un canal DMA du HSTX lisait le slot qu'on allait écrire)
rx_underrun lignes dont le DMA de réception était encore occupé à l'armement suivant (§4)
bg_refill rechargements de la fenêtre de carte qui ont dépassé le budget d'un vblank (§5)
cam caméra de la trame ; (pas de scene) si la NOR ne porte pas de magic PDSC
cyc/line sommes de la dernière trame divisées par 480 : compose = rp3_compose_line, bg = rp3_background_line, wait = attente de l'overlay, pass = compose_frame() entière, attente du vsync exclue — attente de balayage incluse, donc « une ligne par ligne » dès que le cœur 0 est en avance
bus délai de phase courant, état du balayage si compilé, mots en erreur / mots vérifiés si RP3_BUS_VERIFY (§4)
pal dernière génération appliquée, nombre d'applications, dont partielles / complètes, blocs à magic faux, cycles de la dernière application (§4)
map dernier seq du bloc C appliqué, runs et entrées écrits dans la fenêtre (cumulés), blocs C à magic faux ou runs débordant du bloc ; cam=rp1 si RP1 pilote la caméra (bit 0 du bloc A), local si la carte est locale (bit 1) — §4, §5

Valeurs de référence 2026-08-26, scène de test et jeu de test RP1 : late=0 skipped=0 collide=0 rx_underrun=0 bg_refill=0, compose=4543, bg 1 476-2 115, err=0. Le 2026-08-27, fond piloté par RP1 : late=0, bad figé aux trames d'avant le démarrage de RP1, map bad=0, cam=rp1 local, 12 runs / 144 entrées par 120 trames.

La transparence est une rangée de la table comme une autre : la boucle ne contient aucun branchement et n'a donc pas de chemin rapide. La détection d'octet nul (x - 0x01010101) & ~x & 0x80808080 est employée côté RP2, dans le blitter.

Le cœur 1 n'est pas une réserve

Sa boucle de thread ne fait qu'imprimer le rapport de mesure, et dort en WFE le reste du temps — il serait tentant d'en conclure qu'il est libre. Il ne l'est pas. Tout le travail de sortie vit dans son handler d'interruption :

dma_irq_handler 948 octets de code, sous-handlers inlinés
Fréquence 1 005 fois par trame — deux fois par ligne active (liste de commandes, puis données), une fois par ligne de blanking
Période une IRQ toutes les 16,6 µs, soit ~4 180 cycles
Priorité 0, la plus haute

Ce n'est pas un handler qui rend un pointeur : il construit et poste les listes de commandes HSTX. Et il ne tolère pas le retard — un seul mot de commande mal dimensionné désynchronise l'expandeur définitivement, ce dont video_output_force_resync() porte la trace.

La période d'interruption est plus courte que la composition d'une ligne (~4 180 cycles contre 4 160 à 5 440) : une ligne composée sur le cœur 1 serait interrompue au moins une fois, par construction.

Lui confier une ligne sur deux si late_lines s'avérait non nul reste envisageable, pas acquis. Ça ne doublerait pas le budget : le cœur 1 doit d'abord servir ses 1 005 interruptions. Le taux d'occupation réel de ce handler n'est pas chiffré, et c'est lui qu'il faudrait mesurer avant de compter sur quoi que ce soit.

Budget SRAM

C'est la contrainte la plus serrée de RP3. Relevé sur le binaire (arm-none-eabi-nm / size, RP3/build/rp3_mixer.elf, 2026-08-26) :

Objet Octets
rp3_blend — table de mélange 131 072
tileset — 1 024 tuiles 8 × 8 résidentes (§5) 65 536
di_ring_buffer — data islands de pico_hdmi 36 864
map_window — fenêtre de carte 128 × 64 entrées (§5) 16 384
composed — 8 lignes RGB565 10 240
overlay — 8 lignes 8bpp reçues 5 120
core1_stack 2 048
ctrl + ctrl_raw — blocs de contrôle reçus, bruts et démiroités (§4) 1 280 + 1 280
line_buffer — interne à pico_hdmi 1 280
safe_line — ligne affichée avant la première composition 1 280
stage + out_buf — tampons du rastériseur de fond (§5) 648 + 648
rp3_palette 512
section .bss 279 692
.text / .scratch_x / .data 25 056 / 1 744 / 1 884
Total sur 520 Ko (size : text 33 344, bss 282 012) ≈ 308 Ko

Pas d'USB sur RP3 (pico_enable_stdio_usb 0) : la console passe par UART0 et RP1.

Le fond n'est pas un framebuffer. Il réside sous forme de tileset (64 Ko) et de fenêtre de carte (16 Ko), 80 Ko au lieu des 307 Ko qu'un plein cadre 8bpp coûterait ; la carte du monde reste en NOR. C'est ce qui laisse la table de mélange tenir. Voir §5.

Les 36 Ko de di_ring_buffer sont alloués par pico_hdmi même en mode DVI, où les data islands ne servent pas : DI_RING_BUFFER_SIZE y est un #define en dur, non configurable. La dépendance étant vendorée verbatim, ce coût est constaté et non corrigé ; il est récupérable si la SRAM venait à manquer.

Répartition des cœurs

pico_hdmi occupe le cœur 1 — réellement, pas nominalement : son IRQ DMA y construit et poste les listes de commandes HSTX 1 005 fois par trame, à priorité 0. C'est au passage qu'elle appelle notre callback, qui se contente de rendre un pointeur vers une ligne déjà composée. Voir §3.

Le cœur 0 reste en avance sur le balayage — demander la ligne d'overlay à RP2, rastériser la ligne de fond (§5), composer, publier.

La composition n'a donc jamais lieu en contexte d'interruption.


4. Réception de l'overlay

16 bits SDR à 42 MHz, PIO et DMA, zéro CPU. clk_sys / 6 : la période du bus est un nombre entier de cycles clk_sys, condition pour que la phase d'échantillonnage reste fixe.

Réception système-synchrone : la machine à états échantillonne sur une phase fixe. La période du bus fait 6 cycles = 23,8 ns ; la fenêtre sans erreur mesurée en occupe 4 (15,9 ns), voir ci-dessous.

RP3 assert HREQ une fois par ligne — depuis la machine PIO elle-même, en side-set, pour que l'origine du temps soit l'instruction qui lève la ligne et non un gpio_put du CPU. La calibration de phase est un décalage constant entre ce front et le premier échantillon, poussé dans la FIFO avec chaque ligne, donc réglable à chaud.

Mesuré le 2026-08-26 : balayage automatique de 0 à 39 cycles contre un motif connu des deux côtés (RP3_RX_PHASE_SWEEP, RP3_BUS_VERIFY) — sans erreur de 10 à 13, erreurs partielles à 8-9 et à partir de 14. Réglé à 12, zéro erreur sur 11 M de mots ensuite. La fenêtre de 4 cycles absorbe la dérive de phase inter-puces d'un démarrage à l'autre (post-diviseurs des PLL, < 1 cycle).

Outils de bring-up

Deux options de config.h, à zéro dans les builds normaux :

  • RP3_BUS_VERIFY — compare une ligne sur huit, mot par mot, au motif que l'émetteur de bring-up de RP2 envoie : cellules de 16 × 16 en damier, pixel = (x + ligne) & 255 dans une cellule sur deux, 0 dans l'autre. La comparaison se fait sur le mot tel que reçu (miroir compris), ~6 400 cycles pour la ligne vérifiée, ~800 par ligne en moyenne, absorbés par les six lignes d'avance. Compté err=n/m dans le rapport.
  • RP3_RX_PHASE_SWEEP — part du délai 0, essaie chaque valeur pendant 120 trames jusqu'à RP3_RX_PHASE_SWEEP_MAX = 40 (80 s d'image brouillée), puis se cale au milieu de la plus longue plage sans erreur. Exige RP3_BUS_VERIFY.

Bus câblé en ordre inverse

L'inversion est dans le cuivre, pas dans le PIO. RP2 émet OVERLAY_0..15 sur ses GP26-41, en ordre croissant ; RP3 les reçoit sur GP45-30, en ordre décroissant. Son GP30 porte donc OVERLAY_15 et son GP45 porte OVERLAY_0. Le bloc GP26-45 de RP3 est le miroir intégral du bloc de RP2, signaux de synchronisation compris — les deux puces se font face et ce câblage évite de croiser seize pistes.

Vérifié sur la netlist de la carte, pas déduit : kicad-cli sch export netlist sur papyDeck_HW/papyDeck.

in pins, 16 n'inverse rien : il associe fidèlement la broche base+n au bit n, et la direction de décalage de l'ISR ne décide que de la place du champ dans le mot, jamais de l'ordre des bits à l'intérieur. Comme RP3 lit ses broches en ordre croissant depuis GP30 alors que les signaux y descendent :

bit capturé n  =  OVERLAY_(15 − n)

Ce qui fait deux choses à la fois :

RP2 émet RP3 capture
bits 0-7 — pixel 2k bits 8-15, ordre des bits inversé
bits 8-15 — pixel 2k+1 bits 0-7, ordre des bits inversé

Les deux pixels s'échangent, et chaque index est inversé dans son octet. in_shiftdir étant à droite, le premier transfert atterrit dans la moitié basse et un mot de DMA contient quatre pixels dans cet ordre d'octets, depuis le poids faible : 2k+1, 2k, 2k+3, 2k+2. En décalage à gauche les deux moitiés s'échangeraient : 2k+2, 2k+3, 2k, 2k+1.

Les deux permutations sont absorbées sans un cycle supplémentaire :

  • l'échange des pixels l'est par le choix du décalage qui alimente chaque sortie dans la boucle de composition — du placement d'opérandes, pas une instruction ;
  • l'inversion des bits l'est par la table de mélange, dont les rangées sont rangées à rev8(index), permutation faite une seule fois à la construction ;
  • rev8(0) = 0, donc l'index transparent reste 0 et la clé de couleur du blitter est intacte.

L'inversion des bits est la carte et ne se discute pas. L'échange des pixels, lui, découle d'une convention, énoncée une seule fois et en noms de signaux :

Sur le fil, le pixel 2k voyage sur OVERLAY_0..7, le pixel 2k+1 sur OVERLAY_8..15.

Cet énoncé fait foi. L'octet dans lequel RP3 le reçoit, et le décalage qui alimente chaque sortie dans sa boucle, en sont des conséquences — pas des choix libres. La convention est celle qui est gratuite des deux côtés : elle est ce que l'autopull de RP2 produit sans rien faire, et RP3 l'absorbe en placement d'opérandes.

Les deux puces ont un levier sur le même effet. Corriger l'échange des deux côtés l'annule et réintroduit le défaut. Voir ArchitectureRP2.md §2, qui porte l'obligation d'émission — c'est ce que lira celui qui écrira RP2.

Format reçu : un index de palette 8 bits par pixel, sans octet d'attribut. Ce qu'il portait — priorité devant/derrière, banque de palette — vit désormais dans l'espace d'index (§3).

Après les 480 lignes de pixels, le bus porte des blocs de contrôle : palette, modes de mélange, backdrop, et depuis le 2026-08-27 la caméra et les écritures de carte du fond. RP1 en est le propriétaire logique, RP2 les relaie, RP3 les applique.

Implémenté le 2026-08-26, étendu le 2026-08-27. Une trame compte 484 lignes sur le bus : les 480 de pixels, puis quatre blocs de contrôle d'une ligne (640 octets) chacun — A palette et caméra, B modes, C écritures de carte, D réservé (RP3_CTRL_LINES) — demandés par HREQ exactement comme des lignes (320 transferts) et reçus dans leurs propres tampons (ctrl_raw). RP2 les relaie tels quels depuis le paquet de RP1 qui accompagne la display list (RP2/include/rp2/dlist.h, offsets 4 112 à 6 671 du paquet PDDL, voir ArchitectureRP2.md §5) : sprites, palette, caméra et carte d'un même paquet atterrissent sur la même trame, atomiques par construction.

Le nombre de lignes de contrôle doit être pair. Les deux cœurs de RP2 se partagent les numéros de ligne absolus par parité, et l'adoption du paquet suivant se fait sur le cœur qui compose la ligne 480. Avec 483 lignes (essayé le 2026-08-27), la ligne 480 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. D'où le bloc D : réservé, à zéro, que RP3 ne lit pas ; il n'existe que pour que le compte soit pair.

Palette et modes (A, B). RP3 défait le miroir du bus, vérifie les magics PDPA / PDPB et une génération ; si elle a changé, il compare la palette, les modes et le backdrop reçus aux siens et recalcule seulement la rangée et la colonne de chaque index modifié (rp3_blend_update_index, 512 cellules) — au-delà de RP3_CTRL_PARTIAL_MAX = 16 index, reconstruction complète (1,2-5 ms, le changement de scène derrière un fondu).

Coût mesuré depuis Lua, 2026-08-26 : 56 258 cycles pour cinq index et le backdrop (≈ 9 400 par index, le backdrop comptant comme l'index 0) ; douze index par trame, 160 000 cycles (≈ 13 300 par index, 0,63 ms du vblank), late = 0, pass +120 cycles/ligne. Soit ≈ 10 000-13 000 cycles par index selon la mesure.

Moment. Les quatre blocs sont demandés dès que la ligne 479 est composée — donc pendant les dernières lignes actives du balayage, le cœur 0 ayant jusqu'à 6 lignes d'avance — et non « pendant le vblank » ; ctrl_apply() s'exécute après la ligne 483, avant la ligne 0 de la trame suivante : la carte (C) d'abord, puis la caméra et les drapeaux (A), puis palette et modes (A, B) si la génération a changé. Ce qui est vrai et observable : la trame N est composée entière avec l'ancienne table, l'ancienne carte et l'ancienne caméra, la trame N+1 entière avec les nouvelles. Jamais de trame partiellement mise à jour.

Format des blocs de contrôle

Petit-boutiste, 640 octets chacun, écrits par RP1 (RP1/src/gfx.c), constantes dans RP3/include/rp3/config.h. Sur le bus ils subissent le même miroir que les pixels ; RP3 le défait avant de lire (ctrl_unmirror) : pour chaque mot DMA reçu, d'octets b0..b3 depuis le poids faible, out[4i] = rev8(b1), out[4i+1] = rev8(b0), out[4i+2] = rev8(b3), out[4i+3] = rev8(b2).

Bloc A — ligne 480 du bus :

offset type contenu
0 u32 magic 0x41504450 (octets P D P A)
4 u32 gen, génération
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 (§5) ; autres bits à zéro, 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 (l'index 0 n'est pas une couleur, §3)
528-639 zéro

flags, cam_x et cam_y sont lus à chaque paquet, indépendamment de gen — le reste du bloc A n'est appliqué que quand la génération change. Côté RP1, le bloc A est réécrit quand gen, la caméra ou les drapeaux changent (rp1_gfx_ctrl), sans que la caméra fasse monter gen.

Bloc B — ligne 481 du bus :

offset type contenu
0 u32 magic 0x42504450 (octets P D P B)
4 u32 gen, la même que dans A
8 u32[2] pad
16 256 × u8 mode par index, valeurs du tableau de §3 ; mode[0] ignoré
272-639 zéro

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

offset type contenu
0 u32 magic 0x43504450 (octets P D P C)
4 u32 seq, incrémenté par RP1 à chaque trame, bloc vide ou non
8 u16 n_runs, nombre de runs qui suivent
10 u16 bytes, octets de runs à partir de l'offset 16 — informatif, RP3 ne le lit pas
12 u32 pad
16 runs n_runs runs à la suite : {u8 x, u8 y, u8 n, u8 flags = 0} puis n × u16 entrées de carte, chaque run arrondi à 4 octets ; 624 octets utiles au plus (RP3_CTRL_MAP_RUNS_OFF = 16)

Un run écrit n entrées (1..255) de gauche à droite à partir de la tuile (x, y), coordonnées en tuiles modulo la fenêtre de 128 × 64 (RP3_MAP_W × RP3_MAP_H, torique dans les deux sens). L'entrée est celle de la carte de scène (§5) : id de tuile bits 0-9, flip H bit 10, flip V bit 11, petit-boutiste. Le flags du run est à zéro et n'est pas lu.

Bloc D — ligne 483 du bus : réservé, à zéro, ignoré par RP3 (voir la règle du nombre pair, ci-dessus).

Règles d'application

ctrl_apply(), RP3/src/main.c, une fois par trame après réception de la ligne 483 :

  • bloc C d'abord, indépendamment de A et B (ctrl_apply_map) : magic PDPC faux → map_bad++, rien ; seq égal au dernier appliqué → rien — RP2 re-relaie le même paquet quand aucun nouveau n'est arrivé, un run n'est donc appliqué qu'une fois ; sinon map_seq = seq et les n_runs runs sont écrits dans la fenêtre (rp3_background_map_write, map_runs++, map_entries += n) ; un run dont l'en-tête ou les entrées dépassent l'octet 639 → map_bad++ et arrêt du bloc, les runs déjà lus restant appliqués ;
  • magic A ou B faux → pal_bad++, rien d'autre n'est appliqué (ni caméra ni palette) ;
  • caméra et drapeaux du bloc A, à chaque paquet : bit 0 → rp1_camera, cam_x / cam_y pris pour la trame suivante (sans le bit, le rebond de bring-up continue, §5) ; bit 1 → rp3_background_set_local (§5) ;
  • gen == 0 → ignoré, RP3 garde sa palette — c'est ce que RP2 envoie tant qu'aucun paquet de RP1 n'est arrivé, et ce que RP1 envoie avec un bloc nul (ctrl == NULL) ;
  • gen égale à la dernière génération appliquée → ignoré ; gen de A ≠ gen de B → ignoré, on attend la trame suivante ;
  • toute autre valeur — différente, pas nécessairement croissante — est appliquée ;
  • diff contre l'état courant : l'index i ∈ 1..255 est « modifié » si sa couleur ou son mode diffère ; le backdrop modifié compte comme l'index 0 ; n ≤ 16 → rangée + colonne de chacun (rp3_blend_update_index, pal_partial++) ; n > 16 → rp3_blend_build avec les modes reçus (pal_full++) ;
  • côté RP1, la génération part à 1, rp1_gfx_palette_init() la monte (gen = 2 au démarrage), puis +1 à chaque modification effective (une valeur identique ne la monte pas) ; les blocs A et B sont reconstruits seulement si gen a changé — le bloc A aussi quand la caméra ou les drapeaux changent, sans monter gen — mais envoyés à chaque paquet ; le bloc C est reconstruit à chaque trame, seq +1, avec les runs en attente qui tiennent dans 624 octets, dans l'ordre (ArchitectureRP1.md §10).

Palette de RP3 au démarrage

  1. rp3_palette_init : index 0 noir, backdrop noir, rampe RGB332 pour 1..255 — r = ((i>>5)&7)·255/7, g = ((i>>2)&7)·255/7, b = (i&3)·255/3 ;
  2. si la scène en NOR a un palette_off ∉ {0, 0xFFFFFFFF} (§5) : les 256 entrées, index 0 compris, sont chargées depuis la NOR ;
  3. modes par défaut (§3), table construite (rp3_blend_build) ;
  4. ensuite seuls les blocs de contrôle (gen ≠ 0) changent palette, modes et backdrop. RP1 embarque la même palette.bin (generated/scene_palette.h) et la possède dès son démarrage.

Protocole de ligne — cadence et garde

  • 484 HREQ par trame : 480 lignes puis 4 blocs de contrôle, tous de 320 transferts.
  • Par ligne (overlay_rx.c, overlay_rx.pio) : le cœur 0 vide les FIFO, pousse 319 puis le délai (12) ; la machine à états lève HREQ, attend délai + 1 cycles, échantillonne 320 × 16 bits toutes les 6 cycles (42 MHz) ; HREQ retombe quand elle revient au pull — haut ≈ 13 + 1 920 cycles ≈ 7,7 µs. RP2 attend HREQ bas puis haut avant d'émettre, à cadence fixe, sans contrôle de flux.
  • Séquence du cœur 0 par trame : tic de vsync → busy_wait_us(20) (le temps que RP2 voie VSYNC et se resynchronise) → caméra (§5) → HREQ de la ligne 0 ; puis pour chaque ligne L : attendre L, armer L+1, se retenir tant que L − faisceauRP3_LINE_BUFFERS − 1 (avance maximale 6 lignes, voir §3), composer L, publier ; après la 479 : armer et attendre 480 à 483, une à une, ctrl_apply(). En début de trame le cœur 0 demande donc 6 lignes en rafale (~27 µs).
  • Pas de handshake « pas prêt » : RP2 envoie toujours le slot demandé (un slot pas encore composé chez lui compte late de son côté et sort avec le contenu de L − 12) ; côté RP3, un DMA encore occupé à l'armement suivant compte rx_underrun++ et le transfert est abandonné.
  • RP2 se resynchronise sur le front montant de VSYNC : si (next_arm − 1) % 484 ≠ 0, desync++ et redémarrage de sa machine à états à la frontière suivante. Compteurs RP2 mesurés 2026-08-26 (482 lignes) : late = 0, desync = 0, lead_min = 8 ; 2026-08-27 (484 lignes, fond piloté par RP1) : late = 1, desync = 1 (démarrage), txstall = 2, 0 rejet.
  • Première trame après le démarrage : safe_line (ligne 0 du fond, sans overlay) est rendue sur toutes les lignes jusqu'à la première composition.

VSYNC et HSYNC exportés

  • VSYNC GP26 (→ RP2 GP45, RP1 GP39) : actif haut ; monte dans le callback vsync de pico_hdmi — première ligne de l'impulsion V, après le front porch de 10 lignes — et retombe au callback de la ligne active 0 : impulsion ≈ 35 lignes ≈ 1,11 ms. Tout le monde se cale sur le front montant : RP2 pour se resynchroniser, RP1 pour compter les trames (59,99 Hz mesuré).
  • HSYNC GP27 : impulsion GPIO (haut puis bas immédiat) à chaque callback de ligne active seulement — 480 par trame, aucune pendant le vblank, gigue d'IRQ. Inutilisé par RP2, qui est cadencé par HREQ ; le passage en PIO reste ouvert.
  • Les trois puces tournent à 252 MHz depuis le même XOSC 12 MHz, sans configuration (§1).

5. Fond — tuiles résidentes, carte streamée

Implémenté et mesuré le 2026-08-26 (RP3/src/background.c, dérivé de tiles-scroll-proposal/ avec ses deux passes fusionnées). Le fond est un monde de tuiles 8 × 8 qui défile ; ce qui réside où découle d'une seule contrainte :

Une scanline touche 81 tuiles à des adresses quelconques. Lues dans la NOR, ce sont 81 accès aléatoires de 8 octets, chacun payant commande + adresse + dummy (~100 cycles clk_sys au mieux ; le cache XIP de 16 Ko ne tient pas un tileset de 64 Ko) — tout le budget de la ligne. Le flux XIP ne sait faire que du séquentiel.

Quoi Taille
Tileset — 1 024 tuiles 8 × 8 en 8 bpp SRAM, chargé par DMA au changement de scène (~2 ms) 64 Ko
Fenêtre de carte — 128 × 64 entrées de 16 bits, bouclante SRAM, entretenue au vblank 16 Ko
Carte du monde NOR, rangée par blocs de 16 × 16 entrées selon le monde
Tampons de ligne du rastériseur SRAM 1,3 Ko

L'écran visible fait 80 × 60 tuiles ; 81 × 61 avec la tuile coupée par le scroll fin. La fenêtre est plus large que nécessaire (128 colonnes pour 81) pour découpler le rechargement du scroll : une colonne peut être chargée des dizaines de trames avant d'être visible. Le rangement par blocs fait d'une colonne nouvellement exposée 4 rafales séquentielles au lieu de 60 accès dispersés. RP3_REFILL_BUDGET borne le travail d'un vblank pour qu'un téléport ne coûte pas une trame.

Conventions — ce que l'outil d'assets doit respecter

  • Tuiles 8 × 8, 8 bpp, un index de palette par pixel, rangées ligne par ligne (RP3_TILE_W/H).
  • Entrée de carte sur 16 bits : bits 0-9 id de tuile (0-1023), bit 10 flip H, bit 11 flip V, bits 12-15 réservés à zéro. Le flip vertical est gratuit (on indexe la rangée depuis l'autre bout), l'horizontal coûte deux rev par tuile. La réserve est porteuse : le chemin sans flip indexe le tileset avec l'entrée entière, sans masquer l'id (background.c, FETCH_TILE) ; seul le chemin avec flip applique RP3_TILE_ID_MASK. Un bit 12-15 à 1 lit hors du tileset.
  • L'index 0 signifie « rien de dessiné » (§3) : aucune tuile visible ne l'emploie. La tuile 0 est le trou par convention.
  • Carte rangée par blocs de RP3_MAP_BLOCK = 16 × 16 entrées, blocs en ordre ligne.
  • Scène en NOR à RP3_SCENE_FLASH_OFFSET (1 Mo, adresse 0x10100000) : en-tête de 32 octets lu au démarrage, format ci-dessous. Référence : RP3/tools/mkscene.py.

Blob de scène PDSC — format exact

Petit-boutiste, écrit par RP3/tools/mkscene.py, lu par RP3/src/background.c. Les offsets de l'en-tête sont absolus depuis le début de la flash (le blob n'est pas relogeable) :

offset type contenu
0 u32 magic 0x43534450 (octets P D S C)
4 u16 world_w, largeur du monde en tuiles
6 u16 world_h, hauteur du monde en tuiles
8 u32 tileset_off, offset flash du tileset
12 u32 map_off, offset flash de la carte
16 u32 palette_off, offset flash de la palette ; 0 ou 0xFFFFFFFF = pas de palette
20-31 zéro

Ordre écrit par mkscene.py derrière l'en-tête : palette (512 o à 0x100020) → tileset (65 536 o à 0x100220) → carte (à 0x110220).

  • Palette : 256 × u16 RGB565, la palette.bin de scripts/pdpal.py (§3), chargée entière au démarrage (§4).
  • Tileset : 1 024 tuiles × 64 o, tuile t à t·64, 8 rangées de 8 octets, octet 0 = pixel gauche, un index de palette par octet ; tuile 0 = 64 zéros = le trou.
  • Carte : entrées u16 (format ci-dessus), rangées par blocs de 16 × 16 en ordre ligne : bw = ceil(world_w / 16), entrée (x, y) à map_off + ((((y/16)·bw + x/16)·256) + (y%16)·16 + x%16)·2 ; taille ceil(world_w/16) · ceil(world_h/16) · 512 octets. Hors monde ⇒ tuile 0.
  • Monde de test : 256 × 128 tuiles = 2 048 × 1 024 px.
  • Sans magic valide : scene_ok = false, fond = index 0 partout = backdrop, rapport marqué (pas de scene).

mkscene.py exige --palette (défaut RP3/build/palette.bin), produite avant par RP3/tools/mkpalette.py — qui dépend de ../RawAssets/, de RP2/tools/mksprites.py et de Pillow (../papyDeck_Sim/.venv). Ordre : mkpalette.pymkscene.py (et mksprites.py côté RP2).

Rastériseur — une passe, mesurée

Par ligne, 81 lectures de deux mots dans le tileset, rotation du scroll fin au vol pendant que les mots sont en registres, écriture alignée sur 32 bits pour le compositeur — qui caste son pointeur de fond en uint32_t * et ne tolère pas de désalignement. Le scroll fin se décompose en un saut de 0 ou 1 mot et un décalage de 0, 8, 16 ou 24 bits.

cycles/ligne mesurés (DWT)
Version à deux passes de la proposition (tampon intermédiaire + rotation) 3 426
Une passe, rotation au vol — scroll aligné sur un mot 1 476
Une passe, rotation au vol — scroll fin 2 115

Avec le compositeur à 4 543 (§3), la ligne coûte ~6 700 cycles sur 8 000 (84 %), 7 500 avec la vérification de bus du bring-up. late = 0, bg_refill = 0 en scroll diagonal continu. Le chemin rapide du compositeur (§3) reste la réserve suivante si le budget devait se resserrer.

Caméra et scroll

  • Caméra en pixels monde, posée par rp3_background_set_camera() au vblank et latchée à start_frame : toute la trame est rastérisée contre une seule position. La ligne L de l'écran lit la rangée monde cam_y + L, la colonne cam_x en tête ; pas de bouclage du monde en mode NOR (en mode local, la fenêtre est le monde et boucle — voir ci-dessous).
  • Fenêtre de 128 × 64 tuiles recentrée quand la coordonnée-tuile de la caméra change ; un saut ≥ la fenêtre provoque un rechargement complet ; au-delà de RP3_REFILL_BUDGET = 8 rangées + colonnes dans un vblank, bg_refill++ et le reste attend la trame suivante (tuiles périmées ou vides visibles, pas de trame sautée).
  • Le monde est lu par le cache XIP (le binaire tourne en SRAM, le cache est libre) ; le tileset est chargé par DMA XIP_NOCACHE_NOALLOC au changement de scène. Le stream FIFO XIP n'est plus utilisé nulle part.
  • La caméra est à RP1 depuis le 2026-08-27. gfx.camera(x, y) en pixels monde, transportée dans le bloc A (bit 0 de flags, cam_x, cam_y — §4) avec la display list de la trame ; RP3 la prend telle quelle dans camera_step() pour la trame suivante, elle arrive donc à l'écran avec les sprites du même paquet. Au pixel entier : le rastériseur est au pixel, un scroll à sous-pixel n'aurait pas de sens. RP3 ne la borne pas : en mode NOR c'est au jeu de rester dans [0, monde − écran] (au-delà, les tuiles hors monde sont vides, tuile 0). Tant que le bit 0 n'est pas levé — RP1 n'a jamais appelé gfx.camera, ou c'est le superviseur qui parle — le rebond de bring-up de main.c continue : départ (0,0), vitesse (3,1) px/trame, rebond dans [0, monde − écran].

Carte locale — le mode texte

Implémenté et mesuré le 2026-08-27 (rp3_background_set_local, rp3_background_map_write, RP3/src/background.c). Le bit 1 du bloc A (gfx.bg_local(true) côté RP1) fait de la fenêtre de 128 × 64 tuiles le monde lui-même : torique, 1 024 × 512 px, jamais rechargée depuis la NOR ; la caméra n'est plus bornée par rien, le rastériseur enroule dans les deux sens (map_window[y & 63][x & 127]). Le tileset reste celui de la scène NOR — RP3 n'a pas d'API pour en changer à chaud.

  • Écritures : les runs du bloc C (§4) vont directement dans map_window, coordonnées modulo 128 × 64, appliqués après la ligne 483 et visibles à la trame suivante — avec la caméra et les sprites du même paquet. Une entrée coûte 2 octets sur le fil et une écriture de 16 bits chez RP3 ; le bloc C porte 624 octets utiles par trame : ~300 entrées selon le découpage (4 octets d'en-tête et arrondi à 4 par run, 255 entrées par run au plus), une rangée de 128 tuiles en un run de 260 octets, une ligne de 80 caractères en 164.
  • Passage au mode local : ce que la fenêtre contenait reste visible tant que RP1 ne l'a pas écrasé — la scène NOR autour de la dernière caméra ; RP1 remplit ce qu'il veut, rangée par rangée s'il le faut. Retour au mode NOR : rechargement complet de la fenêtre autour de la caméra (win_tx = win_ty = INT32_MINwindow_reload_all).
  • Hors mode local, les mêmes écritures sont acceptées mais ce sont des sur-impressions : la prochaine recharge de la rangée ou de la colonne — un déplacement de caméra qui la fait entrer dans la fenêtre — les efface. Utilisable tant que la caméra ne bouge pas ; non persistant. Ouvert.
  • Ce que ça permet : un mode texte sans aucune entrée de display list ni cycle du blitter. L'écran de 80 × 60 tuiles fait 4 800 entrées, remplies en 16 à 20 trames selon le découpage des runs (~0,3 s) ; une ligne de 80 caractères tient dans une trame avec de la marge ; un compteur de HUD coûte un run de quelques octets. C'est la voie du dashboard et du texte de HUD des jeux (Notes-uSD.md §12), avec la fonte système ci-dessous.
  • Tuiles système 704-1023 et fontes (2026-08-27, soir) : les 320 dernières tuiles de tout tileset sont réservées au système ; RP3/tools/mkscene.py y place deux fontes, pour les caractères 32..95 (espace = tuile vide) : 8×8 en 960 + (code − 32) (64 tuiles, 960-1023), la fonte arcade RP3/assets/font8x8arcade.png (générée avec arcade.hexed.pw — 49 glyphes, majuscules, chiffres et - & " ' [ ! ] $ = : ; ,, plus le bloc plein ; caractères absents = bloc plein, qui occupe la case de # — un curseur) ; 16×16 en 704 + 4·(code − 32) (4 tuiles par glyphe dans l'ordre haut-gauche, haut-droit, bas-gauche, bas-droit ; 256 tuiles, 704-959), RP3/assets/fight16.png de devurandom (OpenGameArt, « New original grafx2 font collection » ; 64 cases au pas de 17 px, une ligne de repères en haut, codes 33..96 — tout l'ASCII ; caractères absents = ?). Le 8×8 est illisible sur un téléviseur (80 colonnes) ; le 16×16 (40 × 30 caractères) est la taille des menus. Encre = index 191, fond = 0 (le trou : le backdrop — un glyphe remplace sa tuile). RP1 (gfx.text(x, y, s, taille)) replie les minuscules sur les majuscules et écrit deux runs par ligne de texte en 16×16. Les glyphes Arial de bring-up (tuiles 1..95) ont disparu : les tuiles du décor de test sont maintenant 1..8. Les jeux gardent 704 tuiles et ont du texte gratuit, avec les mêmes glyphes que le dashboard.
  • Mesuré avec le jeu de test de RP1 (damier de 128 × 64 rempli une rangée par trame, titre, compteur toutes les 10 trames, caméra à (frame / 2) % 1024 — un défilement horizontal lent piloté par RP1, les torches par-dessus) : late = 0, bad figé aux trames d'avant le démarrage de RP1, map bad = 0 ensuite, 12 runs / 144 entrées par 120 trames en régime établi, comptés identiques côté RP1 et RP3, 0 run en attente, 0 perdu.

Reste ouvert : l'écran de démarrage et une seconde encre (ombre) ; des écritures persistantes en mode NOR ; un changement de tileset à chaud ; RP3 ne vérifie pas que les entrées écrites respectent la réserve des bits 12-15 (« la réserve est porteuse », ci-dessus).


6. Audio — conception, non implémenté

État 2026-08-26 : le firmware force le mode DVI en permanence (video_output_set_dvi_mode(true) dans main.c), ce qui désactive tous les data islands et donc l'audio ; aucun code RP3 ne reçoit d'échantillons, le lien SPI RP1→RP3 n'existant pas (§1). Le moniteur voit un flux DVI. Ce qui suit est le plan.

RP3 insérerait les échantillons fournis par RP1 dans des data islands du flux HDMI. pico_hdmi implémente la file d'attente correspondante : hstx_di_queue_push, hstx_di_queue_set_sample_rate, hstx_di_queue_set_samples_per_line_exact, ACR et Audio InfoFrame.

Coût en broches : zéro. Les échantillons arriveraient par le lien SPI depuis RP1 — 48 kHz stéréo 16 bits = 192 Ko/s, soit 15 % d'un SPI à 10 MHz.

Le mode DVI resterait disponible en mise au point, ou si un moniteur refuse de se synchroniser en présence de data islands.

RP3 ne porte que la voie HDMI. La sortie analogique — casque et haut-parleur — est un DAC TLV320DAC3100 raccordé à RP1, qui est la source des échantillons. Voir ArchitectureRP1.md §9. Les deux voies diffèrent de 0,1 % en cadence, le pixel clock valant 25,2 MHz au lieu de 25,175 : une seule est active à la fois, RP1 choisissant d'après l'EDID.