redox-wayland-compositor/docs/phase4-display-backend.md
Votre Nom 753a30757b 🎉 Phase 4 vraie validée visuellement : pixels custom plein écran
Capture : docs/phase4-victory-1280x800.png — dégradé ARGB animé 1280x800
écrit par redox-wl-fullscreen-paint, occupant tout l'écran QEMU sans
trace de bootlog, fbcond ou Orbital.

Cause racine du verrou (3 bugs en cascade) :

1. ConsumerHandle local à RedoxOutput::open() → droppé en fin de fn →
   inputd::on_close retirait le VT de self.vts → tous les `inputd -A <vt>`
   ultérieurs retournaient warning "switch to non-existent VT"

2. L'env var VT=N posée par init n'a aucun lien avec le VT alloué par
   inputd. inputd auto-incrémente next_vt_id à partir de 2 (VT 1 réservé
   bootlog). Avec fbbootlogd VT 1 + fbcond VT 2, notre paint = VT 3.

3. Sans le bon VT activé, set_crtc est silencieusement no-op côté
   driver-graphics (lib.rs:575 : `if *vt == self.active_vt { ... }`).

Fixes :
- RedoxOutput stocke `_consumer: ConsumerHandle` pour préserver le VT
- RedoxOutput.vt() lu via fpath sur consumer fd (inputd retourne
  `<scheme>/<vt>`)
- Binary lit output.vt() puis fait inputd -A <vt> avec le bon numéro
- 300ms de sleep pour propagation active_vt avant take_crtc

Validation automatisée : qemu -display none + monitor unix socket +
ncat -U pour sendkey ret + screendump à T+14s + ImageMagick.

Image Redox restaurée à boot Orbital normal après la session.

Phase 4 close. La piste 1 (consume events VT) reste utile pour le
hot-switch propre Ctrl+Alt+Fn mais n'est plus bloquante.

Leyoda 2026 – GPLv3
2026-05-09 10:46:20 +02:00

13 KiB

Phase 4 — Display backend Redox : résultats

Document produit le 2026-05-08 dans le cadre du plan directeur REDOX_COSMIC_XWAYLAND_RS_PLAN.md.

Périmètre : valider qu'un binaire Rust userspace peut ouvrir le display Redox, énumérer les modes via le subset DRM/KMS, et allouer un buffer hardware-backed pour le rendu.

Verdict global

Pipeline display backend Redox accessible et fonctionnel.

Le crate redox-wl-test-display-backend a tourné dans une session Redox réelle (image bootée via make qemu audio=no), depuis la console VT 2, avec succès complet de la séquence :

ConsumerHandle::new_vt()
  └─> open_display_v2()
       └─> V2GraphicsHandle::from_file()
            └─> resource_handles().connectors() → 1 connecteur
                 └─> mode 1280x800
            └─> get_driver_capability(DumbBuffer) → Ok(1)
            └─> CpuBackedBuffer::new(64x64, Argb8888) → OK
                 └─> shadow_buf().fill(pattern)
                 └─> sync_rect()
                 └─> destroy()

Aucun blocage. Le display backend Redox fonctionne directement avec les APIs Orbital sans modification.

Sortie complète du test

Capturée le 2026-05-08 dans la console VT 2 de Redox bootée via make qemu :

[disp] Phase 4 display backend test on Redox
[disp] VT env = None
[disp] inputd consumer handle opened
[disp] display file opened from inputd path
[disp] V2GraphicsHandle created
[disp] 1 connector(s) reported by KMS subset
[disp]   #0 connector connector::Handle(19): state=Connected, 1 mode(s)
[disp]      first mode: 1280x800
[disp] 1 connected display(s)
[disp] driver caps: dumb_buffer=Ok(1) cursor=?x?
[disp] CpuBackedBuffer allocated 64x64 ARGB8888 (shadow=false)
[disp] painted test pattern + sync_rect
[disp] CpuBackedBuffer destroyed
[disp] PASS: display backend pipeline reachable

