redox-wayland-compositor/crates/redox-wl-display/src/lib.rs
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

237 lines
8.2 KiB
Rust

//! 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::os::fd::AsRawFd;
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 {
/// Consumer handle GARDÉ EN VIE pour que le VT alloué par inputd persiste.
/// Si on drop ce handle, inputd retire le VT de sa table et toute opération
/// `inputd -A <vt>` ultérieure dira "non-existent VT".
_consumer: ConsumerHandle,
handle: V2GraphicsHandle,
width: u32,
height: u32,
connector: connector::Handle,
crtc: drm_crtc::Handle,
mode: Mode,
fb: Option<framebuffer::Handle>,
buffer: Option<CpuBackedBuffer>,
/// VT alloué par inputd lors de l'open consumer.
/// À utiliser pour `inputd -A <vt>` afin de devenir le VT actif.
vt: usize,
}
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()?;
// Découvrir notre VT alloué par inputd via fpath sur le consumer fd.
// Le fpath retourne `<display_scheme>/<vt>` (cf inputd main.rs:271).
let mut buf = [0u8; 1024];
let n = libredox_call_fpath(consumer.event_handle().as_raw_fd() as usize, &mut buf)?;
let path = std::str::from_utf8(&buf[..n])
.map_err(|e| io::Error::other(format!("fpath utf8: {e}")))?;
let vt: usize = path
.rsplit('/')
.next()
.and_then(|s| s.parse().ok())
.ok_or_else(|| io::Error::other(format!("can't parse VT from fpath {path:?}")))?;
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 {
_consumer: consumer,
handle,
width: w as u32,
height: h as u32,
connector,
crtc,
mode,
fb: None,
buffer: None,
vt,
})
}
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn handle(&self) -> &V2GraphicsHandle {
&self.handle
}
/// Le VT alloué par inputd lors de l'open consumer.
/// Utiliser cette valeur pour `inputd -A <vt>` ou `ControlHandle::activate_vt`.
pub fn vt(&self) -> usize {
self.vt
}
/// 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(())
}
}
/// Wrapper minimal sur `libredox::call::fpath` (la fn FFI directe n'a pas de
/// signature stable côté `inputd::ConsumerHandle`).
fn libredox_call_fpath(fd: usize, buf: &mut [u8]) -> io::Result<usize> {
libredox::call::fpath(fd, buf).map_err(|e| io::Error::from_raw_os_error(e.errno()))
}
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);
}
}
}