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,203 @@
//! Display backend wrapper for Redox.
//!
//! Réutilise le pattern d'Orbital (cf orbital/src/core/display.rs) en exposant
//! une API minimaliste pour un compositor :
//!
//! let mut output = RedoxOutput::open()?;
//! output.take_crtc()?; // alloue framebuffer + set_crtc
//! {
//! let pixels = output.pixels_mut()?;
//! // peindre dans pixels
//! }
//! output.present()?; // sync_rect + dirty_framebuffer
//! // Drop libère framebuffer + buffer
//!
//! Pour l'instant : un seul connecteur, un seul mode (le premier renvoyé par
//! le KMS Redox), pas de page flip / double buffer / hotplug.
use std::io;
use std::slice;
use drm::Device as _;
use drm::buffer::{Buffer as _, DrmFourcc};
use drm::control::{
ClipRect, Device as _, Mode, connector,
crtc as drm_crtc, framebuffer,
};
use graphics_ipc::{CpuBackedBuffer, V2GraphicsHandle};
use inputd::ConsumerHandle;
pub struct RedoxOutput {
handle: V2GraphicsHandle,
width: u32,
height: u32,
connector: connector::Handle,
crtc: drm_crtc::Handle,
mode: Mode,
fb: Option<framebuffer::Handle>,
buffer: Option<CpuBackedBuffer>,
}
impl RedoxOutput {
/// Ouvre le display via inputd → graphics-ipc, choisit le premier
/// connecteur connecté + son premier mode + le premier CRTC compatible.
pub fn open() -> io::Result<Self> {
let consumer = ConsumerHandle::new_vt()?;
let display_file = consumer.open_display_v2()?;
let handle = V2GraphicsHandle::from_file(display_file)?;
let resources = handle.resource_handles().map_err(io::Error::other)?;
// First connected connector
let mut chosen: Option<(connector::Handle, _)> = None;
for &c in resources.connectors() {
let info = handle.get_connector(c, true)?;
if info.state() == connector::State::Connected {
chosen = Some((c, info));
break;
}
}
let (connector, info) =
chosen.ok_or_else(|| io::Error::other("no connected display"))?;
let mode = info
.modes()
.first()
.copied()
.ok_or_else(|| io::Error::other("connector has no mode"))?;
let (w, h) = mode.size();
let encoder_handle = info
.encoders()
.first()
.copied()
.ok_or_else(|| io::Error::other("connector has no encoder"))?;
let encoder = handle.get_encoder(encoder_handle)?;
let crtc = resources
.filter_crtcs(encoder.possible_crtcs())
.first()
.copied()
.ok_or_else(|| io::Error::other("no crtc compatible with encoder"))?;
Ok(Self {
handle,
width: w as u32,
height: h as u32,
connector,
crtc,
mode,
fb: None,
buffer: None,
})
}
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn handle(&self) -> &V2GraphicsHandle {
&self.handle
}
/// Alloue un CpuBackedBuffer ARGB8888 plein écran, l'ajoute comme
/// framebuffer DRM et appelle `set_crtc` pour qu'il soit affiché.
/// Le binaire qui appelle ça doit être l'unique handler du VT cible
/// (sinon Orbital ou un autre serveur va se battre pour le CRTC).
pub fn take_crtc(&mut self) -> io::Result<()> {
if self.fb.is_some() || self.buffer.is_some() {
return Err(io::Error::other("CRTC already taken"));
}
let buffer = CpuBackedBuffer::new(
&self.handle,
(self.width, self.height),
DrmFourcc::Argb8888,
32,
)?;
let fb = self.handle.add_framebuffer(buffer.buffer(), 32, 32)?;
self.handle.set_crtc(
self.crtc,
Some(fb),
(0, 0),
&[self.connector],
Some(self.mode),
)?;
self.fb = Some(fb);
self.buffer = Some(buffer);
Ok(())
}
/// Renvoie une slice mutable des pixels au format `u32` (ARGB8888,
/// little-endian → mémoire BGRA octet par octet, mais en u32 c'est
/// directement 0xAARRGGBB).
pub fn pixels_mut(&mut self) -> io::Result<&mut [u32]> {
let buffer = self
.buffer
.as_mut()
.ok_or_else(|| io::Error::other("CRTC not taken yet"))?;
let bytes = buffer.shadow_buf();
let (w, h) = (self.width as usize, self.height as usize);
// SAFETY: alignment 4 garantie par DumbBuffer ARGB8888 + len % 4 == 0
let pixels = unsafe {
slice::from_raw_parts_mut(bytes.as_mut_ptr() as *mut u32, w * h)
};
Ok(pixels)
}
/// Pousse le contenu du buffer sur l'écran. Appel l'équivalent de
/// `wl_surface.commit` côté Wayland — ici on fait un sync_rect
/// (shadow → on-screen) + dirty_framebuffer (notification au driver).
pub fn present(&mut self) -> io::Result<()> {
let fb = self.fb.ok_or_else(|| io::Error::other("no fb"))?;
let buffer = self
.buffer
.as_mut()
.ok_or_else(|| io::Error::other("no buffer"))?;
buffer.sync_rect(0, 0, self.width, self.height);
self.handle.dirty_framebuffer(
fb,
&[ClipRect::new(0, 0, self.width as u16, self.height as u16)],
)?;
Ok(())
}
/// Variante de `present` qui ré-applique `set_crtc` AVANT le flush.
/// Utile en démarrage si un autre serveur (fbbootlogd) tient encore
/// le CRTC malgré notre `take_crtc` initial. À appeler à chaque frame
/// au début, à enlever quand on est sûr de tenir le display.
pub fn present_with_takeover(&mut self) -> io::Result<()> {
let fb = self.fb.ok_or_else(|| io::Error::other("no fb"))?;
// Re-forcer set_crtc : repren le CRTC si quelqu'un d'autre l'avait pris
self.handle.set_crtc(
self.crtc,
Some(fb),
(0, 0),
&[self.connector],
Some(self.mode),
)?;
let buffer = self
.buffer
.as_mut()
.ok_or_else(|| io::Error::other("no buffer"))?;
buffer.sync_rect(0, 0, self.width, self.height);
self.handle.dirty_framebuffer(
fb,
&[ClipRect::new(0, 0, self.width as u16, self.height as u16)],
)?;
Ok(())
}
}
impl Drop for RedoxOutput {
fn drop(&mut self) {
if let Some(fb) = self.fb.take() {
let _ = self.handle.destroy_framebuffer(fb);
}
if let Some(buffer) = self.buffer.take() {
let _ = buffer.destroy(&self.handle);
}
}
}