Notes :

  • VT env = None : le binaire a été lancé manuellement, pas via init. Pour un compositor en production, il sera lancé par init avec VT=N et appellera inputd -A N (déjà branché dans le code, désactivé sans VT).
  • cursor=?x? : get_driver_capability(CursorWidth/Height) retourne Err pour ce display QEMU. Pas critique, on a DumbBuffer=Ok(1) qui suffit.
  • shadow=false : DumbPreferShadow capability vaut 0 sur ce backend, donc les writes vont direct au framebuffer (pas de shadow buffer en RAM).

Fait notable : coexistence avec Orbital

Au moment du test, Orbital tournait sur VT 3 (init 20_orbital). Notre binaire a été lancé depuis VT 2 (console getty depuis 30_console).

inputd a accepté deux handlers consumer simultanés sur deux VTs distincts, et le scheme display.* a été accessible depuis le second handler sans conflit avec celui d'Orbital.

C'est une bonne nouvelle pour la stratégie de coexistence du plan directeur : on peut développer un compositor Wayland sur un VT séparé tout en gardant Orbital opérationnel sur le sien — sans modifier l'init Redox.

Implications pour la suite

Ce qui est désormais validé

  1. Le subset DRM/KMS Redox (graphics-ipc::V2GraphicsHandle) est complet pour les besoins d'un compositor minimal :

    • énumération connecteurs/encoders/CRTCs
    • création/destruction de DumbBuffers
    • mapping CPU writable + sync rect (= dirty_framebuffer)
    • support driver capabilities (DumbBuffer, DumbPreferShadow)
  2. La crate Rust drm 0.15 upstream fonctionne directement sur Redox sans patch (déjà confirmé en compile time, maintenant en runtime).

  3. La fondation pour un RedoxOutput (cf phase 4 du plan directeur) est en place : il suffit de wrapper ce qu'on a fait dans le test en une struct propre + ajouter modeset (set_crtc) et page-flipping.

Ce qui reste pour un display backend complet

