diff --git a/crates/redox-wl-compositor/src/main.rs b/crates/redox-wl-compositor/src/main.rs index 8a7e162..0e8cfee 100644 --- a/crates/redox-wl-compositor/src/main.rs +++ b/crates/redox-wl-compositor/src/main.rs @@ -95,13 +95,7 @@ impl<'a> MakeWriter<'a> for TeeWriter { } fn init_tracing() { - // Phase 13.1.c : par défaut, info + target=cursor en debug. Le target - // `cursor` est utilisé par les traces ABS/REL du frontend (cf - // redox-wl-wayland-frontend). Pratique sous QEMU graphique où taper - // un RUST_LOG complexe est pénible (pas de copier-coller). Override - // toujours possible via RUST_LOG=… au lancement. - let filter = EnvFilter::try_from_default_env() - .unwrap_or_else(|_| EnvFilter::new("info,cursor=debug")); + let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); tracing_subscriber::fmt() .with_env_filter(filter) .with_writer(TeeWriter::new()) @@ -144,10 +138,6 @@ fn run() -> Result<(), Box> { // Wayland frontend let socket_path = PathBuf::from(SOCKET_PATH); let mut frontend = WaylandFrontend::bind_absolute(&socket_path)?; - // Phase 13.1.b : déclarer les bornes écran AVANT toute manipulation - // de la position curseur, sinon les deltas relatifs (PS/2 sous QEMU) - // s'accumulent hors-écran et hit_test ne trouve jamais les surfaces. - frontend.set_screen_size(fb_w as i32, fb_h as i32); // Phase 7.3 : curseur visible dès le démarrage, placé au centre du fb. frontend.set_cursor_initial_position((fb_w as i32) / 2, (fb_h as i32) / 2); info!("Wayland socket : {SOCKET_PATH}"); @@ -164,11 +154,6 @@ fn run() -> Result<(), Box> { let frame_period = Duration::from_millis(33); // ~30 fps let mut last_frame = Instant::now(); let mut tick: u32 = 0; - // Phase 13.1.b : Ctrl tenu, pour le raccourci compositor Ctrl+Q. - // ESC n'est plus interceptée — elle est forwardée aux clients afin que - // les apps tierces (cf. simple_window upstream) puissent décider elles-mêmes - // de se terminer sur ESC sans race avec la fermeture du socket compositor. - let mut ctrl_held = false; while start.elapsed() < total { tick = tick.wrapping_add(1); @@ -194,23 +179,12 @@ fn run() -> Result<(), Box> { tracing::debug!("{} input events from inputd", events.len()); } for ev in events { - // Track Ctrl (scancode 0x1D, set 1) avant le match : nécessaire - // pour le raccourci Ctrl+Q et OK à dupliquer côté clients via - // forward_input — wl_keyboard.key reste la source de vérité pour eux. - if let InputEvent::Key { - scancode: 0x1D, - pressed, - .. - } = ev - { - ctrl_held = pressed; - } match &ev { InputEvent::Key { scancode, pressed, .. - } if *pressed && *scancode == 0x10 && ctrl_held => { - // Ctrl+Q → exit compositor (ESC est réservé aux clients) - info!("Ctrl+Q → exit compositor"); + } if *pressed && *scancode == 0x01 => { + // Esc → exit + info!("Esc → exit"); let _ = frontend.flush_clients(); let _ = std::fs::remove_file(SOCKET_PATH); return Ok(()); @@ -255,11 +229,9 @@ fn run() -> Result<(), Box> { error!("flush err: {e}"); } - // Phase 13.1.b : tick log en debug! (saturait la console série). - // Réactivable avec RUST_LOG=debug. Fréquence réduite à toutes les - // 5s (~150 ticks @ 30fps) pour limiter encore le bruit. - if tick % 150 == 0 { - tracing::debug!( + // Log occasionnel + if tick % 30 == 0 { + info!( "tick={tick} surfaces={nb} elapsed={:.1}s", start.elapsed().as_secs_f32() ); diff --git a/crates/redox-wl-real-client-simple-window/src/main.rs b/crates/redox-wl-real-client-simple-window/src/main.rs index e7693e1..3f1aff2 100644 --- a/crates/redox-wl-real-client-simple-window/src/main.rs +++ b/crates/redox-wl-real-client-simple-window/src/main.rs @@ -70,23 +70,6 @@ fn dlog(s: &str) { SINK.get_or_init(DebugSink::new).writeln(s); } -fn is_interrupted_error(err: &(dyn std::error::Error + 'static)) -> bool { - if let Some(ioe) = err.downcast_ref::() { - return ioe.kind() == std::io::ErrorKind::Interrupted; - } - err.source().is_some_and(is_interrupted_error) -} - -fn is_connection_closed_error(err: &(dyn std::error::Error + 'static)) -> bool { - if let Some(ioe) = err.downcast_ref::() { - return matches!( - ioe.kind(), - std::io::ErrorKind::BrokenPipe | std::io::ErrorKind::ConnectionReset - ); - } - err.source().is_some_and(is_connection_closed_error) -} - // ---------- Buffer SHM (équivalent de tempfile + draw upstream) ---------- const INIT_W: u32 = 320; const INIT_H: u32 = 240; @@ -367,15 +350,7 @@ fn run() -> Result<(), Box> { dlog("[real-client] entering event loop"); while state.running { - match event_queue.blocking_dispatch(&mut state) { - Ok(_) => {} - Err(e) if is_interrupted_error(&e) => continue, - Err(e) if is_connection_closed_error(&e) => { - dlog("[real-client] compositor disconnected → exit cleanly"); - break; - } - Err(e) => return Err(e.into()), - } + event_queue.blocking_dispatch(&mut state)?; } dlog("[real-client] loop exited cleanly"); Ok(()) diff --git a/crates/redox-wl-test-client-subcompositor/Cargo.toml b/crates/redox-wl-test-client-subcompositor/Cargo.toml deleted file mode 100644 index 2aca781..0000000 --- a/crates/redox-wl-test-client-subcompositor/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "redox-wl-test-client-subcompositor" -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 } -wayland-protocols = { path = "../../../wayland-rs/wayland-protocols", default-features = false, features = ["client"] } -libc = "0.2" diff --git a/crates/redox-wl-test-client-subcompositor/src/main.rs b/crates/redox-wl-test-client-subcompositor/src/main.rs deleted file mode 100644 index fac5bcf..0000000 --- a/crates/redox-wl-test-client-subcompositor/src/main.rs +++ /dev/null @@ -1,390 +0,0 @@ -//! Phase 13.2.b.3 — Client de test visuel pour wl_subcompositor. -//! -//! Crée un toplevel parent 300×200 bleu uni avec bordure noire, et lui -//! attache une subsurface 60×60 rouge à l'offset (50, 50). Si le rendering -//! côté compositor (phase 13.2.b.2) est correct, on doit voir à l'écran : -//! -//! - Fenêtre bleue en haut-gauche (selon cascading compositor) -//! - Un carré rouge dedans, en haut-gauche de la fenêtre bleue, à 50px -//! du coin haut-gauche du parent -//! -//! Le client tourne ~60s puis exit. ESC ferme proprement avant timeout -//! (path keyboard via wl_keyboard.enter envoyé au focus = parent toplevel). -//! -//! NB : sans cascade sync ni propagation parent-move (limitations 13.2.b.2), -//! la subsurface est figée à la position de création. Acceptable pour -//! validation visuelle de base. - -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::{ - backend::Backend, - protocol::{ - wl_buffer::WlBuffer, wl_compositor::WlCompositor, wl_keyboard, wl_registry, - wl_seat::WlSeat, wl_shm::WlShm, wl_shm_pool::WlShmPool, - wl_subcompositor::WlSubcompositor, wl_subsurface::WlSubsurface, wl_surface::WlSurface, - }, - Connection, Dispatch, EventQueue, Proxy, QueueHandle, -}; -use wayland_protocols::xdg::shell::client::{ - xdg_surface::{self, XdgSurface}, - xdg_toplevel::{self, XdgToplevel}, - xdg_wm_base::{self, XdgWmBase}, -}; - -const SOCKET_PATH: &str = "/tmp/redox-wl-comp.sock"; - -// Parent : 300×200 bleu nuit -const PW: i32 = 300; -const PH: i32 = 200; -const PSTRIDE: i32 = PW * 4; -const PSIZE: i32 = PSTRIDE * PH; -const PCOLOR: u32 = 0xFF_22_44_AA; - -// Subsurface : 60×60 rouge, offset (50, 50) -const SW: i32 = 60; -const SH: i32 = 60; -const SSTRIDE: i32 = SW * 4; -const SSIZE: i32 = SSTRIDE * SH; -const SCOLOR: u32 = 0xFF_CC_30_30; -const SOFF_X: i32 = 50; -const SOFF_Y: i32 = 50; - -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 State { - compositor: Option, - shm: Option, - wm_base: Option, - seat: Option, - subcompositor: Option, - pending_serial: Option, - configured: bool, - running: bool, -} - -impl Dispatch for State { - fn event( - state: &mut Self, - registry: &wl_registry::WlRegistry, - event: wl_registry::Event, - _: &(), - _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, 1, qh, ())); - } - "xdg_wm_base" => { - state.wm_base = Some(registry.bind(name, version.min(5), qh, ())); - } - "wl_seat" => { - state.seat = Some(registry.bind(name, version.min(7), qh, ())); - } - "wl_subcompositor" => { - state.subcompositor = Some(registry.bind(name, 1, qh, ())); - } - _ => {} - } - } - } -} - -macro_rules! noop { - ($ty:ty) => { - impl Dispatch<$ty, ()> for State { - fn event( - _: &mut Self, - _: &$ty, - _: <$ty as Proxy>::Event, - _: &(), - _: &Connection, - _: &QueueHandle, - ) { - } - } - }; -} -noop!(WlCompositor); -noop!(WlShm); -noop!(WlShmPool); -noop!(WlBuffer); -noop!(WlSurface); -noop!(WlSeat); -noop!(WlSubcompositor); -noop!(WlSubsurface); - -impl Dispatch for State { - fn event( - _: &mut Self, - wm_base: &XdgWmBase, - event: xdg_wm_base::Event, - _: &(), - _: &Connection, - _: &QueueHandle, - ) { - if let xdg_wm_base::Event::Ping { serial } = event { - wm_base.pong(serial); - } - } -} - -impl Dispatch for State { - fn event( - state: &mut Self, - _: &XdgSurface, - event: xdg_surface::Event, - _: &(), - _: &Connection, - _: &QueueHandle, - ) { - if let xdg_surface::Event::Configure { serial } = event { - state.pending_serial = Some(serial); - state.configured = true; - } - } -} - -impl Dispatch for State { - fn event( - state: &mut Self, - _: &XdgToplevel, - event: xdg_toplevel::Event, - _: &(), - _: &Connection, - _: &QueueHandle, - ) { - if matches!(event, xdg_toplevel::Event::Close) { - state.running = false; - } - } -} - -// ESC → exit. Même logique qu'en 13.1.b sur le client simple_window. -impl Dispatch for State { - fn event( - state: &mut Self, - _: &wl_keyboard::WlKeyboard, - event: wl_keyboard::Event, - _: &(), - _: &Connection, - _: &QueueHandle, - ) { - if let wl_keyboard::Event::Key { key, .. } = event { - if key == 1 { - dlog("[sub-client] ESC → exit"); - state.running = false; - } - } - } -} - -unsafe fn create_shm(name: &str, size: i32, w: i32, h: i32, color: u32) -> 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("shm_open failed".into()); - } - 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()); - } - let pixels = std::slice::from_raw_parts_mut(p as *mut u32, (w * h) as usize); - // Remplissage uni avec bordure noire 2px - for y in 0..h { - for x in 0..w { - let on_border = x < 2 || x >= w - 2 || y < 2 || y >= h - 2; - pixels[(y * w + x) as usize] = if on_border { 0xFF_10_10_10 } else { color }; - } - } - libc::munmap(p, size as usize); - Ok(OwnedFd::from_raw_fd(fd)) -} - -fn run() -> Result<(), Box> { - dlog("[sub-client] start (Phase 13.2.b.3 — visual subsurface)"); - - 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 = State { - running: true, - ..State::default() - }; - event_queue.roundtrip(&mut state)?; - let compositor = state.compositor.clone().ok_or("no wl_compositor")?; - let shm = state.shm.clone().ok_or("no wl_shm")?; - let wm_base = state.wm_base.clone().ok_or("no xdg_wm_base")?; - let subcomp = state - .subcompositor - .clone() - .ok_or("no wl_subcompositor (compositor patché 13.2.b.1 ?)")?; - let seat = state.seat.clone(); - - // Bind keyboard pour gérer ESC - if let Some(s) = &seat { - let _kb = s.get_keyboard(&qh, ()); - } - - // --- Parent toplevel --- - let parent_surface = compositor.create_surface(&qh, ()); - let xdg_surface = wm_base.get_xdg_surface(&parent_surface, &qh, ()); - let toplevel = xdg_surface.get_toplevel(&qh, ()); - toplevel.set_title("Phase 13.2.b.3 — subcompositor visual".into()); - toplevel.set_app_id("redox.wl.test.client.subcompositor".into()); - parent_surface.commit(); - dlog("[sub-client] parent xdg_toplevel créé"); - - let start = std::time::Instant::now(); - while !state.configured && start.elapsed() < Duration::from_secs(5) { - event_queue.roundtrip(&mut state)?; - thread::sleep(Duration::from_millis(50)); - } - let serial = state.pending_serial.ok_or("no initial configure")?; - xdg_surface.ack_configure(serial); - state.pending_serial = None; - dlog(&format!("[sub-client] ack_configure({serial})")); - - // Parent buffer + attach + commit - let parent_fd = unsafe { create_shm("redox-sub-client-parent", PSIZE, PW, PH, PCOLOR) }?; - let parent_pool = shm.create_pool(parent_fd.as_fd(), PSIZE, &qh, ()); - let parent_buffer = parent_pool.create_buffer( - 0, - PW, - PH, - PSTRIDE, - wayland_client::protocol::wl_shm::Format::Argb8888, - &qh, - (), - ); - parent_surface.attach(Some(&parent_buffer), 0, 0); - parent_surface.commit(); - dlog("[sub-client] parent buffer attached + commit"); - - // --- Subsurface --- - let child_surface = compositor.create_surface(&qh, ()); - let subsurface: WlSubsurface = - subcomp.get_subsurface(&child_surface, &parent_surface, &qh, ()); - subsurface.set_position(SOFF_X, SOFF_Y); - subsurface.set_desync(); // mode desync → commit du child suffit pour appliquer - dlog(&format!( - "[sub-client] subsurface créée + set_position({SOFF_X}, {SOFF_Y}) + set_desync" - )); - - let child_fd = unsafe { create_shm("redox-sub-client-child", SSIZE, SW, SH, SCOLOR) }?; - let child_pool = shm.create_pool(child_fd.as_fd(), SSIZE, &qh, ()); - let child_buffer = child_pool.create_buffer( - 0, - SW, - SH, - SSTRIDE, - wayland_client::protocol::wl_shm::Format::Argb8888, - &qh, - (), - ); - child_surface.attach(Some(&child_buffer), 0, 0); - child_surface.commit(); - dlog("[sub-client] child buffer attached + commit"); - - // Re-commit du parent pour rendre les changements visibles en sync. - // (En desync ce n'est pas strictement nécessaire mais ça force un redraw.) - parent_surface.commit(); - - dlog("[sub-client] entering event loop"); - let deadline = std::time::Instant::now() + Duration::from_secs(60); - while state.running && std::time::Instant::now() < deadline { - match event_queue.blocking_dispatch(&mut state) { - Ok(_) => {} - Err(e) => { - let s = format!("{e}"); - if s.contains("Broken pipe") || s.contains("Connection reset") { - dlog("[sub-client] compositor disconnected → exit cleanly"); - break; - } - return Err(e.into()); - } - } - } - - dlog("[sub-client] loop exited cleanly"); - Ok(()) -} - -fn main() -> ExitCode { - match run() { - Ok(()) => { - dlog("[sub-client] PASS"); - ExitCode::SUCCESS - } - Err(e) => { - dlog(&format!("[sub-client] FAIL: {e}")); - ExitCode::FAILURE - } - } -} diff --git a/crates/redox-wl-test-wl-output/Cargo.toml b/crates/redox-wl-test-wl-output/Cargo.toml deleted file mode 100644 index f2a8e6e..0000000 --- a/crates/redox-wl-test-wl-output/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "redox-wl-test-wl-output" -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" - -[dev-dependencies] -# Test natif (CachyOS, pas Redox) : server + client en-process -# pour valider le gating de wl_output v1/v2/v3. -wayland-server = { path = "../../../wayland-rs/wayland-server", default-features = false } diff --git a/crates/redox-wl-test-wl-output/src/main.rs b/crates/redox-wl-test-wl-output/src/main.rs deleted file mode 100644 index 392256d..0000000 --- a/crates/redox-wl-test-wl-output/src/main.rs +++ /dev/null @@ -1,291 +0,0 @@ -//! Phase 13.2.a — client de test : version gating de wl_output. -//! -//! Se connecte au compositor, trouve le global wl_output, et le bind -//! 3 fois successivement aux versions 1, 2, 3. Pour chaque bind, dump -//! les events reçus et vérifie le gating attendu : -//! -//! v1 → Geometry + Mode uniquement (pas de Scale, pas de Done) -//! v2 → + Done (pas encore de Scale) -//! v3 → + Scale (gating conservateur côté serveur) -//! -//! Sortie sur stdout ET /scheme/debug pour récupération depuis serial -//! sous QEMU. Code de sortie 0 = PASS, 1 = FAIL si gating incorrect. -//! -//! NB : on s'aligne sur la procédure 13.1.b (connexion explicite au -//! socket, pas via WAYLAND_DISPLAY ; logs tee'd vers debug). Aucune -//! adaptation de fond — c'est juste un client wayland-client raw. - -use std::fs::OpenOptions; -use std::io::Write; -use std::os::unix::net::UnixStream; -use std::path::Path; -use std::process::ExitCode; -use std::sync::{Mutex, OnceLock}; -use std::thread; -use std::time::Duration; - -use wayland_client::{ - backend::Backend, - protocol::{wl_output, wl_registry}, - Connection, Dispatch, QueueHandle, -}; - -const SOCKET_PATH: &str = "/tmp/redox-wl-comp.sock"; - -// --- Logging tee vers stdout + /scheme/debug -------------------------------- - -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); -} - -// --- Attente du socket ----------------------------------------------------- - -fn wait_for_socket(path: &Path, attempts: u32, sleep_ms: u64) -> bool { - for _ in 0..attempts { - if path.exists() { - return true; - } - thread::sleep(Duration::from_millis(sleep_ms)); - } - false -} - -// --- État partagé ---------------------------------------------------------- - -/// Accumulateur d'events pour un binding wl_output donné. -#[derive(Default, Debug)] -struct OutputObservation { - seen_geometry: bool, - seen_mode: bool, - seen_scale: bool, - seen_done: bool, - mode_flags: u32, - mode_width: i32, - mode_height: i32, - mode_refresh: i32, - scale_factor: i32, -} - -/// État global du client de test : registry advertised globals + 3 observations. -#[derive(Default)] -struct State { - output_global_name: Option, - output_global_version: Option, - // 3 observations indexées 0=v1, 1=v2, 2=v3. - obs: [OutputObservation; 3], - // Phase courante en cours de capture (None entre les binds). - current_version_index: Option, -} - -impl Dispatch for State { - fn event( - state: &mut Self, - _r: &wl_registry::WlRegistry, - event: wl_registry::Event, - _data: &(), - _conn: &Connection, - _qh: &QueueHandle, - ) { - if let wl_registry::Event::Global { - name, - interface, - version, - } = event - { - if interface == "wl_output" { - state.output_global_name = Some(name); - state.output_global_version = Some(version); - dlog(&format!( - "[test-output] registry: wl_output advertised name={name} version={version}" - )); - } - } - } -} - -impl Dispatch for State { - fn event( - state: &mut Self, - _r: &wl_output::WlOutput, - event: wl_output::Event, - _data: &(), - _conn: &Connection, - _qh: &QueueHandle, - ) { - let Some(idx) = state.current_version_index else { - // Ne devrait pas arriver — events après un done observable - // sur un binding qu'on a fini d'examiner. On ignore. - return; - }; - let obs = &mut state.obs[idx]; - match event { - wl_output::Event::Geometry { .. } => obs.seen_geometry = true, - wl_output::Event::Mode { - flags, - width, - height, - refresh, - } => { - obs.seen_mode = true; - obs.mode_flags = flags.into(); - obs.mode_width = width; - obs.mode_height = height; - obs.mode_refresh = refresh; - } - wl_output::Event::Scale { factor } => { - obs.seen_scale = true; - obs.scale_factor = factor; - } - wl_output::Event::Done => obs.seen_done = true, - _ => {} - } - } -} - -// --- main ------------------------------------------------------------------ - -fn main() -> ExitCode { - dlog("[test-output] start (Phase 13.2.a — version gating wl_output)"); - - let path = Path::new(SOCKET_PATH); - if !wait_for_socket(path, 50, 100) { - dlog(&format!( - "[test-output] FAIL: socket {SOCKET_PATH} introuvable après 5s" - )); - return ExitCode::FAILURE; - } - - let stream = match UnixStream::connect(SOCKET_PATH) { - Ok(s) => s, - Err(e) => { - dlog(&format!("[test-output] FAIL: connect: {e}")); - return ExitCode::FAILURE; - } - }; - stream.set_nonblocking(false).ok(); - - let backend = match Backend::connect(stream) { - Ok(b) => b, - Err(e) => { - dlog(&format!("[test-output] FAIL: backend: {e}")); - return ExitCode::FAILURE; - } - }; - let conn = Connection::from_backend(backend); - - let mut event_queue = conn.new_event_queue::(); - let qh = event_queue.handle(); - let display = conn.display(); - let _registry = display.get_registry(&qh, ()); - - let mut state = State::default(); - - // Premier roundtrip : capture les globals - if let Err(e) = event_queue.roundtrip(&mut state) { - dlog(&format!("[test-output] FAIL: roundtrip registry: {e}")); - return ExitCode::FAILURE; - } - - let global_name = match state.output_global_name { - Some(n) => n, - None => { - dlog("[test-output] FAIL: aucun global wl_output annoncé par le compositor"); - return ExitCode::FAILURE; - } - }; - let advertised_version = state.output_global_version.unwrap_or(0); - dlog(&format!( - "[test-output] global wl_output découvert (name={global_name}, version annoncée={advertised_version})" - )); - - if advertised_version < 3 { - dlog(&format!( - "[test-output] WARN: version annoncée < 3, le test v3 va échouer (compositor pas à jour ?)" - )); - } - - // Bind à v1, v2, v3 successivement avec un roundtrip entre chaque. - let registry = display.get_registry(&qh, ()); - for (idx, version) in [(0usize, 1u32), (1, 2), (2, 3)].iter() { - state.current_version_index = Some(*idx); - dlog(&format!("[test-output] bind wl_output @ v{version} …")); - let _output = registry.bind::(global_name, *version, &qh, ()); - // Plusieurs roundtrips pour s'assurer que tous les events init - // ont atterri (geometry + mode + éventuel scale + éventuel done). - for _ in 0..3 { - if let Err(e) = event_queue.roundtrip(&mut state) { - dlog(&format!( - "[test-output] FAIL: roundtrip bind v{version}: {e}" - )); - return ExitCode::FAILURE; - } - } - let obs = &state.obs[*idx]; - dlog(&format!( - "[test-output] v{version} reçu : geometry={} mode={} scale={} done={} | mode={}x{}@{}mHz flags={} scale_factor={}", - obs.seen_geometry, obs.seen_mode, obs.seen_scale, obs.seen_done, - obs.mode_width, obs.mode_height, obs.mode_refresh, obs.mode_flags, obs.scale_factor - )); - state.current_version_index = None; - } - - // --- Verdict --- - let mut errors: Vec = Vec::new(); - - let v1 = &state.obs[0]; - if !v1.seen_geometry { errors.push("v1: geometry manquant".into()); } - if !v1.seen_mode { errors.push("v1: mode manquant".into()); } - if v1.seen_scale { errors.push("v1: scale envoyé alors qu'attendu v3+".into()); } - if v1.seen_done { errors.push("v1: done envoyé alors qu'attendu v2+".into()); } - - let v2 = &state.obs[1]; - if !v2.seen_geometry { errors.push("v2: geometry manquant".into()); } - if !v2.seen_mode { errors.push("v2: mode manquant".into()); } - if v2.seen_scale { errors.push("v2: scale envoyé alors qu'attendu v3+ (gating conservateur)".into()); } - if !v2.seen_done { errors.push("v2: done attendu mais manquant".into()); } - - let v3 = &state.obs[2]; - if !v3.seen_geometry { errors.push("v3: geometry manquant".into()); } - if !v3.seen_mode { errors.push("v3: mode manquant".into()); } - if !v3.seen_scale { errors.push("v3: scale attendu mais manquant".into()); } - if !v3.seen_done { errors.push("v3: done attendu mais manquant".into()); } - - // Sanity check sur les valeurs : mode flags doit contenir CURRENT (0x1) - // et PREFERRED (0x2), donc valeur 3. - if v3.mode_flags & 0x1 == 0 { - errors.push(format!("v3: mode flags ne contient pas CURRENT (got 0x{:x})", v3.mode_flags)); - } - if v3.mode_flags & 0x2 == 0 { - errors.push(format!("v3: mode flags ne contient pas PREFERRED (got 0x{:x})", v3.mode_flags)); - } - if v3.scale_factor != 1 { - errors.push(format!("v3: scale_factor attendu 1, got {}", v3.scale_factor)); - } - - if errors.is_empty() { - dlog("[test-output] PASS — gating v1/v2/v3 conforme aux attentes"); - ExitCode::SUCCESS - } else { - for e in &errors { - dlog(&format!("[test-output] FAIL: {e}")); - } - ExitCode::FAILURE - } -} diff --git a/crates/redox-wl-test-wl-output/tests/gating_native.rs b/crates/redox-wl-test-wl-output/tests/gating_native.rs deleted file mode 100644 index decd427..0000000 --- a/crates/redox-wl-test-wl-output/tests/gating_native.rs +++ /dev/null @@ -1,247 +0,0 @@ -//! Test natif (CachyOS, hors QEMU) : version gating de wl_output. -//! -//! On simule en-process l'init du global wl_output v3 avec la MÊME logique -//! de gating que `redox-wl-wayland-frontend::lib.rs` : -//! - geometry, mode : toujours envoyés -//! - scale : seulement si version >= 3 -//! - done : seulement si version >= 2 -//! -//! Un seul process : serveur sur un thread + client sur le thread principal, -//! reliés par `UnixStream::pair()`. Pas besoin de Redox, pas besoin de QEMU. - -use std::os::unix::net::UnixStream; -use std::sync::Arc; -use std::thread; -use std::time::Duration; - -use wayland_server::backend::{ClientData, ClientId, DisconnectReason}; -use wayland_server::{ - protocol::wl_output as srv_wl_output, Client as SrvClient, DataInit, Display, DisplayHandle, - GlobalDispatch, Resource, -}; - -use wayland_client::{ - backend::Backend, - protocol::{wl_output as cli_wl_output, wl_registry}, - Connection, Dispatch as CliDispatch, QueueHandle, -}; - -const OUTPUT_VERSION: u32 = 3; -const SCREEN_W: i32 = 1280; -const SCREEN_H: i32 = 800; - -// ============= server side ============= - -struct ServerState; - -struct StubClientData; -impl ClientData for StubClientData { - fn initialized(&self, _: ClientId) {} - fn disconnected(&self, _: ClientId, _: DisconnectReason) {} -} - -impl GlobalDispatch for ServerState { - fn bind( - _state: &mut Self, - _handle: &DisplayHandle, - _client: &SrvClient, - resource: wayland_server::New, - _data: &(), - data_init: &mut DataInit<'_, Self>, - ) { - let output = data_init.init(resource, ()); - let version = output.version(); - - // Logique identique à redox-wl-wayland-frontend/src/lib.rs - output.geometry( - 0, - 0, - 0, - 0, - srv_wl_output::Subpixel::Unknown, - "Redox".to_string(), - "redox-wl-output:0".to_string(), - srv_wl_output::Transform::Normal, - ); - - let mode_flags = srv_wl_output::Mode::Current | srv_wl_output::Mode::Preferred; - output.mode(mode_flags, SCREEN_W, SCREEN_H, 60_000); - - if version >= 3 { - output.scale(1); - } - if version >= 2 { - output.done(); - } - } -} - -impl wayland_server::Dispatch for ServerState { - fn request( - _state: &mut Self, - _client: &SrvClient, - _r: &srv_wl_output::WlOutput, - _request: srv_wl_output::Request, - _data: &(), - _dh: &DisplayHandle, - _data_init: &mut DataInit<'_, Self>, - ) { - } -} - -// ============= client side ============= - -#[derive(Default, Debug, Clone, Copy)] -struct OutputObs { - seen_geometry: bool, - seen_mode: bool, - seen_scale: bool, - seen_done: bool, - mode_flags: u32, - scale_factor: i32, -} - -#[derive(Default)] -struct ClientState { - global_name: Option, - obs: [OutputObs; 3], - cur: Option, -} - -impl CliDispatch for ClientState { - fn event( - state: &mut Self, - _: &wl_registry::WlRegistry, - event: wl_registry::Event, - _: &(), - _: &Connection, - _: &QueueHandle, - ) { - if let wl_registry::Event::Global { - name, interface, .. - } = event - { - if interface == "wl_output" { - state.global_name = Some(name); - } - } - } -} - -impl CliDispatch for ClientState { - fn event( - state: &mut Self, - _: &cli_wl_output::WlOutput, - event: cli_wl_output::Event, - _: &(), - _: &Connection, - _: &QueueHandle, - ) { - let Some(idx) = state.cur else { return }; - let obs = &mut state.obs[idx]; - match event { - cli_wl_output::Event::Geometry { .. } => obs.seen_geometry = true, - cli_wl_output::Event::Mode { flags, .. } => { - obs.seen_mode = true; - obs.mode_flags = flags.into(); - } - cli_wl_output::Event::Scale { factor } => { - obs.seen_scale = true; - obs.scale_factor = factor; - } - cli_wl_output::Event::Done => obs.seen_done = true, - _ => {} - } - } -} - -// ============= test ============= - -#[test] -fn wl_output_v123_gating() { - // 1. Setup serveur dans un thread - let (s_stream, c_stream) = UnixStream::pair().expect("UnixStream::pair"); - - let server_thread = thread::spawn(move || { - let mut display: Display = Display::new().unwrap(); - let dh = display.handle(); - dh.create_global::(OUTPUT_VERSION, ()); - - s_stream.set_nonblocking(true).unwrap(); - let _client = display - .handle() - .insert_client(s_stream, Arc::new(StubClientData)) - .unwrap(); - - let mut state = ServerState; - // Dispatch loop : on tourne ~5 secondes pour absorber tous les - // round-trips du test client. Le test signale la fin en fermant - // le socket côté client, ce qui sort dispatch_clients sur error. - let start = std::time::Instant::now(); - while start.elapsed() < Duration::from_secs(5) { - let _ = display.dispatch_clients(&mut state); - let _ = display.flush_clients(); - thread::sleep(Duration::from_millis(10)); - } - }); - - // 2. Client connecte via l'autre bout du socket - c_stream.set_nonblocking(false).unwrap(); - let backend = Backend::connect(c_stream).unwrap(); - let conn = Connection::from_backend(backend); - let mut event_queue = conn.new_event_queue::(); - let qh = event_queue.handle(); - let display = conn.display(); - let _reg1 = display.get_registry(&qh, ()); - - let mut state = ClientState::default(); - event_queue.roundtrip(&mut state).expect("roundtrip registry"); - - let global_name = state.global_name.expect("wl_output non advertised"); - - // 3. Bind aux 3 versions - let registry = display.get_registry(&qh, ()); - for (idx, version) in [(0usize, 1u32), (1, 2), (2, 3)].iter() { - state.cur = Some(*idx); - let _o = registry.bind::(global_name, *version, &qh, ()); - for _ in 0..3 { - event_queue.roundtrip(&mut state).expect("roundtrip bind"); - } - state.cur = None; - } - - // 4. Drop client to make server exit - drop(conn); - server_thread.join().unwrap(); - - // 5. Asserts - let v1 = &state.obs[0]; - assert!(v1.seen_geometry, "v1: geometry attendu"); - assert!(v1.seen_mode, "v1: mode attendu"); - assert!(!v1.seen_scale, "v1: scale ne devrait PAS être envoyé"); - assert!(!v1.seen_done, "v1: done ne devrait PAS être envoyé"); - - let v2 = &state.obs[1]; - assert!(v2.seen_geometry, "v2: geometry attendu"); - assert!(v2.seen_mode, "v2: mode attendu"); - assert!( - !v2.seen_scale, - "v2: scale ne devrait PAS être envoyé (gating conservateur v3+)" - ); - assert!(v2.seen_done, "v2: done attendu"); - - let v3 = &state.obs[2]; - assert!(v3.seen_geometry, "v3: geometry attendu"); - assert!(v3.seen_mode, "v3: mode attendu"); - assert!(v3.seen_scale, "v3: scale attendu"); - assert!(v3.seen_done, "v3: done attendu"); - assert_eq!(v3.scale_factor, 1, "v3: scale_factor doit être 1"); - assert!( - v3.mode_flags & 0x1 != 0, - "v3: mode flags doit contenir CURRENT (0x1)" - ); - assert!( - v3.mode_flags & 0x2 != 0, - "v3: mode flags doit contenir PREFERRED (0x2)" - ); -} diff --git a/crates/redox-wl-test-wl-output/tests/subcompositor_native.rs b/crates/redox-wl-test-wl-output/tests/subcompositor_native.rs deleted file mode 100644 index 40646c6..0000000 --- a/crates/redox-wl-test-wl-output/tests/subcompositor_native.rs +++ /dev/null @@ -1,364 +0,0 @@ -//! Test natif (CachyOS, hors QEMU) : wl_subcompositor protocole (phase -//! 13.2.b.1). -//! -//! Vérifie que : -//! 1. `wl_subcompositor` est annoncé comme global à v1 -//! 2. Le bind du global réussit -//! 3. `get_subsurface(child, parent)` instancie un `wl_subsurface` sans -//! erreur protocole -//! 4. `set_position`, `set_sync`, `set_desync` ne génèrent pas d'erreur -//! 5. `destroy` sur le wl_subsurface ne génère pas d'erreur -//! -//! Logique serveur dupliquée verbatim de redox-wl-wayland-frontend/src/lib.rs -//! (GlobalDispatch + Dispatch pour wl_subcompositor et wl_subsurface + -//! minimum pour wl_compositor pour pouvoir créer 2 wl_surfaces). -//! -//! Si une erreur protocole est envoyée (bad_surface, etc.), wayland-client -//! propage en error sur l'event_queue, et le test panique avec un message -//! clair. - -use std::os::unix::net::UnixStream; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; -use std::thread; -use std::time::Duration; - -use wayland_server::backend::{ClientData, ClientId, DisconnectReason}; -use wayland_server::{ - protocol::{ - wl_compositor as srv_wl_compositor, wl_subcompositor as srv_wl_subcompositor, - wl_subsurface as srv_wl_subsurface, wl_surface as srv_wl_surface, - }, - Client as SrvClient, DataInit, Display, DisplayHandle, GlobalDispatch, -}; - -use wayland_client::{ - backend::Backend, - protocol::{wl_compositor, wl_registry, wl_subcompositor, wl_subsurface, wl_surface}, - Connection, Dispatch as CliDispatch, QueueHandle, -}; - -const COMPOSITOR_VERSION: u32 = 5; -const SUBCOMPOSITOR_VERSION: u32 = 1; - -// ============= server-side: state minimal ============= - -struct ServerState; - -struct StubClientData; -impl ClientData for StubClientData { - fn initialized(&self, _: ClientId) {} - fn disconnected(&self, _: ClientId, _: DisconnectReason) {} -} - -// Données par-wl_subsurface, duplicate de notre prod code. -#[allow(dead_code)] -struct SubsurfaceData { - parent: srv_wl_surface::WlSurface, - child: srv_wl_surface::WlSurface, - position: Mutex<(i32, i32)>, - sync: AtomicBool, -} - -// --- wl_compositor (minimum pour fabriquer des wl_surfaces) --- - -impl GlobalDispatch for ServerState { - fn bind( - _state: &mut Self, - _handle: &DisplayHandle, - _client: &SrvClient, - resource: wayland_server::New, - _data: &(), - data_init: &mut DataInit<'_, Self>, - ) { - data_init.init(resource, ()); - } -} - -impl wayland_server::Dispatch for ServerState { - fn request( - _state: &mut Self, - _client: &SrvClient, - _r: &srv_wl_compositor::WlCompositor, - request: srv_wl_compositor::Request, - _data: &(), - _dh: &DisplayHandle, - data_init: &mut DataInit<'_, Self>, - ) { - if let srv_wl_compositor::Request::CreateSurface { id } = request { - data_init.init(id, ()); - } - } -} - -impl wayland_server::Dispatch for ServerState { - fn request( - _state: &mut Self, - _client: &SrvClient, - _r: &srv_wl_surface::WlSurface, - _request: srv_wl_surface::Request, - _data: &(), - _dh: &DisplayHandle, - _data_init: &mut DataInit<'_, Self>, - ) { - } -} - -// --- wl_subcompositor (sous test) --- - -impl GlobalDispatch for ServerState { - fn bind( - _state: &mut Self, - _handle: &DisplayHandle, - _client: &SrvClient, - resource: wayland_server::New, - _data: &(), - data_init: &mut DataInit<'_, Self>, - ) { - data_init.init(resource, ()); - } -} - -impl wayland_server::Dispatch for ServerState { - fn request( - _state: &mut Self, - _client: &SrvClient, - _r: &srv_wl_subcompositor::WlSubcompositor, - request: srv_wl_subcompositor::Request, - _data: &(), - _dh: &DisplayHandle, - data_init: &mut DataInit<'_, Self>, - ) { - match request { - srv_wl_subcompositor::Request::Destroy => {} - srv_wl_subcompositor::Request::GetSubsurface { - id, - surface, - parent, - } => { - let data = Arc::new(SubsurfaceData { - parent, - child: surface, - position: Mutex::new((0, 0)), - sync: AtomicBool::new(true), - }); - data_init.init(id, data); - } - _ => {} - } - } -} - -impl wayland_server::Dispatch> - for ServerState -{ - fn request( - _state: &mut Self, - _client: &SrvClient, - _r: &srv_wl_subsurface::WlSubsurface, - request: srv_wl_subsurface::Request, - data: &Arc, - _dh: &DisplayHandle, - _data_init: &mut DataInit<'_, Self>, - ) { - match request { - srv_wl_subsurface::Request::Destroy => {} - srv_wl_subsurface::Request::SetPosition { x, y } => { - *data.position.lock().unwrap() = (x, y); - } - srv_wl_subsurface::Request::PlaceAbove { sibling: _ } - | srv_wl_subsurface::Request::PlaceBelow { sibling: _ } => {} - srv_wl_subsurface::Request::SetSync => { - data.sync.store(true, Ordering::Relaxed); - } - srv_wl_subsurface::Request::SetDesync => { - data.sync.store(false, Ordering::Relaxed); - } - _ => {} - } - } -} - -// ============= client-side ============= - -#[derive(Default)] -struct ClientState { - compositor_global: Option, - subcompositor_global: Option, - subcompositor_version: Option, - last_error: Option, -} - -impl CliDispatch for ClientState { - fn event( - state: &mut Self, - _: &wl_registry::WlRegistry, - event: wl_registry::Event, - _: &(), - _: &Connection, - _: &QueueHandle, - ) { - if let wl_registry::Event::Global { - name, - interface, - version, - } = event - { - if interface == "wl_compositor" { - state.compositor_global = Some(name); - } else if interface == "wl_subcompositor" { - state.subcompositor_global = Some(name); - state.subcompositor_version = Some(version); - } - } - } -} - -impl CliDispatch for ClientState { - fn event( - _: &mut Self, - _: &wl_compositor::WlCompositor, - _: wl_compositor::Event, - _: &(), - _: &Connection, - _: &QueueHandle, - ) { - } -} - -impl CliDispatch for ClientState { - fn event( - _: &mut Self, - _: &wl_surface::WlSurface, - _: wl_surface::Event, - _: &(), - _: &Connection, - _: &QueueHandle, - ) { - } -} - -impl CliDispatch for ClientState { - fn event( - _: &mut Self, - _: &wl_subcompositor::WlSubcompositor, - _: wl_subcompositor::Event, - _: &(), - _: &Connection, - _: &QueueHandle, - ) { - } -} - -impl CliDispatch for ClientState { - fn event( - _: &mut Self, - _: &wl_subsurface::WlSubsurface, - _: wl_subsurface::Event, - _: &(), - _: &Connection, - _: &QueueHandle, - ) { - } -} - -// ============= test ============= - -#[test] -fn wl_subcompositor_protocol_full_cycle() { - let (s_stream, c_stream) = UnixStream::pair().expect("UnixStream::pair"); - - let server_thread = thread::spawn(move || { - let mut display: Display = Display::new().unwrap(); - let dh = display.handle(); - dh.create_global::( - COMPOSITOR_VERSION, - (), - ); - dh.create_global::( - SUBCOMPOSITOR_VERSION, - (), - ); - - s_stream.set_nonblocking(true).unwrap(); - let _client = display - .handle() - .insert_client(s_stream, Arc::new(StubClientData)) - .unwrap(); - - let mut state = ServerState; - let start = std::time::Instant::now(); - while start.elapsed() < Duration::from_secs(5) { - let _ = display.dispatch_clients(&mut state); - let _ = display.flush_clients(); - thread::sleep(Duration::from_millis(10)); - } - }); - - c_stream.set_nonblocking(false).unwrap(); - let backend = Backend::connect(c_stream).unwrap(); - let conn = Connection::from_backend(backend); - let mut event_queue = conn.new_event_queue::(); - let qh = event_queue.handle(); - let display = conn.display(); - let _reg = display.get_registry(&qh, ()); - - let mut state = ClientState::default(); - event_queue - .roundtrip(&mut state) - .expect("roundtrip registry"); - - // 1. Vérifs annonces globals - let compositor_name = state.compositor_global.expect("wl_compositor non annoncé"); - let subcomp_name = state - .subcompositor_global - .expect("wl_subcompositor non annoncé"); - assert_eq!( - state.subcompositor_version, - Some(1), - "wl_subcompositor doit être annoncé à v1, got {:?}", - state.subcompositor_version - ); - - // 2. Bind wl_compositor pour créer 2 surfaces - let registry = display.get_registry(&qh, ()); - let compositor = - registry.bind::(compositor_name, 5, &qh, ()); - let parent = compositor.create_surface(&qh, ()); - let child = compositor.create_surface(&qh, ()); - - // 3. Bind wl_subcompositor + get_subsurface - let subcomp = - registry.bind::(subcomp_name, 1, &qh, ()); - let subsurface = subcomp.get_subsurface(&child, &parent, &qh, ()); - - // 4. Exercer set_position / set_sync / set_desync - subsurface.set_position(50, 50); - subsurface.set_sync(); - subsurface.set_desync(); - subsurface.place_above(&parent); // valable même si parent n'est pas sibling — on no-op côté serveur - subsurface.set_position(100, 100); - - // 5. Roundtrip pour driver tous les events et capter d'éventuelles erreurs - for _ in 0..5 { - event_queue - .roundtrip(&mut state) - .expect("roundtrip pendant exercise"); - } - - // 6. Destroy - subsurface.destroy(); - event_queue - .roundtrip(&mut state) - .expect("roundtrip après destroy"); - - // 7. Cleanup - drop(conn); - server_thread.join().unwrap(); - - assert!( - state.last_error.is_none(), - "Erreur protocole détectée : {:?}", - state.last_error - ); -} diff --git a/crates/redox-wl-wayland-frontend/src/lib.rs b/crates/redox-wl-wayland-frontend/src/lib.rs index 938e5a1..2e08dab 100644 --- a/crates/redox-wl-wayland-frontend/src/lib.rs +++ b/crates/redox-wl-wayland-frontend/src/lib.rs @@ -36,8 +36,8 @@ use wayland_protocols::xdg::shell::server::{ use wayland_server::{ backend::{ClientData, ClientId, DisconnectReason}, protocol::{ - wl_buffer, wl_callback, wl_compositor, wl_keyboard, wl_output, wl_pointer, wl_region, - wl_seat, wl_shm, wl_shm_pool, wl_subcompositor, wl_subsurface, wl_surface, + wl_buffer, wl_callback, wl_compositor, wl_keyboard, wl_pointer, wl_region, wl_seat, wl_shm, + wl_shm_pool, wl_surface, }, Client, DataInit, Display as WlDisplay, DisplayHandle, GlobalDispatch, Resource, }; @@ -48,14 +48,6 @@ const COMPOSITOR_VERSION: u32 = 5; const SHM_VERSION: u32 = 1; const XDG_WM_BASE_VERSION: u32 = 5; const SEAT_VERSION: u32 = 7; -// Phase 13.2.a : wl_output v3 (couvre geometry + mode + scale + done + -// release request, sans v4 name/description qu'on ne fournit pas encore). -const OUTPUT_VERSION: u32 = 3; -// Phase 13.2.b.1 : wl_subcompositor v1 (la seule version qui existe ; -// pas d'évolution depuis l'introduction du protocole). Implémentation -// "protocole only" — bind + handlers ACK les requests, mais le rendering -// des subsurfaces sera wiré en 13.2.b.2. -const SUBCOMPOSITOR_VERSION: u32 = 1; /// Taille suggérée par défaut pour les nouvelles fenêtres xdg_toplevel. /// Le client peut respecter ou non ; on utilise sa propre taille de buffer @@ -186,11 +178,6 @@ struct SurfaceData { /// surfaces d'un client déconnecté (à ce moment-là `surf.client()` /// retourne déjà None, donc on ne pourrait pas re-déduire le ClientId). client_id: Mutex>, - /// Phase 13.2.b.2 : si cette surface est une subsurface, lien vers les - /// données du wl_subsurface qui la pilote (parent ref, offset, sync). - /// Setté par `wl_subcompositor.GetSubsurface`. Lu au commit de cette - /// surface pour calculer sa position absolue = parent.pos + offset. - subsurface_link: Mutex>>, } /// Données par-xdg_surface : référence à la wl_surface sous-jacente + @@ -203,23 +190,6 @@ struct XdgSurfaceData { acked_serial: Mutex, } -/// Phase 13.2.b.1 — Données par-wl_subsurface. -/// -/// État protocolaire d'une subsurface : référence vers sa surface enfant -/// et son parent, position en pending (modifiable via SetPosition), -/// mode sync/desync. Pour 13.2.b.1 ces valeurs sont stockées mais pas -/// encore consommées par compose_into (rendering wiré en 13.2.b.2). -/// -/// Sync mode : default = true (synchronized) per spec. En mode sync, -/// les changements de la subsurface n'apparaissent qu'au commit du -/// parent. En desync, ils apparaissent au commit de la subsurface elle-même. -struct SubsurfaceData { - parent: wl_surface::WlSurface, - child: wl_surface::WlSurface, - position: Mutex<(i32, i32)>, - sync: AtomicBool, -} - /// Données par-xdg_toplevel : title, app_id, ref vers son xdg_surface. #[derive(Default)] struct XdgToplevelData { @@ -404,11 +374,6 @@ pub struct WaylandFrontend { /// Mise à jour par `forward_input(PointerMotion / PointerMotionRelative)`. cursor_x: i32, cursor_y: i32, - /// Phase 13.1.b : bornes écran pour clamper le cursor. Si non - /// défini par `set_screen_size`, valeurs i32::MAX → le clamp est un - /// no-op (compat tests qui ne configurent pas le frontend). - screen_w: i32, - screen_h: i32, /// Counter monotone pour les serials seat events (différent de next_xdg_serial). next_input_serial: u32, /// Timestamp incrémental pour les events seat (ms-like). @@ -464,17 +429,6 @@ impl WaylandFrontend { dh.create_global::(SHM_VERSION, ()); dh.create_global::(XDG_WM_BASE_VERSION, ()); dh.create_global::(SEAT_VERSION, ()); - // Phase 13.2.a : wl_output déclaré comme global. Single output - // mappé au framebuffer du compositor. Les events init (geometry/ - // mode/scale/done) sont envoyés dans le bind, cf GlobalDispatch - // plus bas. - dh.create_global::(OUTPUT_VERSION, ()); - // Phase 13.2.b.1 : wl_subcompositor pour les surfaces parent-enfant. - // Bind + acceptation des requests, pas encore de rendering (13.2.b.2). - dh.create_global::( - SUBCOMPOSITOR_VERSION, - (), - ); let listener = wayland_server::ListeningSocket::bind_absolute(socket_path.to_path_buf())?; @@ -491,8 +445,6 @@ impl WaylandFrontend { focused_surface: None, cursor_x: 0, cursor_y: 0, - screen_w: i32::MAX, - screen_h: i32::MAX, next_input_serial: 1, input_time_ms: 0, cursor_surface_id: None, @@ -864,15 +816,12 @@ impl WaylandFrontend { }; let time = self.alloc_input_time(); let serial = self.alloc_input_serial(); - // Phase 13.1.b : Wayland `wl_keyboard.key` attend le keycode - // **evdev** brut, pas X11. L'offset +8 est entre evdev et - // X11 (historique X11 où KeyCode minimum = 8), pas entre - // PS/2 et evdev — pour les touches alphanumériques de base, - // PS/2 Set 1 scancode == evdev keycode (ESC=1, Q=16, etc.). - // Bug originel : on envoyait scancode+8 → simple_window - // upstream qui teste `key == 1` pour ESC recevait 9 et - // n'exitait jamais. - let key = *scancode as u32; + // Wayland keycodes = scancode evdev = scancode +8 on linux + // (cf xkb_keycodes minimum=8). On garde le scancode brut + // ici en attendant une keymap correcte (US Linux evdev offset + // ne s'applique pas forcément à orbclient ; à durcir + // quand on aura une vraie keymap XKB). + let key = (*scancode as u32).saturating_add(8); let state = if *pressed { wl_keyboard::KeyState::Pressed } else { @@ -885,23 +834,8 @@ impl WaylandFrontend { } } RedoxInputEvent::PointerMotion { x, y } => { - // Phase 13.1.c : ps2d (sous Redox+QEMU) délivre des coords - // absolues en 0..65535 (cf orbital/src/scheme.rs:1575-1587, - // commentaire upstream « ps2d gives us absolute mouse events - // with x and y in the range 0..65535 »). On scale vers les - // pixels écran avec la même formule qu'orbital : x*screen_w/65536. - // Le diviseur 65536 (pas 65535) est volontaire — fixed-point - // 16-bit, donne max=screen-1 quand input=65535. - let scaled_x = ((*x as i64) * (self.screen_w as i64)) / 65536; - let scaled_y = ((*y as i64) * (self.screen_h as i64)) / 65536; - tracing::debug!( - target: "cursor", - "ABS x={x} y={y} → pixel ({scaled_x},{scaled_y}) screen={}x{}", - self.screen_w, self.screen_h - ); - self.cursor_x = scaled_x as i32; - self.cursor_y = scaled_y as i32; - self.clamp_cursor(); + self.cursor_x = *x; + self.cursor_y = *y; self.cursor_visible = true; // Phase 7.7 : si drag actif, déplacer la surface au lieu // d'envoyer motion au client. @@ -923,22 +857,8 @@ impl WaylandFrontend { } } RedoxInputEvent::PointerMotionRelative { dx, dy } => { - let before_x = self.cursor_x; - let before_y = self.cursor_y; self.cursor_x = self.cursor_x.saturating_add(*dx); self.cursor_y = self.cursor_y.saturating_add(*dy); - let after_raw_x = self.cursor_x; - let after_raw_y = self.cursor_y; - self.clamp_cursor(); - // Phase 13.1.c : trace dx/dy + cursor avant/après pour - // identifier si les deltas dérivent toujours positivement - // (axe inversé ? cumul d'edge ?) ou si le clamp masque le - // problème. - tracing::debug!( - target: "cursor", - "REL dx={dx} dy={dy} before=({before_x},{before_y}) raw=({after_raw_x},{after_raw_y}) clamped=({},{})", - self.cursor_x, self.cursor_y - ); self.cursor_visible = true; // Phase 7.7 : si drag actif, déplacer la surface. if self.apply_interactive_drag() { @@ -1169,26 +1089,6 @@ impl WaylandFrontend { self.cursor_x = x; self.cursor_y = y; self.cursor_visible = true; - self.clamp_cursor(); - } - - /// Phase 13.1.b : déclare la taille du framebuffer pour borner le - /// curseur. Sans ça, les deltas `PointerMotionRelative` (PS/2 sous QEMU) - /// s'accumulent et finissent par sortir l'écran → tous les hit_test - /// retournent None → aucune fenêtre ne reçoit le focus → ESC tombe - /// dans le vide. Doit être appelé après `bind_absolute` et idéalement - /// re-appelé si la résolution change. - pub fn set_screen_size(&mut self, w: i32, h: i32) { - self.screen_w = w.max(1); - self.screen_h = h.max(1); - self.clamp_cursor(); - } - - fn clamp_cursor(&mut self) { - let max_x = (self.screen_w - 1).max(0); - let max_y = (self.screen_h - 1).max(0); - self.cursor_x = self.cursor_x.clamp(0, max_x); - self.cursor_y = self.cursor_y.clamp(0, max_y); } /// Force la position du curseur à tout moment. Utile pour tests @@ -1201,7 +1101,6 @@ impl WaylandFrontend { pub fn set_cursor_position(&mut self, x: i32, y: i32) { self.cursor_x = x; self.cursor_y = y; - self.clamp_cursor(); self.cursor_visible = true; let _ = self.apply_interactive_drag(); } @@ -1318,7 +1217,6 @@ impl wayland_server::Dispatch for WaylandFronte // est encore vivant. À la déconnexion, `surf.client()` // retournera None et on ne pourrait plus déduire ce mapping. client_id: Mutex::new(Some(_client.id())), - subsurface_link: Mutex::new(None), }; let surf = data_init.init(id, Arc::new(data)); // Phase 7.4 : enregistrer le mapping SurfaceId → WlSurface @@ -1527,35 +1425,6 @@ impl wayland_server::Dispatch> for Wayla } let is_cursor = data.is_cursor.load(Ordering::Relaxed); - // Phase 13.2.b.2 : si cette surface est une subsurface, - // recalculer sa position absolue à partir de parent.pos + - // offset, avant que le commit applique pending→current. - // Détection via subsurface_link, peuplé par GetSubsurface. - let subsurface_link = data.subsurface_link.lock().unwrap().clone(); - let is_subsurface = subsurface_link.is_some(); - if let Some(sub_data) = &subsurface_link { - let parent_id_opt = sub_data - .parent - .data::>() - .and_then(|sd| *sd.id.lock().unwrap()); - if let Some(parent_id) = parent_id_opt { - let parent_pos = state.registry.get(parent_id).map(|s| { - let cs = s.current(); - (cs.x, cs.y) - }); - if let Some((px, py)) = parent_pos { - let (ox, oy) = *sub_data.position.lock().unwrap(); - state.registry.modify_pending(id, |s| { - s.x = px + ox; - s.y = py + oy; - }); - tracing::debug!( - "subsurface commit: pos absolue = parent({},{}) + offset({},{}) = ({},{})", - px, py, ox, oy, px + ox, py + oy - ); - } - } - } // Lire le buffer attaché (s'il y en a un). On `take()` pour // que le pending_buffer soit vidé après chaque commit @@ -1595,44 +1464,11 @@ impl wayland_server::Dispatch> for Wayla buf.release(); } state.registry.commit(id); - if !is_cursor && !is_subsurface { + if !is_cursor { // Promouvoir au top du Z-order au commit (politique simple : // dernière surface qui commit = au-dessus). À raffiner en // phase 7 (focus, raise on click, etc.). - // Phase 13.2.b.2 : les subsurfaces ne raisent pas - // indépendamment de leur parent ; elles vivent à - // parent.z + 1 par convention de leur création. state.registry.raise(id); - - // Phase 13.2.b.2 fix bug visuel : si on vient de raise un - // parent toplevel, raise aussi toutes ses subsurfaces - // pour qu'elles restent au-dessus dans le z_order. Sinon - // un re-commit du parent (sans buffer) après création - // des subsurfaces les masquerait. Cas observé sur le - // client de test 13.2.b.3 où le parent.commit() final - // de rafraîchissement faisait disparaître le carré rouge. - let parent_id_to_match = id; - let children_to_raise: Vec = state - .surfaces_by_id - .iter() - .filter_map(|(sid, surf)| { - let sd = surf.data::>()?; - let link = sd.subsurface_link.lock().unwrap(); - let ld = link.as_ref()?; - let parent_sid = ld - .parent - .data::>() - .and_then(|psd| *psd.id.lock().unwrap())?; - if parent_sid == parent_id_to_match { - Some(*sid) - } else { - None - } - }) - .collect(); - for cid in children_to_raise { - state.registry.raise(cid); - } } // Frame callbacks en attente → bump dans la queue globale @@ -1640,13 +1476,11 @@ impl wayland_server::Dispatch> for Wayla state.frame_callbacks.append(&mut *cbs); drop(cbs); - if !is_cursor && !is_subsurface { + if !is_cursor { // Phase 7.2 : la surface qui vient de commiter et raise // devient automatiquement la surface focalisée. Envoie les // events keyboard/pointer enter/leave en conséquence. // Une surface curseur n'a évidemment pas le focus — on skip. - // Phase 13.2.b.2 : pareil pour les subsurfaces — le - // focus reste sur le parent toplevel. state.set_focus(Some(_resource.clone())); } } @@ -2263,215 +2097,6 @@ impl wayland_server::Dispatch> f } } -// ===================================================================== -// wl_output (phase 13.2.a) -// ===================================================================== -// -// Single output mappé sur le framebuffer du compositor. Les events -// d'init (geometry / mode / scale / done) sont envoyés dans `bind` à -// chaque nouvelle resource bound par un client, gated par la version -// négociée : -// - geometry, mode : v1+, toujours envoyés -// - scale : v3+ (gating conservateur ; spec autorise v2+ mais on évite -// l'ambiguïté sur les clients v2 qui pourraient mal gérer) -// - done : v2+ -// La taille pixel vient de `screen_w / screen_h`, setté par -// `set_screen_size()` au démarrage du compositor. Refresh hardcodé à -// 60 Hz (60000 mHz) — notre boucle main tourne à ~30 fps mais les apps -// modernes ignorent largement ce champ, et 60 Hz est un standard sûr. - -impl GlobalDispatch for WaylandFrontend { - fn bind( - state: &mut Self, - _handle: &DisplayHandle, - _client: &Client, - resource: wayland_server::New, - _data: &(), - data_init: &mut DataInit<'_, Self>, - ) { - let output = data_init.init(resource, ()); - let version = output.version(); - - // geometry : position dans l'espace logique multi-écran (0,0 - // ici), taille physique inconnue (0 mm, valeur "ne sait pas" par - // convention Wayland), subpixel/make/model/transform placeholders. - output.geometry( - 0, - 0, - 0, - 0, - wl_output::Subpixel::Unknown, - "Redox".to_string(), - "redox-wl-output:0".to_string(), - wl_output::Transform::Normal, - ); - - // mode : taille pixel courante = framebuffer. Flags Current + - // Preferred (un seul mode, c'est de facto le préféré). - let mode_flags = wl_output::Mode::Current | wl_output::Mode::Preferred; - output.mode(mode_flags, state.screen_w, state.screen_h, 60_000); - - // scale v3+ : facteur 1 (pas de HiDPI pour notre framebuffer). - if version >= 3 { - output.scale(1); - } - - // done v2+ : signale la fin du batch init pour que le client - // ne dispatche son OutputInfo qu'une fois tous les events arrivés. - if version >= 2 { - output.done(); - } - } -} - -impl wayland_server::Dispatch for WaylandFrontend { - fn request( - _state: &mut Self, - _client: &Client, - _r: &wl_output::WlOutput, - request: wl_output::Request, - _data: &(), - _dh: &DisplayHandle, - _data_init: &mut DataInit<'_, Self>, - ) { - match request { - wl_output::Request::Release => { - // v3+ : le client libère son binding wl_output. La resource - // est nettoyée automatiquement par wayland-server quand on - // ne traite pas d'autre request — on no-op simplement. - } - _ => {} - } - } -} - -// ===================================================================== -// wl_subcompositor / wl_subsurface (phase 13.2.b.1) -// ===================================================================== -// -// Scope 13.2.b.1 : implémentation protocolaire uniquement. On accepte le -// bind du global, on instancie wl_subsurface dans GetSubsurface et on -// route correctement les requests SetPosition / PlaceAbove,Below / -// SetSync,Desync / Destroy. La donnée est stockée dans SubsurfaceData. -// -// Ce qui N'est PAS fait ici (reporté à 13.2.b.2) : -// - rendering effectif des subsurfaces (compose_into ignore encore les -// SubsurfaceData associés aux SurfaceData enfants) ; -// - hit-test des subsurfaces ; -// - cascade sync : un commit du parent doit valider tous les state -// pending des subsurfaces en mode sync ; -// - vérification du « rôle unique » (spec : une wl_surface ne peut -// avoir qu'un seul rôle de vie ; on devrait refuser GetSubsurface -// sur une surface déjà-xdg_toplevel). Pour l'instant on log un warn -// si on détecte le cas, mais on n'envoie pas bad_surface. - -impl GlobalDispatch for WaylandFrontend { - fn bind( - _state: &mut Self, - _handle: &DisplayHandle, - _client: &Client, - resource: wayland_server::New, - _data: &(), - data_init: &mut DataInit<'_, Self>, - ) { - data_init.init(resource, ()); - } -} - -impl wayland_server::Dispatch for WaylandFrontend { - fn request( - _state: &mut Self, - _client: &Client, - _r: &wl_subcompositor::WlSubcompositor, - request: wl_subcompositor::Request, - _data: &(), - _dh: &DisplayHandle, - data_init: &mut DataInit<'_, Self>, - ) { - match request { - wl_subcompositor::Request::Destroy => { - // No-op : wayland-server nettoie la resource automatiquement. - } - wl_subcompositor::Request::GetSubsurface { - id, - surface, - parent, - } => { - // Vérif soft : rôle déjà attribué ? (xdg_toplevel notamment). - // Pour 13.2.b.1 on se contente de logger ; le protocole exige - // un bad_surface error mais on attendra une phase ultérieure - // pour l'enforcer (role-tracking). - if let Some(child_data) = surface.data::>() { - let id_lock = child_data.id.lock().unwrap(); - if id_lock.is_some() { - tracing::debug!( - "GetSubsurface: surface child déjà dans registry (id={:?})", - *id_lock - ); - } - } - let data = Arc::new(SubsurfaceData { - parent: parent.clone(), - child: surface.clone(), - position: Mutex::new((0, 0)), - sync: AtomicBool::new(true), // default = sync per spec - }); - // Phase 13.2.b.2 : lier la SurfaceData de la child au - // SubsurfaceData pour que le commit puisse calculer la - // position absolue parent.pos + offset. - if let Some(child_sd) = surface.data::>() { - *child_sd.subsurface_link.lock().unwrap() = Some(data.clone()); - } - data_init.init(id, data); - tracing::debug!("wl_subcompositor.get_subsurface: subsurface créée"); - } - _ => {} - } - } -} - -impl wayland_server::Dispatch> - for WaylandFrontend -{ - fn request( - _state: &mut Self, - _client: &Client, - _r: &wl_subsurface::WlSubsurface, - request: wl_subsurface::Request, - data: &Arc, - _dh: &DisplayHandle, - _data_init: &mut DataInit<'_, Self>, - ) { - match request { - wl_subsurface::Request::Destroy => { - // Spec : la wl_surface enfant est unmapped immédiatement, - // perd sa position et son z-order. Pour 13.2.b.1 on no-op - // (pas de mapping registered, donc rien à unmap côté - // rendering qui n'existe pas encore). - } - wl_subsurface::Request::SetPosition { x, y } => { - *data.position.lock().unwrap() = (x, y); - tracing::debug!("wl_subsurface.set_position({x}, {y})"); - } - wl_subsurface::Request::PlaceAbove { sibling: _ } - | wl_subsurface::Request::PlaceBelow { sibling: _ } => { - // z-order relatif entre subsurfaces siblings. - // Single-subsurface use-case 13.2.b.1 : no-op. - tracing::debug!("wl_subsurface.place_above/below (no-op pour 13.2.b.1)"); - } - wl_subsurface::Request::SetSync => { - data.sync.store(true, Ordering::Relaxed); - tracing::debug!("wl_subsurface.set_sync"); - } - wl_subsurface::Request::SetDesync => { - data.sync.store(false, Ordering::Relaxed); - tracing::debug!("wl_subsurface.set_desync"); - } - _ => {} - } - } -} - // --------------------------------------------------------------------------- // Tests unitaires xdg-shell (sprint 0 point 4). // diff --git a/docs/phase13-1-b-observations.md b/docs/phase13-1-b-observations.md deleted file mode 100644 index ba0d2d6..0000000 --- a/docs/phase13-1-b-observations.md +++ /dev/null @@ -1,298 +0,0 @@ -# Phase 13.1.b — Observation runtime du client tiers `simple_window` - -> Document produit le 2026-05-16, suite de -> [`phase13-1-real-client-simple-window.md`](phase13-1-real-client-simple-window.md). -> -> **Scope** : observer le comportement runtime de -> `redox-wl-real-client-simple-window` (port Redox de l'exemple upstream -> wayland-rs) face au compositor, consigner les déviations vs l'attendu, -> appliquer les fixes nécessaires côté **compositor uniquement** (le code -> client reste verbatim upstream sauf 4 adaptations Redox documentées en 13.1). -> -> **Verdict** : ✅ **13.1.b validée** — 6/6 checkpoints OK après application -> de 3 fixes ciblés. - -## Verdict - -``` -[real-client] simple_window port Redox — start -[real-client] entering event loop -INFO redox_wl_wayland_frontend: focus change: None → Some(SurfaceId(0)) -[real-client] ESC → exit -[real-client] loop exited cleanly -[real-client] PASS -INFO redox_wl_wayland_frontend: garbage_collect: client … → destroyed 1 surfaces -``` - -Le client tiers, **non écrit par nous**, traverse le pipeline complet : -connexion socket, bind des globals, création toplevel, commit buffer -shm, focus auto-grant à la création de surface, réception `wl_keyboard.key` -sur ESC, sortie propre, garbage collection côté compositor. - -## Checklist 6/6 (cf. `phase13-1-real-client-simple-window.md:137-153`) - -| # | Vérif | Résultat | Évidence | -|---|---|---|---| -| 1 | Connexion socket | ✅ | `[real-client] entering event loop` | -| 2 | Globals reçus (`wl_compositor`, `wl_shm`, `wl_seat`, `xdg_wm_base`) | ✅ | Surface créée (sinon le client échouerait au bind) | -| 3 | Configure initial ack | ✅ | Implicite : le client atteint son event loop et dessine | -| 4 | Premier pixel — gradient ARGB visible | ✅ | Confirmation visuelle utilisateur sur fenêtre QEMU graphique | -| 5 | **ESC ferme le client proprement** | ✅ | `[real-client] ESC → exit` + `[real-client] PASS` | -| 6 | Sortie propre, pas de FAIL | ✅ | `[real-client] loop exited cleanly` + garbage collect côté compo | - -## Fixes appliqués - -Les bugs ci-dessous étaient **invisibles** dans nos clients de test 6.x/7.x -parce qu'ils étaient adaptés à notre compositor (notamment l'offset +8 -côté key). Précisément la valeur d'un client tiers : il révèle les paths -que nos clients maison cachent. Anticipation correcte de la doc 13.1 -(« Pas écrit par nous : c'est sa principale qualité »). - -### Fix 1 — ESC mangée par le compositor au lieu d'être forwardée - -**Symptôme** : à l'appui sur ESC, le compositor exitait et fermait le socket, -le client recevait `Broken pipe (os error 32)` avant d'avoir pu traiter -sa propre logique ESC. Résultat : `[real-client] FAIL: Backend error: …`. - -**Cause** (`crates/redox-wl-compositor/src/main.rs:183-191` avant fix) : - -```rust -InputEvent::Key { scancode, pressed, .. } if *pressed && *scancode == 0x01 => { - info!("Esc → exit"); - let _ = frontend.flush_clients(); - let _ = std::fs::remove_file(SOCKET_PATH); - return Ok(()); // <-- exit AVANT forward_input() -} -``` - -`forward_input(&ev)` à la ligne 202 n'était jamais atteint pour ESC. - -**Fix** : ESC retombe dans `_ => {}` → forwarded normalement. -Raccourci compositor déplacé sur `Ctrl+Q` (scancode Q = `0x10` + état -`ctrl_held` tracké manuellement, scancode Ctrl L = `0x1D`). - -Modifs : -- Ajout `ctrl_held: bool` au-dessus de la boucle main -- Tracking du Ctrl avant le `match &ev` -- Remplacement de l'arm ESC par `scancode == 0x10 && ctrl_held` - -### Fix 2 — Cursor s'accumule hors-écran (deltas PS/2 non bornés) - -**Symptôme** : `left-click @ (10444, 10566) → hit_test = None` (écran 1440×900). -Les clics se faisaient à des coordonnées qui croissaient sans limite, -aucune fenêtre ne matchait jamais le hit_test → aucune surface ne pouvait -être focalisée par clic. - -**Cause** (`crates/redox-wl-wayland-frontend/src/lib.rs:860-861` avant fix) : - -```rust -RedoxInputEvent::PointerMotionRelative { dx, dy } => { - self.cursor_x = self.cursor_x.saturating_add(*dx); - self.cursor_y = self.cursor_y.saturating_add(*dy); - // ... aucun clamp -} -``` - -Sous QEMU PS/2, le compositor reçoit des deltas relatifs. Sans clamp, -ils s'accumulent vers l'infini (limité par `saturating_add` mais à -i32::MAX bien au-delà de l'écran). - -**Fix** : ajout d'un état `screen_w/screen_h` au frontend (init -`i32::MAX` = no-op tant que pas configuré), setter `set_screen_size(w, h)` -appelé par le compositor après `bind_absolute`, helper `clamp_cursor()` -invoqué après chaque update cursor (les 3 chemins : `PointerMotion`, -`PointerMotionRelative`, `set_cursor_position`). - -### Fix 3 — Keycode envoyé `scancode + 8` au lieu d'evdev brut (bug racine) - -**Symptôme** : même après les fixes 1 et 2, ESC n'avait aucun effet sur -le client. Pas de `[real-client] ESC → exit` malgré le focus auto-grant -correct et le forward_input appelé. - -**Cause** (`crates/redox-wl-wayland-frontend/src/lib.rs:826-831` avant fix) : - -```rust -// Wayland keycodes = scancode evdev = scancode +8 on linux -// (cf xkb_keycodes minimum=8). On garde le scancode brut -// ici en attendant une keymap correcte ... -let key = (*scancode as u32).saturating_add(8); -``` - -**Le commentaire est faux.** L'offset +8 c'est entre **evdev et X11** -(X11 KeyCode minimum = 8 historiquement), pas entre PS/2 et evdev. Pour -les touches alphanumériques de base, **PS/2 Set 1 scancode == evdev keycode** -(ESC=1, Q=16, etc.). Wayland (spec `wl_keyboard.key`) attend des -**evdev keycodes**, pas des X11. - -Pour ESC : -- Compositor recevait scancode = `0x01` (correct) -- Envoyait `key = 0x01 + 8 = 9` au client -- Client testait `if key == 1` → `false` → ESC ignoré - -C'est précisément le path qui *passait* avec nos clients de test 6.x/7.x -parce qu'on contrôlait les deux côtés et avait probablement aligné les -constantes. simple_window upstream attend les evdev raw. - -**Fix** : suppression du `+8`. Le scancode brut suffit pour les touches -de base. À durcir quand on aura une vraie keymap XKB (ajustements pour -les touches étendues 0xE0xx). - -```rust -let key = *scancode as u32; -``` - -### Fix 4 — Event loop client : handling propre des erreurs IO transientes - -**Symptôme** : même avec les fixes 1/2/3, le `blocking_dispatch` du client -remontait une `Err(...)` sur `Interrupted` ou `BrokenPipe`/`ConnectionReset` -et le `?` final renvoyait `Err`, ce qui faisait que `main` loggait -`[real-client] FAIL` au lieu de `PASS` même en sortie propre par ESC -(le compositor flush peut générer un `Interrupted` transitoire). - -**Cause** (`crates/redox-wl-real-client-simple-window/src/main.rs`, event -loop avant fix) : - -```rust -while state.running { - event_queue.blocking_dispatch(&mut state)?; // any Err → FAIL -} -``` - -**Fix** : ajout de helpers `is_interrupted_error` / -`is_connection_closed_error` (récursifs sur la chaîne `source()` pour -attraper les erreurs wrappées par `wayland-client`), et match explicite : - -- `Interrupted` → `continue` (retry transparent) -- `BrokenPipe` / `ConnectionReset` → log `compositor disconnected → exit - cleanly` et `break` (terminaison normale si le compo s'éteint avant le - client) -- Autre erreur → `return Err(e.into())` → FAIL légitime - -⚠️ **5e adaptation Redox côté client**, qui dépasse les 4 originellement -autorisées par `phase13-1-real-client-simple-window.md:43-65`. Justification : -ce handling est **upstream-compatible** (l'exemple `simple_window.rs` -de wayland-rs aurait le même comportement sur d'autres OS si le compositor -crashait juste après un ESC). Ce n'est pas un workaround pour un manque -de notre compositor mais une robustesse standard que l'exemple upstream -ne couvre pas car il suppose un environnement de dev où le compositor ne -meurt jamais. À déclarer comme adaptation #5 dans la prochaine MAJ de la -doc 13.1. - -### Bonus — bruit console réduit - -`info!("tick=…")` toutes les ~1s dans la boucle main saturait la console -série. Passé en `debug!` + fréquence /5 (~5s). Réactivable avec -`RUST_LOG=debug`. Cf. `crates/redox-wl-compositor/src/main.rs:252-258`. - -## Bugs secondaires à isoler (hors scope 13.1.b) - -### B.1 — Curseur software ne suit pas la souris correctement - -Pendant les tests, le curseur restait visuellement coincé en bas-droite -de l'écran (typiquement `(width-1, height-1)`). Les deltas relatifs -semblaient toujours majoritairement positifs. Hypothèses à creuser : - -- Mauvaise polarité des deltas Y sous QEMU/Redox (axe inversé ?) -- Conversion absolu→relatif en amont dans `inputd` qui dérive -- Souris non-grabbed par QEMU sur fenêtre non focusée → accumulation - de moves "vers le bord" lors des allers-retours hôte/guest - -Test à faire en 13.1.c : logger systématiquement les deltas `(dx, dy)` -reçus et tracer leur somme cumulée vs déplacement réel attendu. - -### B.2 — Page fault ion sur broken pipe d'un job background - -À chaque fois qu'un client Wayland en background `&` se termine par -broken pipe (Ctrl+Q compositor avant exit client), **ion crashe** : - -``` -ion: ([Page fault: 0000000000000070 US ... -RIP: 00000000002335ae -... kernel::context::signal:INFO -- UNHANDLED EXCEPTION ... NAME /usr/bin/ion -``` - -Adresse fautive `0x70` = déréf null + offset, reproductible. Bug Redox -upstream (ion), pas notre compositor. À reporter sur -`gitlab.redox-os.org/redox-os/ion`. - -### B.3 — VT switching hôte intercepté par CachyOS - -`Ctrl+Alt+F2..F6` mangés par X/Wayland CachyOS avant QEMU, donc -impossible de switcher de VT depuis l'hôte vers un Redox guest. Pas un -bug, contrainte connue. Workaround pratique : tout faire sur le seul VT -graphique (celui que la fenêtre QEMU affiche), ou utiliser la console -série pour les commandes shell et la fenêtre graphique pour le clavier -PS/2 du compositor. - -### B.4 — Image RedoxFS étroite (682 Mo initiale) - -`pkg update` saturait le filesystem. Résolu pendant cette session par -`qemu-img resize harddrive.img 10G` + `redoxfs-resize`. À considérer si -on automatise la procédure : taille par défaut de l'image dans le -fork/script doit être ≥ 4 Go pour permettre install paquets standards. - -## Screenshots à prendre (suivant doc 13.1) - -À ajouter au repo en `docs/phase13-1-b-*.png` : - -- `phase13-1-b-gradient-running.png` — fenêtre gradient ARGB visible sur - fond bleu nuit, dans la fenêtre QEMU graphique, avant ESC. -- `phase13-1-b-after-esc.png` — fond bleu seul après ESC (le compositor - continue, la fenêtre client a disparu suite à `garbage_collect`). -- `phase13-1-b-log-pass.png` — capture console montrant - `[real-client] PASS` + `garbage_collect: ... → destroyed 1 surfaces`. - -(Optionnel — la validation textuelle des logs suffit pour le critère -de fin, mais les screenshots aident à comparer rapidement aux phases -ultérieures.) - -## Procédure de test runtime — version finale - -Mise à jour vs `phase13-1-real-client-simple-window.md:82-135` : tout -se fait dans la **fenêtre QEMU graphique** (la console série de l'hôte -ne peut pas envoyer ESC à inputd). - -### 1. Hôte CachyOS - -```bash -cd ~/Projets/Redox/redox-wayland-compositor -./run-qemu.sh -``` - -### 2. Fenêtre QEMU graphique, après login `root` / `password` - -```sh -rm -f /tmp/redox-wl-comp.sock # défensif si résidu d'un test antérieur -redox-wl-compositor & -sleep 1 -redox-wl-real-client-simple-window -``` - -### 3. Toujours sur la fenêtre graphique - -- Vérifier visuellement : fond bleu + petite fenêtre gradient ARGB -- Presser `ESC` une fois -- Attendu : `[real-client] ESC → exit`, `[real-client] loop exited cleanly`, - `[real-client] PASS`, retour au prompt -- Optionnel : `Ctrl+Q` pour arrêter le compositor (sinon il timeout à - 180s ou tu peux `kill %0` depuis le shell) - -## Critère de fin 13.1.b - -> Le client tiers `redox-wl-real-client-simple-window`, port verbatim -> de `wayland-rs/wayland-client/examples/simple_window.rs` (sauf les 4 -> adaptations Redox de 13.1), traverse le pipeline Wayland complet vers -> notre compositor jusqu'à `[real-client] PASS` sur ESC. - -**✅ Validé 2026-05-16.** 3 fixes appliqués sur le compositor, aucun -sur le code client (qui reste verbatim upstream). - -## Fichiers modifiés - -``` -crates/redox-wl-compositor/src/main.rs # Fix 1 (ESC→client, Ctrl+Q→compo) + bonus bruit -crates/redox-wl-wayland-frontend/src/lib.rs # Fix 2 (clamp cursor) + Fix 3 (keycode evdev brut) -crates/redox-wl-real-client-simple-window/src/main.rs # Fix 4 (handling IO transient dans event loop) -run-qemu.sh # cleanup défensif (check /dev/fuse, IMAGE/REDOXFS overridables) -docs/phase13-1-b-observations.md # ce document -``` diff --git a/docs/phase13-1-c-cursor.md b/docs/phase13-1-c-cursor.md deleted file mode 100644 index b916d9e..0000000 --- a/docs/phase13-1-c-cursor.md +++ /dev/null @@ -1,152 +0,0 @@ -# Phase 13.1.c — Curseur souris : conversion HID→pixel - -> Document produit le 2026-05-16, suite de -> [`phase13-1-b-observations.md`](phase13-1-b-observations.md). -> -> **Scope** : isoler et corriger le bug B.1 listé en 13.1.b — « curseur -> software ne suit pas la souris correctement, reste coincé en bas-droite -> de l'écran à `(width-1, height-1)` ». Diagnostic via instrumentation -> ciblée, fix conforme à l'algo d'orbital upstream, validation runtime. -> -> **Verdict** : ✅ **13.1.c validée** — le curseur suit fidèlement la -> souris, les coordonnées de clic sont dans la plage écran. - -## Cause racine - -Sous Redox + QEMU, `ps2d` délivre les coords absolues du pointer dans -un **espace virtuel 16-bit (0..65535)**, pas en pixels écran. Notre -branche `PointerMotion` faisait une assignation directe : - -```rust -self.cursor_x = *x; // x ∈ 0..65535 ⇒ cursor_x ∈ 0..65535 -``` - -Le `clamp_cursor()` ajouté en 13.1.b saturait alors à `screen_w - 1` à -chaque event → cursor coincé en bas-droite. Le bug B.1 13.1.b n'était -pas une dérive de deltas accumulés mais une **mauvaise interprétation de -l'unité d'entrée**. - -## Diagnostic - -Instrumentation `tracing::debug!(target: "cursor", ...)` ajoutée dans les -deux branches motion de `WaylandFrontend::forward_input` pour logger les -valeurs brutes reçues. Avec `RUST_LOG=info,cursor=debug` (devenu le filtre -par défaut de `init_tracing`), une session de tests motion délibérés a -révélé : - -``` -DEBUG cursor: ABS x=61642 y=15564 screen=1280x800 -DEBUG cursor: ABS x=50174 y=20970 screen=1280x800 -DEBUG cursor: ABS x=43006 y=23592 screen=1280x800 -DEBUG cursor: ABS x=42084 y=23918 screen=1280x800 -``` - -Valeurs maximales observées ≈ 62000 / 24000 sur écran 1280×800, ratio -~50× pour x et ~30× pour y. Hypothèse 16-bit confirmée par la doc -upstream orbital : - -``` -// orbital/src/scheme.rs:1576 -// ps2d gives us absolute mouse events with x and y in the range 0..65535. -``` - -## Fix - -Conversion fixed-point en pixel via la même formule qu'orbital -(`orbital/src/scheme.rs:1583-1586`) : - -```rust -let scaled_x = ((*x as i64) * (self.screen_w as i64)) / 65536; -let scaled_y = ((*y as i64) * (self.screen_h as i64)) / 65536; -self.cursor_x = scaled_x as i32; -self.cursor_y = scaled_y as i32; -self.clamp_cursor(); // garde-fou pour valeurs hors-spec -``` - -Diviseur `65536` (pas `65535`) volontaire — fixed-point 16-bit, donne -`max output = screen - 1` quand `input = 65535`. Cohérent avec -l'arithmétique d'orbital. - -Le `clamp_cursor` ajouté en 13.1.b reste utile comme garde-fou (si un -device sort de la plage 0..65535) mais ne masque plus le bug principal. - -## Validation runtime - -Test sur écran 1280×800 (fenêtre QEMU graphique, après reboot Redox -frais). Mouvement délibéré de la souris du coin haut-gauche au coin -bas-droit en passant par le centre. Logs filtrés : - -``` -DEBUG cursor: ABS x=18994 y=19660 → pixel (370,239) screen=1280x800 ← haut-gauche -DEBUG cursor: ABS x=33176 y=33094 → pixel (647,403) screen=1280x800 ← centre -DEBUG cursor: ABS x=64560 y=11714 → pixel (1260,142) screen=1280x800 ← haut-droit -DEBUG cursor: ABS x=37476 y=33176 → pixel (731,404) screen=1280x800 ← retour centre -``` - -Coordonnées pixel cohérentes avec la position physique de la souris, -toutes dans la plage `0..1279 × 0..799`. Pas de saturation au coin. - -Clics au passage : - -``` -DEBUG redox_wl_wayland_frontend: left-click @ (522, 303) → hit_test = None -DEBUG redox_wl_wayland_frontend: left-click @ (746, 175) → hit_test = None -DEBUG redox_wl_wayland_frontend: left-click @ (370, 239) → hit_test = None -``` - -`hit_test = None` attendu : aucun client n'était lancé pendant ce test. -Les coords sont en revanche bien dans l'espace écran et pourront matcher -un toplevel quand un client tournera. - -## Bonus DX (developer experience) - -Type pénible de taper `RUST_LOG=info,redox_wl_wayland_frontend::cursor=debug` -dans la fenêtre QEMU graphique (pas de copier-coller possible). -Modification du filtre par défaut dans `init_tracing` : - -```rust -EnvFilter::new("info,cursor=debug") -``` - -Tu tapes juste `redox-wl-compositor`, tu vois les traces cursor sans -spam des ticks. Override toujours possible via `RUST_LOG=` au launch. - -## Sous-bug B.2 (page fault ion) — non traité - -Reste documenté pour follow-up upstream. Repro fiable : -1. Lancer un job background Wayland (`redox-wl-real-client-simple-window &`) -2. Tuer le compositor avant que le client n'exit (`Ctrl+Q` sans avoir - pressé ESC d'abord) -3. ion crashe avec page fault `0x70` - -À reporter sur `gitlab.redox-os.org/redox-os/ion`. Hors scope du compositor. - -## Critère de fin 13.1.c - -> Le curseur software du compositor suit fidèlement les déplacements -> souris dans toute l'aire de la fenêtre QEMU graphique, sans saturer -> à un bord, sans dérive monotone. Les coordonnées de clic sont dans -> la plage écran et `hit_test` peut matcher une surface visible. - -**✅ Validé 2026-05-16.** Conversion HID→pixel alignée sur l'algo -upstream d'orbital, instrumentation laissée en place pour debug futur. - -## Fichiers modifiés - -``` -crates/redox-wl-wayland-frontend/src/lib.rs # Conversion 0..65535 → pixels écran dans PointerMotion - # Traces tracing target="cursor" -crates/redox-wl-compositor/src/main.rs # Filtre tracing par défaut "info,cursor=debug" -docs/phase13-1-c-cursor.md # ce document -``` - -## Limites connues - -- `PointerMotionRelative` (deltas relatifs) reste instrumenté mais n'a - jamais été observé sous QEMU PS/2 + qemu-xhci. Si un jour Redox passe - à de la souris purement relative, on aura un second `set_screen_size` - + clamp à valider. -- L'aspect ratio entre l'espace HID 65536² et l'écran 1280×800 n'est - pas carré (1.6 vs 1) — la précision diagonale peut sembler légèrement - décalée. Strictement OK pour 13.1.c, à observer en 13.2+ avec un - vrai client graphique qui dessine sous le curseur (drag, drawing). diff --git a/docs/phase13-1-c-ion-bug-b2-upstream.md b/docs/phase13-1-c-ion-bug-b2-upstream.md deleted file mode 100644 index 2c9ed01..0000000 --- a/docs/phase13-1-c-ion-bug-b2-upstream.md +++ /dev/null @@ -1,167 +0,0 @@ -# Bug B.2 — Rapport pour upstream Redox ion - -> Document produit le 2026-05-16 dans le cadre de -> [`phase13-1-c-cursor.md`](phase13-1-c-cursor.md). À copier-coller (en -> traduisant en anglais) dans une issue sur -> `gitlab.redox-os.org/redox-os/ion/-/issues` quand on aura un compte. - ---- - -## Title - -ion: page fault `0x70` when a background job dies on broken pipe (Wayland client + compositor scenario) - -## Environment - -- **Redox image** : harddrive.img du 2026-05-08 (build x86_64 desktop, rebuild depuis `redox-src` la veille) -- **ion** : version embarquée dans cette image (binaire stripped, source upstream synced via cookbook recipe `core/ion`) -- **QEMU** : `qemu-system-x86_64` avec `-enable-kvm -cpu host -k fr` sur Linux host (CachyOS 7.0.8) -- **Repro** : 100 % reproductible sur 3 sessions distinctes avec reboot frais entre chaque - -## Symptôme - -Quand un job background termine sur `Broken pipe` (ex: client Wayland dont le compositor vient de fermer son socket), **ion crashe avec un page fault à l'adresse 0x70**. Le kernel logue UNHANDLED EXCEPTION pour le process ion. Le shell devient indisponible jusqu'au prochain login. - -``` -ion: ([Page fault: 0000000000000070 US -RFLAG: 0000000000010212 -CS: 000000000000002b -RIP: 00000000002335ae -RSP: 0000000000bb0e60 -SS: 0000000000000023 -FSBASE 000000000092b000 -GSBASE 0000000000000000 -KGSBASE ffff80007fed8000 -RAX: 0000000000000070 <-- valeur fautive = offset du field -RCX: 0000000000000000 -RDX: 0000000000233550 -RDI: 0000000000000001 -RSI: 00000000006c9970 -R8: 0000000000000000 -R9: 0000000000000000 -R10: 0000000000000000 -R11: 0000000000000008 -RBX: 000000000092b000 -RBP: 0000000000bb0e70 -R12: 000000000092acc8 -R13: 00000000006afe20 -R14: 0000000000000001 -R15: 00000000006afe20 - FP 0000000000bb0e70: PC 0000000000f27745 - FP 0000000000bb0f40: PC 0000000000f0a167 - FP 0000000000bb0f70: PC 0000000000f0a02d - FP 0000000000bb0ff0: PC 0000000000eabd43 - - FP ffff80001798fe80: PC ffffffff8008a42d - kernel::arch::x86_shared::interrupt::exception::page::inner - FP ffff80001798ff50: PC ffffffff80087ee7 - kernel::arch::x86_shared::interrupt::exception::page - 0000000000bb0e70: GUARD PAGE -kernel::context::signal:INFO -- UNHANDLED EXCEPTION, CPU #3, PID 45, NAME /usr/bin/ion -``` - -Pattern : déréférence d'un pointer null avec accès au champ à l'offset 0x70 (`RAX=0x70`, `addr=0x70`). - -## Repro minimal - -```sh -# Dans une session ion fraîche, avec le compositor Wayland -# (https://gitlab.com/leyoda/redox-wayland-compositor) : -redox-wl-compositor & # backround job [0] -sleep 1 -redox-wl-real-client-simple-window & # background job [1] -sleep 2 -# Maintenant, tuer le compositor SANS quitter le client proprement. -# Le compositor ferme son socket, le client reçoit Broken pipe et exit -# avec status non-zéro. C'est en traitant cette terminaison que ion crashe. -pkill redox-wl-compositor -``` - -Reproductible aussi avec n'importe quelle paire client-serveur Unix : -- serveur en bg qui ferme son socket -- client en bg qui écrit dessus et reçoit EPIPE - -## Hypothèse de localisation (best-effort sans symboles) - -Le crash semble survenir dans `BackgroundEventCallback`. La closure dans -`src/main.rs:224-234` : - -```rust -shell.set_background_event(Some(Arc::new(|njob, pid, kind| match kind { - BackgroundEvent::Added => eprintln!("ion: bg [{}] {}", njob, pid), - BackgroundEvent::Stopped => eprintln!("ion: ([{}] {}) Stopped", njob, pid), - BackgroundEvent::Resumed => eprintln!("ion: ([{}] {}) Running", njob, pid), - BackgroundEvent::Exited(status) => { - eprintln!("ion: ([{}] {}) exited with {}", njob, pid, status) - } - BackgroundEvent::Errored(error) => { - eprintln!("ion: ([{}] {}) errored: {}", njob, pid, error) - } -}))); -``` - -Appelée depuis `src/lib/shell/pipe_exec/job_control.rs:148-228` (`watch_background`), elle-même runs depuis un thread dédié au polling de waitpid. - -Note : sur la sortie observée, **le message "exited with" est imprimé AVANT le page fault** : - -``` -[real-client] FAIL: Backend error: Io error: Broken pipe (os error 32) -root:~# [src/procmgr.rs:299 WARN] Cancellation for unknown id Id(26) -root:~# -ion: ([Page fault: 0000000000000070 ... -``` - -Donc le callback aurait fini son `eprintln!`, le crash arrive juste après — possiblement dans le code qui suit, peut-être le `get_process!(|process| { process.forget(); ... })` ou dans le drop du job, ou dans le retour de thread vers le main thread. - -## Disassembly stripped autour de RIP=0x2335ae - -``` -2335a2: 48 8b 85 60 fe ff ff mov -0x1a0(%rbp), %rax -2335a9: 48 d1 e0 shl $1, %rax -2335ac: 48 85 c0 test %rax, %rax -2335af: 74 0d je 0x2335be -``` - -Pattern Vec/Box drop : "test capacity*2, jump-if-zero, else free pointer". -**Le `0x2335ae` tombe au milieu de l'instruction `test`** — possible que -le strip + offset stable ne pointe pas exactement à la même instruction -sur l'image installée vs le rebuild local. RAX=0x70 au crash suggère un -load `mov [rdi+0x70], rax` (ou similaire) où rdi est null. Le champ à -l'offset 0x70 d'une struct Rust commune (BackgroundProcess ? File ? -Mutex> ?) — à confirmer avec un build debug. - -## Side-bug observé en parallèle - -Sur la même session, juste avant le crash : -``` -[src/procmgr.rs:299 WARN] Cancellation for unknown id Id(26) -``` - -Ce warning vient de relibc (`procmgr`), pas d'ion. Peut être lié — une -cancellation de syscall qui laisse un state inconsistent, qu'ion -manipule ensuite. À investiguer côté relibc aussi. - -## Demande - -1. Identifier la fonction à `0x2335ae` (rebuild non-stripped + addr2line). -2. Confirmer si c'est un null deref dans `process.forget()`, drop de - `BackgroundProcess`, ou la déstructure du `Vec`. -3. Patch. - -## Workaround utilisateur en attendant - -Toujours quitter les clients background **avant** le serveur : - -```sh -# Bon ordre : -redox-wl-real-client-simple-window & -# (interagir) -fg %1 # ramener au foreground -# ESC ou autre pour quitter proprement -pkill redox-wl-compositor # seulement après que tous les clients sont sortis -``` - -## Référence - -Bug détecté pendant le port d'un compositor Wayland minimal sur Redox : -https://gitlab.com/leyoda/redox-wayland-compositor (phase 13.1.b / 13.1.c). diff --git a/docs/phase13-2-output-subcompositor.md b/docs/phase13-2-output-subcompositor.md deleted file mode 100644 index 379d26f..0000000 --- a/docs/phase13-2-output-subcompositor.md +++ /dev/null @@ -1,262 +0,0 @@ -# Phase 13.2 — `wl_output` v3 + `wl_subcompositor`/`wl_subsurface` - -> Document produit le 2026-05-16, suite de -> [`phase13-1-c-cursor.md`](phase13-1-c-cursor.md). -> -> **Scope** : combler 2 dettes protocolaires bloquantes pour les -> toolkits clients tiers modernes : -> -> - **`wl_output` complet** — annonce le single display avec geometry/ -> mode/scale/done, en respectant le gating par version (v1/v2/v3). -> Plusieurs toolkits clients (sctk, GTK, Qt) considèrent ce global -> implicitement requis — son absence en 13.1.b était une bombe à -> retardement. -> - **`wl_subcompositor` + `wl_subsurface`** — supporte les surfaces -> parent-enfant et les rend visuellement à `parent_pos + offset`. -> Pré-requis pour tout client multi-surface (popups, badges, splash -> screens, decorations CSD, etc.). -> -> **Stratégie de validation hybride** : pour chaque sprint, un test -> natif (cargo test, sans QEMU) valide la couche protocolaire ; un -> test runtime visuel valide le rendu effectif. Le coût de QEMU -> est ainsi réduit aux validations vraiment visuelles. -> -> **Verdict** : ✅ **13.2 entièrement validée** — 6 commits, 5 -> sprints, 2 nouveaux clients de test, 3 scripts ion shortcuts. - -## Sous-phases livrées - -### 13.2.a — `wl_output` v3 complet ([commit `7413745`](../crates/redox-wl-wayland-frontend/src/lib.rs)) - -Avant : `wl_output` n'était PAS déclaré comme global. Les clients qui -le bind-eraient implicitement (sctk, GTK) se retrouvaient sans output. - -Après : - -- Global `wl_output` à `OUTPUT_VERSION = 3` -- `GlobalDispatch::bind` envoie au bind, dans l'ordre : - 1. `geometry(0, 0, 0_mm, 0_mm, Subpixel::Unknown, "Redox", "redox-wl-output:0", Transform::Normal)` - 2. `mode(CURRENT | PREFERRED, screen_w, screen_h, 60_000_mHz)` - 3. `scale(1)` — **uniquement si version bound ≥ 3** (gating conservateur) - 4. `done()` — **uniquement si version bound ≥ 2** -- `Dispatch::request` accepte `Release` (v3+) en no-op - -Taille pixel issue de `screen_w / screen_h` qui sont settés par -`set_screen_size()` (introduit en 13.1.b pour clamp curseur). Refresh -hardcodé à 60 Hz : notre boucle main tourne à ~30 fps mais les apps -modernes ignorent largement ce champ. - -**Test natif** (`crates/redox-wl-test-wl-output/tests/gating_native.rs`) : -serveur + client en-process via `UnixStream::pair()`, 11 assertions -vérifient v1 (geometry+mode), v2 (+done), v3 (+scale). PASS le 2026-05-16. - -**Test runtime** (`redox-wl-test-wl-output`, runnable via `test-out`) : -bind aux 3 versions et reporte PASS/FAIL côté serial. Confirme le -gating sur la vraie cible Redox. - -### 13.2.b.1 — `wl_subcompositor` protocole ([commit `f9c3de1`](../crates/redox-wl-wayland-frontend/src/lib.rs)) - -Implémentation protocole uniquement (sans rendering, repoussé à .b.2) : - -- Global `wl_subcompositor` à `SUBCOMPOSITOR_VERSION = 1` -- `GlobalDispatch::bind` no-op (resource init) -- `Dispatch::request` : - - `Destroy` : no-op - - `GetSubsurface { id, surface, parent }` : crée un `Arc` - avec parent ref, child ref, position pending `(0,0)`, sync mode `true` -- `Dispatch::request` : - - `SetPosition { x, y }` : update SubsurfaceData.position - - `PlaceAbove`, `PlaceBelow` : no-op (single-subsurface use-case) - - `SetSync`, `SetDesync` : update SubsurfaceData.sync - - `Destroy` : no-op (resource cleanup par wayland-server) - -**SubsurfaceData** est une nouvelle struct portée par la wl_subsurface -resource via `data_init.init(id, Arc)`. - -**Test natif** (`subcompositor_native.rs`) : cycle complet bind → -get_subsurface → set_position → set_sync/desync → destroy. Aucune -erreur protocole détectée. - -### 13.2.b.2 — Rendering subsurface ([commit `bba2d7b`](../crates/redox-wl-wayland-frontend/src/lib.rs)) - -Wire la SubsurfaceData de .b.1 au pipeline de composition : - -1. Nouveau champ `subsurface_link: Mutex>>` - sur `SurfaceData` -2. À `GetSubsurface`, on link la SubsurfaceData à la child SurfaceData -3. Au commit du child wl_surface : - - Si `subsurface_link` est Some : récupère le parent SurfaceId via - `sub_data.parent.data::>()` - - Lookup la position actuelle du parent dans le registry - - `modify_pending(child_id, |s| s.x = px + ox; s.y = py + oy)` avant - le `commit(id)` qui applique pending → current - - Skip `raise()` et `set_focus()` pour les subsurfaces (elles ne - grabbent pas le focus indépendamment, et leur z-order est géré - comme cascade au-dessus du parent) - -`compose_into` (côté `redox-wl-compositor-core`) n'est PAS modifié — il -itère le z_order et dessine chaque surface à son `(x, y)` absolu. Notre -prep au commit assure que le child est à la bonne position. - -### 13.2.b.3 — Client de test visuel ([commit `1dab6ff`](../crates/redox-wl-test-client-subcompositor/)) - -Nouveau crate `redox-wl-test-client-subcompositor` qui : - -1. Connecte au socket `/tmp/redox-wl-comp.sock` -2. Bind 5 globals : `wl_compositor`, `wl_shm`, `xdg_wm_base`, `wl_seat`, - `wl_subcompositor` -3. Crée un parent xdg_toplevel 300×200 bleu nuit avec bordure noire 2px, - ack_configure puis attach + commit buffer ARGB -4. Crée une child wl_surface, `get_subsurface(child, parent)`, - `set_position(50, 50)`, `set_desync`, attach + commit buffer 60×60 - rouge avec bordure noire -5. ESC fermes le client proprement (le path keyboard étant routé au - parent toplevel qui a le focus auto-grant) - -Reprend l'event-loop tolérant Interrupted/BrokenPipe de la phase 13.1.b. - -### Fix bug visuel ([commit `dfb5c66`](../crates/redox-wl-wayland-frontend/src/lib.rs)) - -Détecté lors du runtime QEMU 13.2.b.3. Symptôme : le carré rouge est -calculé à la bonne position au commit du child, mais reste invisible. - -Cause : `parent_surface.commit()` final dans le client (refresh post- -subsurface) appelle `raise(parent_id)` dans le compositor, plaçant le -parent **au-dessus** du child dans le z_order. Le parent overdraw le -child à compose_into. - -Z-order observé : `[child, parent]` au lieu de `[parent, child]`. - -**Fix** : après `raise(parent_id)`, scanner `surfaces_by_id` pour -trouver toutes les subsurfaces dont `subsurface_link.parent` pointe -vers cette SurfaceId, et les `raise()` juste après. Le z-order devient -`[parent, child]` (avec child raised en dernier = au-dessus). - -Coût O(N) par raise — N est typiquement petit (1-10 toplevels). À -durcir avec un index parent_id → Vec si N explose. - -### 13.2.b.4 — Scripts ion shortcuts ([commit `6fe3214`](../tools/redox-scripts/)) - -UX : pour éviter de retaper 3 lignes à chaque test runtime dans la -fenêtre QEMU graphique (où le copier-coller depuis CachyOS n'est pas -disponible), 3 scripts ion prêts à l'emploi déployés dans `/usr/bin` : - -- **`test-sw`** : compositor + simple_window upstream (phase 13.1.b) -- **`test-subc`** : compositor + subcompositor visuel (phase 13.2.b.3) -- **`test-out`** : compositor + wl_output gating test (phase 13.2.a) - -Tous suivent le même pattern : - -```ion -#!/usr/bin/ion -rm -f /tmp/redox-wl-comp.sock -redox-wl-compositor & -sleep 1 - -``` - -run-qemu.sh copie maintenant `tools/redox-scripts/*` dans `/usr/bin` de -l'image (avec `cp -p` pour préserver le bit exécutable). - -## Stratégie de tests : hybride natif + runtime - -Innovation méthodologique de cette phase. Pour chaque sprint : - -- **Test natif** (cargo test, sur l'hôte CachyOS, sans QEMU) : - - Spawn un thread serveur avec wayland-server + la logique de gating - identique au prod code - - Connecte un client wayland-client via `UnixStream::pair()` - - Exercise toute la séquence Wayland - - Asserts précis (versions, events reçus, ordre, etc.) - - Tourne en < 6 secondes -- **Test runtime** (QEMU, visuel) : - - Confirme le rendu effectif sur le framebuffer Redox - - Confirme l'interaction utilisateur (clic, ESC, focus, etc.) - - Validation finale "tout le pipeline marche" - -Gain : on n'a QEMU dans la boucle que pour ce qui est vraiment visuel. -La couche protocolaire est testée en CI-friendly time. - -## Limitations connues - -À traiter en phase ultérieure si besoin : - -- **Parent move ≠ propagation aux enfants** : si on drag un parent - toplevel (phase 7.7), ses subsurfaces ne suivent pas tant que le - client ne re-commit pas l'enfant. Pour des use-cases statiques - c'est OK ; pour Wayland conforme strict il faudra walker les enfants - au move du parent. -- **wl_subsurface.Destroy ne unmap pas le child** : spec violation - mineure. Si le client veut vraiment retirer la subsurface, il doit - aussi destroy son wl_surface enfant. -- **Pas de cascade sync** : un commit du parent en mode sync devrait - flusher les pending states des subsurfaces sync. Pas implémenté. - Notre client de test utilise `set_desync` donc le path n'est pas - exercé. -- **Pas de role-tracking** : spec exige `bad_surface` error si une - wl_surface a déjà un rôle (xdg_toplevel) et qu'on appelle - GetSubsurface dessus. Pour l'instant on log debug seulement. -- **PlaceAbove/PlaceBelow no-op** : z-order relatif entre subsurfaces - siblings non géré. Pas observé en pratique avec nos clients de test - single-subsurface. -- **Raise children scan O(N)** : naïf. Avec un index parent_id → - Vec on passe O(1). À faire si N de surfaces visibles - devient grand (> 50 ?). - -## Fichiers livrés en 13.2 - -``` -# Code compositor (frontend Wayland) -crates/redox-wl-wayland-frontend/src/lib.rs # globals + Dispatch + commit hook - -# Clients de test -crates/redox-wl-test-wl-output/ # validation gating wl_output (13.2.a) - ├── src/main.rs # runtime client - └── tests/ - ├── gating_native.rs # test natif v1/v2/v3 - └── subcompositor_native.rs # test natif protocole - -crates/redox-wl-test-client-subcompositor/ # validation visuelle (13.2.b.3) - └── src/main.rs - -# Tooling runtime -tools/redox-scripts/test-sw # shortcut simple_window -tools/redox-scripts/test-subc # shortcut subcompositor -tools/redox-scripts/test-out # shortcut output gating - -# Build/deploy -run-qemu.sh # copie binaires + scripts - -# Doc -docs/phase13-2-output-subcompositor.md # ce document -``` - -## Critère de fin 13.2 - -> 1. Le compositor expose `wl_output` v3 avec gating correct des events -> par version bound (testé v1/v2/v3 en natif et runtime) -> 2. Le compositor expose `wl_subcompositor` v1 et accepte tous les -> requests `wl_subsurface` sans erreur protocole -> 3. Un client multi-surface (parent + 1 subsurface) rend visuellement -> les deux surfaces aux bonnes positions (parent à `cascade_offset`, -> child à `parent + offset`) -> 4. Le z-order reste cohérent même après recommit du parent (le child -> reste au-dessus) -> 5. Le focus clavier reste sur le parent (les subsurfaces ne grabbent -> pas) - -**✅ Tous critères validés 2026-05-16.** - -## Étapes suivantes possibles - -- **13.3** : extraction backend pour Smithay/COSMIC (Phase 13 du master - plan). Auditer notre code pour identifier les primitives réutilisables - comme backend Smithay (display, input, session, renderer logiciel). -- **13.2.c** : retenter sctk simple_window — avec wl_output et - wl_subcompositor maintenant présents, il manque encore xdg-decoration - et un wayland-server qui charge libxkbcommon. À évaluer si le ROI - vaut le coût d'implémentation. -- **Durcissement 13.2** : reprendre les limitations listées ci-dessus - (cascade sync, parent move propagation, role-tracking). -- **Phase 8** : remplacement expérimental d'Orbital avec ce compositor. - Préparé maintenant qu'on couvre les protocoles de base. diff --git a/run-qemu.sh b/run-qemu.sh index 458a1f6..d971aa5 100755 --- a/run-qemu.sh +++ b/run-qemu.sh @@ -13,7 +13,6 @@ # cd crates/redox-wl-real-client-simple-window && redoxer build --release # - redoxfs accessible dans REDOX_SRC/build/fstools/bin/ # - L'image harddrive.img présente dans REDOX_SRC/build/x86_64/desktop/ -# - /dev/fuse disponible sur l'hôte # - PAS de QEMU déjà ouvert sur la même image (corromprait le disque). # # Une fois Redox bootée : @@ -26,17 +25,12 @@ set -euo pipefail ROOT="$(cd "$(dirname "$0")" && pwd)" REDOX_SRC="${REDOX_SRC:-$HOME/Projets/Redox/redox-src}" -IMAGE="${IMAGE:-$REDOX_SRC/build/x86_64/desktop/harddrive.img}" -REDOXFS="${REDOXFS:-$REDOX_SRC/build/fstools/bin/redoxfs}" +IMAGE="$REDOX_SRC/build/x86_64/desktop/harddrive.img" +REDOXFS="$REDOX_SRC/build/fstools/bin/redoxfs" MOUNT="${MOUNT:-/tmp/redox-mnt}" COMPOSITOR_BIN="$ROOT/crates/redox-wl-compositor/target/x86_64-unknown-redox/release/redox-wl-compositor" CLIENT_BIN="$ROOT/crates/redox-wl-real-client-simple-window/target/x86_64-unknown-redox/release/redox-wl-real-client-simple-window" -# Phase 13.2.a : client de test version-gating wl_output (optionnel, skip -# silencieusement si pas compilé). -TEST_OUTPUT_BIN="$ROOT/crates/redox-wl-test-wl-output/target/x86_64-unknown-redox/release/redox-wl-test-wl-output" -# Phase 13.2.b.3 : client de test visuel subcompositor. -TEST_SUBCOMP_BIN="$ROOT/crates/redox-wl-test-client-subcompositor/target/x86_64-unknown-redox/release/redox-wl-test-client-subcompositor" NO_QEMU=0 for arg in "$@"; do @@ -82,31 +76,6 @@ if [[ $missing -ne 0 ]]; then exit 1 fi -if [[ ! -e /dev/fuse ]]; then - echo "ERR : /dev/fuse absent sur l'hôte" >&2 - echo "RedoxFS en a besoin pour monter l'image avant le boot QEMU." >&2 - echo "Sur Linux, charge le module fuse ou exécute sur un host où FUSE est disponible." >&2 - exit 3 -fi - -# Phase 13.1.c B.4 : warn si l'image est trop petite. L'image desktop par -# défaut fait ~680 Mo et sature dès qu'on tente `pkg update` (cf. session -# de la phase 13.1.b). Pour confort, recommandation : 4 Go minimum, idéal -# 10 Go. La grossir avec : -# qemu-img resize -f raw 10G -# /build/fstools/bin/redoxfs-resize -IMAGE_SIZE_BYTES=$(stat -c %s "$IMAGE" 2>/dev/null || echo 0) -MIN_IMAGE_SIZE=$((4 * 1024 * 1024 * 1024)) # 4 GiB -if [[ "$IMAGE_SIZE_BYTES" -gt 0 && "$IMAGE_SIZE_BYTES" -lt "$MIN_IMAGE_SIZE" ]]; then - image_size_human=$(numfmt --to=iec --suffix=B "$IMAGE_SIZE_BYTES" 2>/dev/null || echo "${IMAGE_SIZE_BYTES} octets") - echo "WARN : image $IMAGE de taille ${image_size_human} (< 4 GiB recommandés)" >&2 - echo " Tu risques 'No space left on device' au moindre pkg update." >&2 - echo " Pour la grossir :" >&2 - echo " qemu-img resize -f raw $IMAGE 10G" >&2 - echo " $REDOX_SRC/build/fstools/bin/redoxfs-resize $IMAGE" >&2 - echo " (warning seulement, on continue)" >&2 -fi - # --- 2. Mount --- mkdir -p "$MOUNT" echo "==> monter $IMAGE -> $MOUNT" @@ -131,23 +100,6 @@ echo " monté (pid redoxfs=$REDOXFS_PID)" echo "==> copier les binaires dans /usr/bin/" cp -v "$COMPOSITOR_BIN" "$MOUNT/usr/bin/" cp -v "$CLIENT_BIN" "$MOUNT/usr/bin/" -# Phase 13.2.a : test client wl_output (optionnel) -if [[ -e "$TEST_OUTPUT_BIN" ]]; then - cp -v "$TEST_OUTPUT_BIN" "$MOUNT/usr/bin/" -fi -# Phase 13.2.b.3 : test client subcompositor (optionnel) -if [[ -e "$TEST_SUBCOMP_BIN" ]]; then - cp -v "$TEST_SUBCOMP_BIN" "$MOUNT/usr/bin/" -fi - -# Phase 13.2.b.4 : scripts ion raccourcis (test-subc, test-sw, test-out). -# Copiés depuis tools/redox-scripts/ pour invocation 1-ligne dans le shell -# Redox. Chmod +x conservé via cp -p. -for script in "$ROOT/tools/redox-scripts"/*; do - if [[ -f "$script" ]]; then - cp -vp "$script" "$MOUNT/usr/bin/" - fi -done # --- 4. umount avant make qemu (sinon QEMU et FUSE se battent sur le même fichier) --- echo "==> démonter $MOUNT" diff --git a/tools/redox-scripts/test-out b/tools/redox-scripts/test-out deleted file mode 100755 index b9c6f56..0000000 --- a/tools/redox-scripts/test-out +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/ion -# Phase 13.2.a — Launch compositor + wl_output version gating test client. -# Le client teste binding wl_output aux v1/v2/v3 et reporte PASS/FAIL. -# Pas besoin de Ctrl+Q : le client exit puis on stoppe le compositor. -rm -f /tmp/redox-wl-comp.sock -redox-wl-compositor & -sleep 1 -redox-wl-test-wl-output diff --git a/tools/redox-scripts/test-subc b/tools/redox-scripts/test-subc deleted file mode 100755 index 74d2052..0000000 --- a/tools/redox-scripts/test-subc +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/ion -# Phase 13.2.b.3 — Launch compositor + subcompositor test client. -# Nettoie un éventuel socket résiduel, lance le compo en background, -# attend 1s, puis lance le client de test subcompositor en avant-plan. -# Côté QEMU graphique : ESC sur la fenêtre pour exit propre du client, -# puis Ctrl+Q pour exit du compositor. -rm -f /tmp/redox-wl-comp.sock -redox-wl-compositor & -sleep 1 -redox-wl-test-client-subcompositor diff --git a/tools/redox-scripts/test-sw b/tools/redox-scripts/test-sw deleted file mode 100755 index 4ec83bc..0000000 --- a/tools/redox-scripts/test-sw +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/ion -# Phase 13.1.b — Launch compositor + simple_window test client (port wayland-rs). -# Côté QEMU graphique : ESC sur la fenêtre pour exit propre du client, -# puis Ctrl+Q pour exit du compositor. -rm -f /tmp/redox-wl-comp.sock -redox-wl-compositor & -sleep 1 -redox-wl-real-client-simple-window