Phase 4 vraie — pipeline OK, verrou fbbootlogd identifié

Crates ajoutés :
- redox-wl-display (lib) : RedoxOutput { open, take_crtc, pixels_mut,
  present, present_with_takeover, Drop }
- redox-wl-fullscreen-paint (bin) : take CRTC + 30 frames ARGB animées

Validation logique du pipeline display sur Redox bootée :
- ConsumerHandle::new_vt() OK
- open_display_v2() retourne le fd graphics-ipc
- V2GraphicsHandle énumère 1 connecteur 1280x800
- CpuBackedBuffer ARGB8888 plein écran allocable
- add_framebuffer + set_crtc + dirty_framebuffer répondent tous OK
- 30 itérations sync_rect sans erreur ni leak

Validation visuelle automatisée via QEMU monitor screendump :
- QEMU en -display none + -monitor unix:socket
- ncat -U envoie sendkey ret au bootloader puis screendump à T+15s
- ImageMagick convertit PPM → PNG, visualisable

Verrou identifié : fbbootlogd (lancé par init.initfs.d/20_fbbootlogd.service,
embarqué dans le blob initfs) écrit directement dans le framebuffer mémoire
mappé par vesad, hors du pipeline DRM. Il ne release pas le display quand
notre paint fait set_crtc.

Pour vrai visuel, il faut soit :
1. Consommer les events VT côté RedoxOutput (le pattern Orbital, propre)
2. Désactiver fbbootlogd dans l'image (rapide, debug)
3. Implémenter le handoff complet (long, prod)

Le pipeline étant validé, on peut passer phase 5 (input backend) et revenir
sur le visuel quand on aura un compositor qui consomme les events VT.

docs/phase4-display-backend.md enrichi avec l'analyse complète.

Leyoda 2026 – GPLv3
This commit is contained in:
Votre Nom 2026-05-09 10:11:06 +02:00
parent 100f85dd01
commit 5b1e038333
5 changed files with 420 additions and 0 deletions

View file

@ -0,0 +1,122 @@
//! Phase 4 vraie : prendre le CRTC + dessiner plein écran.
//!
//! Ce binaire est destiné à *remplacer* Orbital sur le VT 3 dans l'init
//! Redox (modifier `usr/lib/init.d/20_orbital`). Il :
//!
//! 1. Lit `VT` env var (mise par init)
//! 2. Ouvre le display via inputd → graphics-ipc
//! 3. Appelle `inputd -A <vt>` pour devenir handler officiel du VT
//! 4. Prend le CRTC (set_crtc avec un framebuffer plein écran)
//! 5. Peint un dégradé ARGB8888 + bandes diagonales animées sur 30 frames
//! 6. Présente une frame par seconde pendant 30 secondes
//! 7. Exit propre — Drop libère le framebuffer
//!
//! Pendant ces 30 s, l'écran QEMU montre **les pixels écrits par notre code**,
//! pas le login Orbital. Premier vrai signal visuel d'un compositor naissant.
use std::env;
use std::fs::OpenOptions;
use std::io::Write;
use std::process::{Command, ExitCode};
use std::sync::{Mutex, OnceLock};
use std::thread;
use std::time::Duration;
use redox_wl_display::RedoxOutput;
/// Mirroir stdout sur /scheme/debug pour que la sortie aille au serial host.
struct DebugSink(Mutex<Option<std::fs::File>>);
impl DebugSink {
fn new() -> Self {
Self(Mutex::new(
OpenOptions::new().write(true).open("/scheme/debug").ok(),
))
}
fn writeln(&self, s: &str) {
println!("{s}");
if let Ok(mut g) = self.0.lock() {
if let Some(f) = g.as_mut() {
let _ = writeln!(f, "{s}");
}
}
}
}
fn dlog(s: &str) {
static SINK: OnceLock<DebugSink> = OnceLock::new();
SINK.get_or_init(DebugSink::new).writeln(s);
}
/// Pixel ARGB8888 calculé depuis (x, y, t) : bande diagonale qui se déplace
/// avec t pour qu'on voie l'écran s'animer (preuve que la boucle render tourne).
fn pixel(x: u32, y: u32, w: u32, h: u32, t: u32) -> u32 {
let r = ((x * 255) / w.max(1)) & 0xFF;
let g = ((y * 255) / h.max(1)) & 0xFF;
let b = ((x.wrapping_add(y).wrapping_add(t.wrapping_mul(8))) & 0xFF) as u32;
(0xFF << 24) | (r << 16) | (g << 8) | b
}
fn run() -> Result<(), Box<dyn std::error::Error>> {
dlog("[paint] Phase 4 vraie — prendre CRTC + dessiner plein écran");
let vt = env::var("VT").ok();
dlog(&format!("[paint] VT = {:?}", vt));
let mut output = RedoxOutput::open().map_err(|e| format!("RedoxOutput::open: {e}"))?;
dlog(&format!(
"[paint] display ouvert : {}x{}",
output.width(),
output.height()
));
if let Some(v) = &vt {
match Command::new("inputd").arg("-A").arg(v).status() {
Ok(s) if s.success() => dlog(&format!("[paint] inputd -A {v} OK")),
Ok(s) => dlog(&format!("[paint] inputd -A {v} exit {:?}", s)),
Err(e) => dlog(&format!("[paint] inputd -A {v} err: {e}")),
}
}
output.take_crtc().map_err(|e| format!("take_crtc: {e}"))?;
dlog("[paint] CRTC pris : on tient l'écran");
let w = output.width();
let h = output.height();
// 30 frames espacées d'une seconde pour qu'on voie le dégradé bouger
for t in 0..30u32 {
{
let pixels = output.pixels_mut()?;
for y in 0..h {
let row = (y * w) as usize;
for x in 0..w {
pixels[row + x as usize] = pixel(x, y, w, h, t);
}
}
}
output
.present_with_takeover()
.map_err(|e| format!("present {t}: {e}"))?;
if t % 5 == 0 {
dlog(&format!("[paint] frame {t}/30 présentée"));
}
thread::sleep(Duration::from_millis(1000));
}
dlog("[paint] 30 frames terminées, exit propre");
drop(output); // libère framebuffer + buffer explicitement
thread::sleep(Duration::from_millis(500)); // laisser le serial flush
Ok(())
}
fn main() -> ExitCode {
match run() {
Ok(()) => {
dlog("[paint] PASS");
ExitCode::SUCCESS
}
Err(e) => {
dlog(&format!("[paint] FAIL: {e}"));
ExitCode::FAILURE
}
}
}