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
237 lines
8.2 KiB
Rust
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);
|
|
}
|
|
}
|
|
}
|