diff --git a/crates/redox-wl-display/Cargo.toml b/crates/redox-wl-display/Cargo.toml new file mode 100644 index 0000000..37a4423 --- /dev/null +++ b/crates/redox-wl-display/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "redox-wl-display" +version = "0.1.0" +edition = "2021" +description = "Display backend wrapper around graphics-ipc + drm subset for Redox" + +[dependencies] +drm = "0.15" +graphics-ipc = { git = "https://gitlab.redox-os.org/redox-os/base.git" } +inputd = { git = "https://gitlab.redox-os.org/redox-os/base.git" } diff --git a/crates/redox-wl-display/src/lib.rs b/crates/redox-wl-display/src/lib.rs new file mode 100644 index 0000000..9e3c4cb --- /dev/null +++ b/crates/redox-wl-display/src/lib.rs @@ -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, + buffer: Option, +} + +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 { + 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); + } + } +} diff --git a/crates/redox-wl-fullscreen-paint/Cargo.toml b/crates/redox-wl-fullscreen-paint/Cargo.toml new file mode 100644 index 0000000..fac2879 --- /dev/null +++ b/crates/redox-wl-fullscreen-paint/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "redox-wl-fullscreen-paint" +version = "0.1.0" +edition = "2021" + +[dependencies] +redox-wl-display = { path = "../redox-wl-display" } diff --git a/crates/redox-wl-fullscreen-paint/src/main.rs b/crates/redox-wl-fullscreen-paint/src/main.rs new file mode 100644 index 0000000..fdf2edb --- /dev/null +++ b/crates/redox-wl-fullscreen-paint/src/main.rs @@ -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 ` 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>); +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 = 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> { + 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 + } + } +} diff --git a/docs/phase4-display-backend.md b/docs/phase4-display-backend.md index 27af846..99828b5 100644 --- a/docs/phase4-display-backend.md +++ b/docs/phase4-display-backend.md @@ -112,6 +112,84 @@ 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 ` 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) + +C'est suffisant pour avancer phase 5 (input backend) et y revenir plus tard +quand on aura un compositor qui consomme les events VT. + ## Prochaine étape : phase 4 vraie Le test actuel prouve **la possibilité technique**. La phase 4 vraie