diff --git a/crates/redox-wl-compositor/Cargo.toml b/crates/redox-wl-compositor/Cargo.toml new file mode 100644 index 0000000..880857b --- /dev/null +++ b/crates/redox-wl-compositor/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "redox-wl-compositor" +version = "0.1.0" +edition = "2021" +description = "Compositor binaire intégrant display + input + frontend Wayland" + +[dependencies] +redox-wl-display = { path = "../redox-wl-display" } +redox-wl-input = { path = "../redox-wl-input" } +redox-wl-compositor-core = { path = "../redox-wl-compositor-core" } +redox-wl-wayland-frontend = { path = "../redox-wl-wayland-frontend" } diff --git a/crates/redox-wl-compositor/src/main.rs b/crates/redox-wl-compositor/src/main.rs new file mode 100644 index 0000000..ac4c6e5 --- /dev/null +++ b/crates/redox-wl-compositor/src/main.rs @@ -0,0 +1,192 @@ +//! Phase 6.4 — Compositor binaire complet. +//! +//! Boucle main d'un mini compositor Wayland : +//! 1. Ouvre RedoxOutput (display) et take CRTC +//! 2. Ouvre InputBackend partageant le ConsumerHandle +//! 3. Bind un ListeningSocket Wayland sur `/tmp/redox-wl-comp.sock` +//! 4. Loop : +//! - accept_pending_clients() +//! - dispatch_clients() (lit les requests, appelle nos Dispatch impls) +//! - poll() input → log + raise on click éventuel +//! - clear bg + compose_into(output) + present +//! - notify_frame_done() pour les wl_callback en attente +//! - flush_clients() +//! - sleep ~16ms +//! +//! Tourne 60 secondes max, exit propre. + +use std::env; +use std::fs::OpenOptions; +use std::io::Write; +use std::path::PathBuf; +use std::process::{Command, ExitCode}; +use std::sync::{Mutex, OnceLock}; +use std::thread; +use std::time::{Duration, Instant}; + +use redox_wl_compositor_core::Framebuffer; +use redox_wl_display::RedoxOutput; +use redox_wl_input::{InputBackend, InputEvent}; +use redox_wl_wayland_frontend::WaylandFrontend; + +const SOCKET_PATH: &str = "/tmp/redox-wl-comp.sock"; +const BG_COLOR: u32 = 0xFF101820; + +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); +} + +fn run() -> Result<(), Box> { + dlog("[comp] Phase 6.4 — compositor Wayland démarrage"); + + // Display + let mut output = RedoxOutput::open()?; + let our_vt = output.vt(); + let fb_w = output.width(); + let fb_h = output.height(); + dlog(&format!("[comp] display {fb_w}x{fb_h}, VT={our_vt}")); + + let _ = Command::new("inputd") + .arg("-A") + .arg(our_vt.to_string()) + .status(); + thread::sleep(Duration::from_millis(300)); + output.take_crtc()?; + dlog("[comp] CRTC pris"); + + // Clear initial → fond bleu nuit pour signaler "compositor up" + { + let pixels = ::pixels_mut(&mut output); + for p in pixels.iter_mut() { + *p = BG_COLOR; + } + } + output.present_with_takeover()?; + + // Input + let input = InputBackend::new(output.consumer()); + + // Wayland frontend + let socket_path = PathBuf::from(SOCKET_PATH); + let mut frontend = WaylandFrontend::bind_absolute(&socket_path)?; + dlog(&format!("[comp] Wayland socket : {SOCKET_PATH}")); + + // Exporter WAYLAND_DISPLAY pour les clients lancés par l'OS qui regarderaient + // l'env. (Notre client de test va connecter explicitement au path.) + unsafe { + env::set_var("WAYLAND_DISPLAY", SOCKET_PATH); + } + + // Boucle principale + let start = Instant::now(); + let total = Duration::from_secs(60); + let frame_period = Duration::from_millis(33); // ~30 fps + let mut last_frame = Instant::now(); + let mut tick: u32 = 0; + + while start.elapsed() < total { + tick = tick.wrapping_add(1); + + // 1. Accepter nouveaux clients Wayland + if let Err(e) = frontend.accept_pending_clients() { + dlog(&format!("[comp] accept err: {e}")); + } + + // 2. Dispatch des requêtes Wayland en attente + if let Err(e) = frontend.dispatch_clients() { + dlog(&format!("[comp] dispatch err: {e}")); + } + + // 3. Input + if let Ok(events) = input.poll() { + for ev in events { + match ev { + InputEvent::Key { + scancode, pressed, .. + } if pressed && scancode == 0x01 => { + // Esc → exit + dlog("[comp] Esc → exit"); + let _ = frontend.flush_clients(); + let _ = std::fs::remove_file(SOCKET_PATH); + return Ok(()); + } + InputEvent::Quit => { + dlog("[comp] Quit reçu"); + let _ = frontend.flush_clients(); + let _ = std::fs::remove_file(SOCKET_PATH); + return Ok(()); + } + _ => {} + } + } + } + + // 4. Render + let nb = frontend.registry.len(); + // Recompose tout à chaque frame pour 6.4 (pas de damage tracking) + { + let pixels = ::pixels_mut(&mut output); + for p in pixels.iter_mut() { + *p = BG_COLOR; + } + } + frontend.registry.compose_into(&mut output); + if let Err(e) = output.present_with_takeover() { + dlog(&format!("[comp] present err: {e}")); + } + + // 5. Frame callbacks done après le present + let elapsed_ms = last_frame.elapsed().as_millis() as u32; + last_frame = Instant::now(); + frontend.notify_frame_done(elapsed_ms); + + // 6. Flush vers les clients + if let Err(e) = frontend.flush_clients() { + dlog(&format!("[comp] flush err: {e}")); + } + + // Log occasionnel + if tick % 30 == 0 { + dlog(&format!( + "[comp] tick={tick} surfaces={nb} elapsed={:.1}s", + start.elapsed().as_secs_f32() + )); + } + + thread::sleep(frame_period); + } + + dlog("[comp] timeout 60s atteint, exit"); + let _ = std::fs::remove_file(SOCKET_PATH); + Ok(()) +} + +fn main() -> ExitCode { + match run() { + Ok(()) => { + dlog("[comp] PASS"); + ExitCode::SUCCESS + } + Err(e) => { + dlog(&format!("[comp] FAIL: {e}")); + ExitCode::FAILURE + } + } +} diff --git a/crates/redox-wl-test-client-shm/Cargo.toml b/crates/redox-wl-test-client-shm/Cargo.toml new file mode 100644 index 0000000..3307876 --- /dev/null +++ b/crates/redox-wl-test-client-shm/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "redox-wl-test-client-shm" +version = "0.1.0" +edition = "2021" + +[dependencies] +wayland-client = { path = "../../../wayland-rs/wayland-client", default-features = false } +wayland-backend = { path = "../../../wayland-rs/wayland-backend", default-features = false } +libc = "0.2" diff --git a/crates/redox-wl-test-client-shm/src/main.rs b/crates/redox-wl-test-client-shm/src/main.rs new file mode 100644 index 0000000..214356e --- /dev/null +++ b/crates/redox-wl-test-client-shm/src/main.rs @@ -0,0 +1,229 @@ +//! Phase 6.4 — Client Wayland test. +//! +//! Se connecte au socket Wayland exposé par le compositor à +//! `/tmp/redox-wl-comp.sock`, crée une surface 320x240, peint un +//! pattern ARGB déterministe (dégradé orangé), commit. Reste connecté +//! 30s pour qu'on puisse capturer l'écran. +//! +//! C'est le test critique de phase 6.4 : si le compositor affiche +//! ce qui suit dans la frame QEMU, on a un VRAI compositor Wayland +//! qui rend des pixels venant d'un client externe. + +use std::ffi::CString; +use std::fs::OpenOptions; +use std::io::Write; +use std::os::fd::{AsFd, FromRawFd, OwnedFd}; +use std::os::unix::net::UnixStream; +use std::process::ExitCode; +use std::ptr; +use std::sync::{Mutex, OnceLock}; +use std::thread; +use std::time::Duration; + +use wayland_client::{ + Connection, Dispatch, EventQueue, Proxy, QueueHandle, + backend::Backend, + protocol::{ + wl_buffer::WlBuffer, wl_compositor::WlCompositor, wl_registry, wl_shm::WlShm, + wl_shm_pool::WlShmPool, wl_surface::WlSurface, + }, +}; + +const SOCKET_PATH: &str = "/tmp/redox-wl-comp.sock"; +const W: i32 = 320; +const H: i32 = 240; +const STRIDE: i32 = W * 4; +const SIZE: i32 = STRIDE * H; + +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); +} + +#[derive(Default)] +struct ClientState { + compositor: Option, + shm: Option, +} + +impl Dispatch for ClientState { + fn event( + state: &mut Self, + registry: &wl_registry::WlRegistry, + event: wl_registry::Event, + _data: &(), + _conn: &Connection, + qh: &QueueHandle, + ) { + if let wl_registry::Event::Global { name, interface, version } = event { + match interface.as_str() { + "wl_compositor" => { + state.compositor = Some(registry.bind(name, version.min(5), qh, ())); + } + "wl_shm" => { + state.shm = Some(registry.bind(name, version.min(1), qh, ())); + } + _ => {} + } + } + } +} + +macro_rules! noop { + ($ty:ty) => { + impl Dispatch<$ty, ()> for ClientState { + fn event( + _state: &mut Self, + _r: &$ty, + _ev: <$ty as Proxy>::Event, + _: &(), + _conn: &Connection, + _qh: &QueueHandle, + ) { + } + } + }; +} +noop!(WlCompositor); +noop!(WlShm); +noop!(WlShmPool); +noop!(WlBuffer); +noop!(WlSurface); + +unsafe fn create_shm_with_pattern(name: &str) -> Result { + let cname = CString::new(name).unwrap(); + let _ = libc::shm_unlink(cname.as_ptr()); + let fd = libc::shm_open(cname.as_ptr(), libc::O_RDWR | libc::O_CREAT, 0o600); + if fd < 0 { + return Err(format!( + "shm_open: errno {}", + std::io::Error::last_os_error().raw_os_error().unwrap_or(0) + )); + } + if libc::ftruncate(fd, SIZE as _) != 0 { + libc::close(fd); + return Err("ftruncate failed".into()); + } + let p = libc::mmap( + ptr::null_mut(), + SIZE as usize, + libc::PROT_READ | libc::PROT_WRITE, + libc::MAP_SHARED, + fd, + 0, + ); + if p == libc::MAP_FAILED { + libc::close(fd); + return Err("mmap failed".into()); + } + + // Pattern ARGB : dégradé orangé + bandes diagonales pour reconnaître + let pixels = std::slice::from_raw_parts_mut(p as *mut u32, (W * H) as usize); + for y in 0..H { + for x in 0..W { + let r: u32 = (200 + (x * 55 / W)) as u32 & 0xFF; + let g: u32 = (80 + (y * 100 / H)) as u32 & 0xFF; + let b: u32 = (((x + y) & 0xFF) as u32).saturating_sub(100); + pixels[(y * W + x) as usize] = (0xFF << 24) | (r << 16) | (g << 8) | b; + } + } + libc::munmap(p, SIZE as usize); + Ok(OwnedFd::from_raw_fd(fd)) +} + +fn run() -> Result<(), Box> { + dlog("[client] connect to compositor"); + + // Attendre que le socket existe (compositor démarré) + for i in 0..50 { + if std::path::Path::new(SOCKET_PATH).exists() { + break; + } + if i == 49 { + return Err("compositor socket missing after 5s".into()); + } + thread::sleep(Duration::from_millis(100)); + } + + let stream = UnixStream::connect(SOCKET_PATH)?; + let backend = Backend::connect(stream)?; + let conn = Connection::from_backend(backend); + let mut event_queue: EventQueue = conn.new_event_queue(); + let qh = event_queue.handle(); + let _registry = conn.display().get_registry(&qh, ()); + + let mut state = ClientState::default(); + event_queue.roundtrip(&mut state)?; + dlog(&format!( + "[client] globals : compositor={} shm={}", + state.compositor.is_some(), + state.shm.is_some() + )); + + let compositor = state + .compositor + .clone() + .ok_or("no wl_compositor global")?; + let shm = state.shm.clone().ok_or("no wl_shm global")?; + + // Crée shm + pattern + let fd = unsafe { create_shm_with_pattern("/redox-wl-client-shm") }?; + dlog(&format!("[client] shm créé, peint {}x{} ARGB", W, H)); + + // wl_shm_pool + wl_buffer + let pool = shm.create_pool(fd.as_fd(), SIZE, &qh, ()); + let buffer = pool.create_buffer(0, W, H, STRIDE, wayland_client::protocol::wl_shm::Format::Argb8888, &qh, ()); + + // wl_surface + let surface = compositor.create_surface(&qh, ()); + surface.attach(Some(&buffer), 0, 0); + surface.damage_buffer(0, 0, W, H); + surface.commit(); + dlog("[client] surface attach + damage + commit envoyés"); + + event_queue.flush()?; + let _ = event_queue.roundtrip(&mut state); + + // Reste connecté ~25s pour qu'on puisse capturer + let start = std::time::Instant::now(); + while start.elapsed() < Duration::from_secs(25) { + let _ = event_queue.dispatch_pending(&mut state); + let _ = event_queue.flush(); + thread::sleep(Duration::from_millis(50)); + } + + dlog("[client] done, exit propre"); + let _ = surface; // tag explicite pour rappeler que la surface vit jusqu'à la fin + let _ = buffer; + let _ = pool; + Ok(()) +} + +fn main() -> ExitCode { + match run() { + Ok(()) => { + dlog("[client] PASS"); + ExitCode::SUCCESS + } + Err(e) => { + dlog(&format!("[client] FAIL: {e}")); + ExitCode::FAILURE + } + } +} diff --git a/crates/redox-wl-wayland-frontend/Cargo.toml b/crates/redox-wl-wayland-frontend/Cargo.toml new file mode 100644 index 0000000..93970d7 --- /dev/null +++ b/crates/redox-wl-wayland-frontend/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "redox-wl-wayland-frontend" +version = "0.1.0" +edition = "2021" +description = "Wayland protocol frontend that maps wl_compositor/wl_shm/wl_surface to redox-wl-compositor-core SurfaceRegistry" + +[dependencies] +redox-wl-compositor-core = { path = "../redox-wl-compositor-core" } +wayland-server = { path = "../../../wayland-rs/wayland-server", default-features = false } +wayland-backend = { path = "../../../wayland-rs/wayland-backend", default-features = false } +libc = "0.2" diff --git a/crates/redox-wl-wayland-frontend/src/lib.rs b/crates/redox-wl-wayland-frontend/src/lib.rs new file mode 100644 index 0000000..c9b545c --- /dev/null +++ b/crates/redox-wl-wayland-frontend/src/lib.rs @@ -0,0 +1,475 @@ +//! Phase 6.4 — Frontend Wayland. +//! +//! Implémente les protocoles Wayland minimaux nécessaires pour qu'un +//! client externe puisse créer une surface, allouer un buffer shm, +//! peindre dedans et le commiter, en se connectant au socket exposé +//! par le compositor. +//! +//! Globals exposés : +//! - `wl_compositor` v5 : `create_surface`, `create_region` (no-op) +//! - `wl_shm` v1 : advertise `Argb8888` + `Xrgb8888` +//! +//! Resources gérés : +//! - `wl_surface` : `attach`, `damage`, `damage_buffer`, `commit`, +//! `frame` (callback), `destroy` +//! - `wl_shm_pool` : `create_buffer`, `destroy`, `resize` +//! - `wl_buffer` : `destroy`, envoi de `release` après usage +//! +//! Au commit, le buffer SHM est lu et **copié** dans un `SurfaceBuffer` +//! owned du compositor-core. C'est plus simple que de garder une +//! référence vivante au mmap (qui peut être unmappé par le client à tout +//! moment). À optimiser plus tard avec des buffer attaché-non-libéré. + +use std::collections::HashMap; +use std::os::fd::{AsRawFd, OwnedFd}; +use std::path::Path; +use std::sync::{Arc, Mutex}; + +use redox_wl_compositor_core::{SurfaceBuffer, SurfaceId, SurfaceRegistry}; +use wayland_server::{ + Client, DataInit, Display as WlDisplay, DisplayHandle, GlobalDispatch, Resource, + backend::{ClientData, ClientId, DisconnectReason}, + protocol::{wl_buffer, wl_compositor, wl_region, wl_shm, wl_shm_pool, wl_surface, wl_callback}, +}; + +const COMPOSITOR_VERSION: u32 = 5; +const SHM_VERSION: u32 = 1; + +/// Pool SHM mmap'd côté compositor. Garde le `OwnedFd` pour qu'inputd ou +/// le kernel ne libère pas la zone tant qu'on a des buffers en référence. +struct ShmPool { + fd: OwnedFd, + map: *mut u8, + size: usize, +} + +unsafe impl Send for ShmPool {} +unsafe impl Sync for ShmPool {} + +impl ShmPool { + unsafe fn new(fd: OwnedFd, size: i32) -> std::io::Result { + let size = size as usize; + let p = libc::mmap( + std::ptr::null_mut(), + size, + libc::PROT_READ, + libc::MAP_SHARED, + fd.as_raw_fd(), + 0, + ); + if p == libc::MAP_FAILED { + return Err(std::io::Error::last_os_error()); + } + Ok(Self { + fd, + map: p as *mut u8, + size, + }) + } + + /// Lit les pixels du buffer à partir de l'offset, taille connue. + /// Pas de validation alignment ; le caller doit fournir des params + /// cohérents (offset + h * stride <= self.size, stride == w*4). + unsafe fn read_argb(&self, offset: usize, w: u32, h: u32, stride: i32) -> Vec { + let stride = stride as usize; + let n = (w as usize) * (h as usize); + let mut out = Vec::with_capacity(n); + for y in 0..h as usize { + let row_ptr = self.map.add(offset + y * stride) as *const u32; + for x in 0..w as usize { + out.push(unsafe { *row_ptr.add(x) }); + } + } + out + } +} + +impl Drop for ShmPool { + fn drop(&mut self) { + unsafe { + let _ = libc::munmap(self.map as *mut _, self.size); + } + // self.fd droppé ensuite + } +} + +/// Données par-buffer côté serveur : référence au pool + paramètres pour relire. +#[derive(Clone)] +struct BufferData { + pool: Arc>, + offset: i32, + width: u32, + height: u32, + stride: i32, + /// Format Wayland brut (Argb8888 = 0, Xrgb8888 = 1) + format: wl_shm::Format, +} + +/// Données par-surface : SurfaceId du compositor + buffer attaché en pending. +#[derive(Default)] +struct SurfaceData { + /// SurfaceId associé dans le SurfaceRegistry. + /// Initialisé par `wl_compositor.create_surface` via Mutex