Le test ne fait pas :

  • modeset/scanout : on alloue un buffer mais on ne fait pas set_crtc(crtc, fb, ...) pour qu'il soit affiché. Orbital tient déjà le CRTC sur VT 3. Pour vraiment "afficher", il faudra prendre la place d'Orbital sur un VT (ou lui prendre VT 3).
  • page flipping : pas de double buffering, pas de wait_for_vblank, pas de page_flip.
  • hotplug : pas d'écoute des events DRM (connector connect/disconnect).
  • resize : V2DisplayMap::resize_if_necessary (cf Orbital core/display.rs) à porter.
  • cursor plane : alloc + set_cursor non testés (driver caps cursor=Err sur QEMU desktop, mais c'est dispo selon Orbital).

Toutes ces étapes sont des extensions évidentes du test actuel, documentées dans orbital/src/core/display.rs qu'on peut copier presque verbatim.

Phase 4 vraie : tentative et limite identifiée (2026-05-08 soir)

Crates créés :

  • redox-wl-display (lib) : RedoxOutput avec open() + take_crtc()
    • pixels_mut() + present() + present_with_takeover() + Drop
  • redox-wl-fullscreen-paint (bin) : prend le CRTC sur VT 2, peint un dégradé ARGB animé sur 30 frames

Pipeline logique : toutes les API DRM répondent OK (open, énumère connecteurs, alloc CpuBackedBuffer, add_framebuffer, set_crtc, sync_rect, dirty_framebuffer). Aucune erreur retour.

Validation visuelle automatisée : QEMU lancé sans display via -display none, monitor unix socket pour screendump. Captures PPM 1280x800 prises pendant et après l'exécution du paint. Conversion en PNG via ImageMagick pour visualisation.

Verrou rencontré

L'écran capturé montre les logs kernel + init en mode texte, pas notre dégradé. Notre paint a bien tourné (lignes [paint] visibles dans la capture, écrites par fbcond sur le framebuffer texte), mais le rendu graphique de notre present_with_takeover() n'apparaît pas.

Cause : fbbootlogd (lancé par init.initfs.d/20_fbbootlogd.service, embarqué dans le blob initfs) écrit directement dans la mémoire framebuffer mappée par vesad, hors du pipeline DRM. Il est ouvert sur le scheme consumer_bootlog qui force VT 1 actif (cf inputd main.rs:189).

Les essais infructueux :

  1. inputd -A <vt> après take_crtc : warning "switch to non-existent VT" parce que l'env var VT=N mise par init n'est pas le VT alloué par inputd (inputd auto-incrémente depuis 2)
  2. VT=2 (vrai VT alloué) + inputd -A 2 : retour OK mais l'écran reste sur la sortie fbbootlogd
  3. Désactivation de 30_console : aucun changement (fbbootlogd est dans l'initfs, pas dans /usr/lib/init.d/)
  4. set_crtc à chaque frame (present_with_takeover) : pas d'effet visuel (fbbootlogd ne lit pas le CRTC, il écrit directement en mémoire)

Cas où Orbital arrive à afficher

Dans le boot standard, Orbital remplace bien fbbootlogd à l'écran. Mais Orbital reçoit un signal VtEvent::Activate via inputd et fait un handoff explicite avec fbbootlogd (cf inputd::ConsumerHandleEvent::Handoff qui arrête fbbootlogd quand un autre VT prend la main).

Notre RedoxOutput ne consomme pas les events VT, donc le handoff ne se déclenche pas et fbbootlogd reste actif.

Étapes restantes pour le visuel

Trois pistes, par coût croissant :

  1. Consommer les events VT côté RedoxOutput — ajouter une boucle consumer.read_events() qui détecte Handoff et release/re-take le CRTC en conséquence. C'est ce que fait Orbital. ~1 jour de travail.
  2. Désactiver fbbootlogd dans l'image — modifier ~/Projets/Redox/base/init.initfs.d/20_fbbootlogd.service (commentaire cmd =), puis make all dans redox-src/. Pratique pour test mais pas pour production. ~15 min build + 5 min config.
  3. Implémenter le protocole inputd handler complet — release_display, handoff réciproque, gestion full du switch VT. ~3-5 jours de travail.

La piste 1 est celle qu'utilise Orbital, donc la cible légitime. La piste 2 permet de valider visuellement plus tôt.

Validation indirecte

Même sans visuel, le pipeline est validé par :

  • les codes de retour OK de toutes les API DRM (set_crtc, dirty_framebuffer)
  • les lignes [paint] frame X/30 présentée qui sortent à chaque iteration
  • la persistance du buffer CpuBackedBuffer (alloc + écriture + sync sans panic, plusieurs centaines de fois sans fuite mémoire visible)

VICTOIRE 2026-05-09 — phase 4 vraie validée visuellement

Capture :

L'écran QEMU montre intégralement le dégradé ARGB animé écrit par notre binaire redox-wl-fullscreen-paint, plein écran 1280x800. Aucune trace de bootlog, fbcond ou fbbootlogd. C'est notre code Rust qui pilote le framebuffer Redox.

La cause racine du verrou (3 bugs en cascade)

Bug 1 — ConsumerHandle était local à RedoxOutput::open() et droppé à la sortie de la fonction. inputd-daemon réagissait avec on_close qui retire le VT de self.vts. Tous les inputd -A <vt> ultérieurs retournaient warning "switch to non-existent VT".

Bug 2 — L'env var VT=N posée par init n'a aucun lien avec le VT réellement alloué par inputd. inputd auto-incrémente next_vt_id à partir de 2 (VT 1 réservé bootlog). On dénombrait :

  • VT 1 = consumer_bootlog de fbbootlogd
  • VT 2 = consumer de fbcond (lancé par init.initfs.d/20_fbcond.service)
  • VT 3 = notre consumer

Bug 3 — Sans le bon VT activé, set_crtc est silencieusement no-op côté driver-graphics (cf lib.rs:575 : if *vt == self.active_vt { self.adapter.set_crtc(...) }).

Le fix

pub struct RedoxOutput {
    /// CONSUMER GARDÉ EN VIE : si on le drop, inputd retire notre VT et
    /// `inputd -A <vt>` dira "non-existent".
    _consumer: ConsumerHandle,
    // ...
    /// VT alloué par inputd, lu via fpath() sur le consumer fd.
    /// (cf `inputd::main.rs:271-281` qui retourne `display.scheme/<vt>`)
    vt: usize,
}

Et le binaire qui appelle :

let our_vt = output.vt();          // VT exact alloué par inputd
Command::new("inputd").arg("-A").arg(our_vt.to_string()).status()?;
thread::sleep(Duration::from_millis(300));   // propagation active_vt
output.take_crtc()?;                          // set_crtc passe la condition
output.present_with_takeover()?;              // pixels visibles à l'écran

Validation visuelle automatisée

Méthode reproductible dans le repo :

qemu-system-x86_64 \
    ... -vga std -display none \
    -monitor unix:/tmp/qmp.sock,server,nowait \
    -serial file:/tmp/qemu-serial.log ... &
sleep 2; printf "sendkey ret\n" | ncat -U /tmp/qmp.sock     # passe bootloader
sleep 14; printf "screendump /tmp/frame.ppm\n" | ncat -U /tmp/qmp.sock
magick /tmp/frame.ppm /tmp/frame.png                         # convert
# Lire le PNG pour voir le rendu

Implications pour la suite

Phase 4 entièrement validée. La fondation RedoxOutput est utilisable telle quelle pour :

  • Phase 5 (input backend) : ajouter consumer.read_events() pour récupérer les events keyboard/mouse de inputd
  • Phase 6 (surfaces shm composées) : composer plusieurs wl_shm buffers dans le pixels_mut() de RedoxOutput
  • Phase 7 (compositor utilisable) : ajouter focus, stacking, damage tracking au-dessus

La piste 1 du plan (consommer events VT) reste utile à terme pour gérer le hot-switch propre entre VTs (Ctrl+Alt+Fn), mais n'est plus bloquante pour afficher.

Prochaine étape : phase 4 vraie

Le test actuel prouve la possibilité technique. La phase 4 vraie consiste à :

  1. Créer une crate redox-wl-display propre (pas un test, une lib)
    • struct RedoxOutput qui wrappe V2GraphicsHandle + Displays
    • méthodes enumerate(), take_crtc(), present(buffer), release_crtc()
  2. Faire un binaire test qui prend effectivement le CRTC, écrit un buffer plein écran, puis le rend (l'écran devient visible du pattern écrit).
  3. Tester en remplaçant temporairement 20_orbital dans l'init par notre binaire — Orbital ne tourne pas, notre binaire tient le display, et l'écran montre notre rendu.

C'est le premier moment où on aura de vrais pixels Redox sortis de notre code sur l'écran.

Code source

crates/redox-wl-test-display-backend/
├── Cargo.toml         # graphics-ipc, inputd, drm via git deps base.git
└── src/main.rs        # 200 lignes, pattern Orbital simplifié

Build :

cd crates/redox-wl-test-display-backend
redoxer build --release

Test :

  1. Monter ~/Projets/Redox/redox-src/build/x86_64/desktop/harddrive.img via redoxfs (cf README section "Voie B")
  2. Copier le binaire dans /usr/bin/ de l'image
  3. Démonter
  4. cd ~/Projets/Redox/redox-src && make qemu audio=no QEMU_USER_FLAGS="-k fr"
  5. Ctrl+Alt+F2 → console VT 2
  6. login root / mot de passe password
  7. redox-wl-test-display-backend

Fin du document de phase 4.