From 4bff319c7f580b8b2f2eb49e1e89a7522e3d5f92 Mon Sep 17 00:00:00 2001 From: Votre Nom Date: Sat, 9 May 2026 14:34:45 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=89=20Phase=207.1=20=E2=80=94=20xdg-sh?= =?UTF-8?q?ell=20minimal=20valid=C3=A9=20runtime?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Capture preuve : docs/phase7-1-xdg-toplevel.png — fenêtre 320x240 du client externe positionnée à (60, 60) par le compositor (cascade par défaut), avec bandes verticales arc-en-ciel + bordure noire 2px. Différence visuelle vs phase 6.4 : la fenêtre est maintenant positionnée par le compositor (pas plaquée à 0,0). Modifications redox-wl-wayland-frontend : - + dep wayland-protocols (feature server) - xdg_wm_base global v5 - XdgSurfaceData { wl_surface, last_serial, acked_serial } - XdgToplevelData { title, app_id } - SurfaceData.xdg_pending_initial_configure : bloque l'affichage tant que ack-configure pas reçu (sémantique xdg-shell standard) - WaylandFrontend.next_xdg_serial : counter monotone serial - WaylandFrontend.next_toplevel_index : cascade position +60 par fenêtre - DEFAULT_TOPLEVEL_SIZE = 640x480 envoyé en initial configure - Dispatch xdg_wm_base : GetXdgSurface, CreatePositioner (no-op), Pong (no-op), Destroy (no-op) - Dispatch xdg_positioner + xdg_popup : no-op (out of scope) - Dispatch xdg_surface : - GetToplevel → cascade pos + envoie xdg_toplevel.configure(640,480) + xdg_surface.configure(serial) - AckConfigure(serial) → enregistre + débloque affichage - Destroy → cache la surface - Dispatch xdg_toplevel : - SetTitle / SetAppId → stocke - tout le reste ignoré (move, resize, maximized, ...) redox-wl-test-client-shm adapté : - + dep wayland-protocols (feature client) - bind 3 globals : wl_compositor, wl_shm, xdg_wm_base - Dispatch XdgWmBase::Event::Ping → pong (au cas où) - Dispatch XdgSurface::Event::Configure → store serial - Dispatch XdgToplevel::Event::Configure / Close → log - Workflow runtime : 1. create_surface + get_xdg_surface + get_toplevel 2. set_title / set_app_id 3. surface.commit (initial empty) 4. attente Configure event (timeout 5s) 5. ack_configure(serial) 6. shm + buffer + attach + commit POST-ack 7. boucle 25s 8. destroy propre Init submodule wayland-rs/wayland-protocols/protocols (XML files gitlab.freedesktop.org/wayland/wayland-protocols). Validation runtime QEMU complète : [client] globals : compositor=true shm=true xdg_wm_base=true [client] xdg_toplevel configure suggéré : 640x480 [client] initial configure reçu, serial=1 [client] ack_configure(1) [client] buffer attach + damage + commit POST-ack [comp] tick=30..450 surfaces=1 elapsed=1.2..18.2s PNG capturée à T+14s : surface visible à (60,60), bandes RGB+jaune+ violet+orange comme attendu. Critère de fin 7.1 validé : client xdg-shell crée toplevel, reçoit configure, ack, commit, affiche via shm, sans panic serveur, 18s stable, destroy propre. Limitations connues : focus, cursor, move/resize, multi-client, robustesse — sous-tickets 7.2-7.7. Image Redox restaurée à boot Orbital normal. docs/phase7-1-xdg-shell.md : compte-rendu complet, cycle de vie, limitations, plan 7.2. Leyoda 2026 – GPLv3 --- crates/redox-wl-test-client-shm/Cargo.toml | 1 + crates/redox-wl-test-client-shm/src/main.rs | 175 ++++++++++--- crates/redox-wl-wayland-frontend/Cargo.toml | 1 + crates/redox-wl-wayland-frontend/src/lib.rs | 257 +++++++++++++++++++- docs/phase7-1-xdg-shell.md | 202 +++++++++++++++ docs/phase7-1-xdg-toplevel.png | Bin 0 -> 1339 bytes 6 files changed, 600 insertions(+), 36 deletions(-) create mode 100644 docs/phase7-1-xdg-shell.md create mode 100644 docs/phase7-1-xdg-toplevel.png diff --git a/crates/redox-wl-test-client-shm/Cargo.toml b/crates/redox-wl-test-client-shm/Cargo.toml index 3307876..efd3561 100644 --- a/crates/redox-wl-test-client-shm/Cargo.toml +++ b/crates/redox-wl-test-client-shm/Cargo.toml @@ -6,4 +6,5 @@ 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-shm/src/main.rs b/crates/redox-wl-test-client-shm/src/main.rs index 214356e..6fa361a 100644 --- a/crates/redox-wl-test-client-shm/src/main.rs +++ b/crates/redox-wl-test-client-shm/src/main.rs @@ -1,13 +1,11 @@ -//! Phase 6.4 — Client Wayland test. +//! Phase 7.1 — Client Wayland xdg-shell. //! -//! Se connecte au socket Wayland exposé par le compositor à -//! `/tmp/redox-wl-comp.sock`, crée une surface 320x240, peint un -//! pattern ARGB déterministe (dégradé orangé), commit. Reste connecté -//! 30s pour qu'on puisse capturer l'écran. +//! Se connecte à `/tmp/redox-wl-comp.sock`, bind les globals +//! wl_compositor + wl_shm + xdg_wm_base, crée une fenêtre xdg_toplevel, +//! attend l'initial configure, ack, peint un buffer ARGB et commit. //! -//! C'est le test critique de phase 6.4 : si le compositor affiche -//! ce qui suit dans la frame QEMU, on a un VRAI compositor Wayland -//! qui rend des pixels venant d'un client externe. +//! Vs phase 6.4 : on passe par xdg-shell (placement managé par le +//! compositor) au lieu de wl_surface seul. use std::ffi::CString; use std::fs::OpenOptions; @@ -28,6 +26,11 @@ use wayland_client::{ wl_shm_pool::WlShmPool, wl_surface::WlSurface, }, }; +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"; const W: i32 = 320; @@ -60,6 +63,10 @@ fn dlog(s: &str) { struct ClientState { compositor: Option, shm: Option, + wm_base: Option, + /// Le serial du dernier configure reçu, à ack. + pending_serial: Option, + configured: bool, } impl Dispatch for ClientState { @@ -79,6 +86,9 @@ impl Dispatch for ClientState { "wl_shm" => { state.shm = Some(registry.bind(name, version.min(1), qh, ())); } + "xdg_wm_base" => { + state.wm_base = Some(registry.bind(name, version.min(5), qh, ())); + } _ => {} } } @@ -106,6 +116,63 @@ noop!(WlShmPool); noop!(WlBuffer); noop!(WlSurface); +// xdg_wm_base : doit répondre aux pings (mais on n'envoie pas) ; on ignore +impl Dispatch for ClientState { + fn event( + _state: &mut Self, + wm_base: &XdgWmBase, + event: xdg_wm_base::Event, + _: &(), + _conn: &Connection, + _qh: &QueueHandle, + ) { + if let xdg_wm_base::Event::Ping { serial } = event { + wm_base.pong(serial); + } + } +} + +// xdg_surface : reçoit les configure, on stocke le serial pour ack +impl Dispatch for ClientState { + fn event( + state: &mut Self, + _xdg_surf: &XdgSurface, + event: xdg_surface::Event, + _: &(), + _conn: &Connection, + _qh: &QueueHandle, + ) { + if let xdg_surface::Event::Configure { serial } = event { + state.pending_serial = Some(serial); + state.configured = true; + } + } +} + +// xdg_toplevel : configure (size + states) + close +impl Dispatch for ClientState { + fn event( + _state: &mut Self, + _r: &XdgToplevel, + event: xdg_toplevel::Event, + _: &(), + _conn: &Connection, + _qh: &QueueHandle, + ) { + match event { + xdg_toplevel::Event::Configure { width, height, .. } => { + dlog(&format!( + "[client] xdg_toplevel configure suggéré : {width}x{height}" + )); + } + xdg_toplevel::Event::Close => { + dlog("[client] xdg_toplevel close request"); + } + _ => {} + } + } +} + unsafe fn create_shm_with_pattern(name: &str) -> Result { let cname = CString::new(name).unwrap(); let _ = libc::shm_unlink(cname.as_ptr()); @@ -133,14 +200,23 @@ unsafe fn create_shm_with_pattern(name: &str) -> Result { return Err("mmap failed".into()); } - // Pattern ARGB : dégradé orangé + bandes diagonales pour reconnaître + // Pattern ARGB : bandes verticales colorées + numéro de fenêtre simulé let pixels = std::slice::from_raw_parts_mut(p as *mut u32, (W * H) as usize); for y in 0..H { for x in 0..W { - let r: u32 = (200 + (x * 55 / W)) as u32 & 0xFF; - let g: u32 = (80 + (y * 100 / H)) as u32 & 0xFF; - let b: u32 = (((x + y) & 0xFF) as u32).saturating_sub(100); - pixels[(y * W + x) as usize] = (0xFF << 24) | (r << 16) | (g << 8) | b; + let band = (x / 32) % 6; + let color: u32 = match band { + 0 => 0xFF_F2_55_44, + 1 => 0xFF_F0_8E_3A, + 2 => 0xFF_E8_C5_3A, + 3 => 0xFF_5A_C0_50, + 4 => 0xFF_45_8B_E0, + 5 => 0xFF_8C_60_D0, + _ => 0xFF_FF_FF_FF, + }; + // Bordure noire 2px + 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); @@ -150,7 +226,7 @@ unsafe fn create_shm_with_pattern(name: &str) -> Result { fn run() -> Result<(), Box> { dlog("[client] connect to compositor"); - // Attendre que le socket existe (compositor démarré) + // Attente du socket for i in 0..50 { if std::path::Path::new(SOCKET_PATH).exists() { break; @@ -171,36 +247,64 @@ fn run() -> Result<(), Box> { let mut state = ClientState::default(); event_queue.roundtrip(&mut state)?; dlog(&format!( - "[client] globals : compositor={} shm={}", + "[client] globals : compositor={} shm={} xdg_wm_base={}", state.compositor.is_some(), - state.shm.is_some() + state.shm.is_some(), + state.wm_base.is_some() )); - let compositor = state - .compositor - .clone() - .ok_or("no wl_compositor global")?; + let compositor = state.compositor.clone().ok_or("no wl_compositor global")?; let shm = state.shm.clone().ok_or("no wl_shm global")?; + let wm_base = state.wm_base.clone().ok_or("no xdg_wm_base global")?; - // Crée shm + pattern - let fd = unsafe { create_shm_with_pattern("/redox-wl-client-shm") }?; - dlog(&format!("[client] shm créé, peint {}x{} ARGB", W, H)); - - // wl_shm_pool + wl_buffer - let pool = shm.create_pool(fd.as_fd(), SIZE, &qh, ()); - let buffer = pool.create_buffer(0, W, H, STRIDE, wayland_client::protocol::wl_shm::Format::Argb8888, &qh, ()); - - // wl_surface + // 1. Création wl_surface + xdg_surface + xdg_toplevel let surface = compositor.create_surface(&qh, ()); + let xdg_surface = wm_base.get_xdg_surface(&surface, &qh, ()); + let toplevel = xdg_surface.get_toplevel(&qh, ()); + toplevel.set_title("Phase 7.1 client".to_string()); + toplevel.set_app_id("redox.wl.test.client.shm".to_string()); + surface.commit(); // commit initial pour signaler "prêt à recevoir configure" + dlog("[client] xdg_toplevel créé, attente initial configure"); + + // 2. Attendre l'initial configure + 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)); + } + if !state.configured { + return Err("no initial configure received".into()); + } + let serial = state.pending_serial.unwrap_or(0); + dlog(&format!("[client] initial configure reçu, serial={serial}")); + + // 3. Ack le configure + xdg_surface.ack_configure(serial); + state.pending_serial = None; + dlog(&format!("[client] ack_configure({serial})")); + + // 4. Préparer + attacher le buffer + let fd = unsafe { create_shm_with_pattern("/redox-wl-client-shm-71") }?; + let pool = shm.create_pool(fd.as_fd(), SIZE, &qh, ()); + let buffer = pool.create_buffer( + 0, + W, + H, + STRIDE, + wayland_client::protocol::wl_shm::Format::Argb8888, + &qh, + (), + ); + surface.attach(Some(&buffer), 0, 0); surface.damage_buffer(0, 0, W, H); surface.commit(); - dlog("[client] surface attach + damage + commit envoyés"); + dlog("[client] buffer attach + damage + commit POST-ack"); event_queue.flush()?; let _ = event_queue.roundtrip(&mut state); - // Reste connecté ~25s pour qu'on puisse capturer + // 5. Boucle vivante 25 secondes let start = std::time::Instant::now(); while start.elapsed() < Duration::from_secs(25) { let _ = event_queue.dispatch_pending(&mut state); @@ -208,10 +312,11 @@ fn run() -> Result<(), Box> { thread::sleep(Duration::from_millis(50)); } - dlog("[client] done, exit propre"); - let _ = surface; // tag explicite pour rappeler que la surface vit jusqu'à la fin - let _ = buffer; - let _ = pool; + dlog("[client] done, destroy propre"); + toplevel.destroy(); + xdg_surface.destroy(); + surface.destroy(); + let _ = event_queue.flush(); Ok(()) } diff --git a/crates/redox-wl-wayland-frontend/Cargo.toml b/crates/redox-wl-wayland-frontend/Cargo.toml index 93970d7..e83bd89 100644 --- a/crates/redox-wl-wayland-frontend/Cargo.toml +++ b/crates/redox-wl-wayland-frontend/Cargo.toml @@ -8,4 +8,5 @@ description = "Wayland protocol frontend that maps wl_compositor/wl_shm/wl_surfa redox-wl-compositor-core = { path = "../redox-wl-compositor-core" } wayland-server = { path = "../../../wayland-rs/wayland-server", default-features = false } wayland-backend = { path = "../../../wayland-rs/wayland-backend", default-features = false } +wayland-protocols = { path = "../../../wayland-rs/wayland-protocols", default-features = false, features = ["server"] } libc = "0.2" diff --git a/crates/redox-wl-wayland-frontend/src/lib.rs b/crates/redox-wl-wayland-frontend/src/lib.rs index c9b545c..8e5fa6c 100644 --- a/crates/redox-wl-wayland-frontend/src/lib.rs +++ b/crates/redox-wl-wayland-frontend/src/lib.rs @@ -26,14 +26,27 @@ use std::path::Path; use std::sync::{Arc, Mutex}; use redox_wl_compositor_core::{SurfaceBuffer, SurfaceId, SurfaceRegistry}; +use wayland_protocols::xdg::shell::server::{ + xdg_popup, xdg_positioner, xdg_surface, xdg_toplevel, xdg_wm_base, +}; use wayland_server::{ Client, DataInit, Display as WlDisplay, DisplayHandle, GlobalDispatch, Resource, backend::{ClientData, ClientId, DisconnectReason}, - protocol::{wl_buffer, wl_compositor, wl_region, wl_shm, wl_shm_pool, wl_surface, wl_callback}, + protocol::{wl_buffer, wl_callback, wl_compositor, wl_region, wl_shm, wl_shm_pool, wl_surface}, }; const COMPOSITOR_VERSION: u32 = 5; const SHM_VERSION: u32 = 1; +const XDG_WM_BASE_VERSION: u32 = 5; + +/// 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 +/// quand il fait son commit. +const DEFAULT_TOPLEVEL_SIZE: (i32, i32) = (640, 480); + +/// Décalage cascading entre fenêtres successives, pour qu'elles +/// n'apparaissent pas toutes à (0, 0). +const CASCADE_OFFSET: i32 = 60; /// Pool SHM mmap'd côté compositor. Garde le `OwnedFd` pour qu'inputd ou /// le kernel ne libère pas la zone tant qu'on a des buffers en référence. @@ -115,6 +128,29 @@ struct SurfaceData { pending_buffer: Mutex>, /// Frame callbacks en attente (à signaler après le prochain present). pending_frame_callbacks: Mutex>, + /// Si une xdg_surface a été créée pour cette wl_surface, true tant + /// que le premier ack_configure n'a pas été reçu. Pendant cette + /// période, les buffers attachés ne doivent pas être affichés + /// (xdg-shell spec : le rôle xdg_surface "désactive" le rendu + /// jusqu'au premier configure-ack). + xdg_pending_initial_configure: Mutex, +} + +/// Données par-xdg_surface : référence à la wl_surface sous-jacente + +/// dernier serial envoyé + ack reçu. +struct XdgSurfaceData { + wl_surface: wl_surface::WlSurface, + last_serial: Mutex, + /// Le dernier serial qui a été ack par le client. Tant que c'est + /// inférieur à `last_serial`, le compositor doit ignorer les commits. + acked_serial: Mutex, +} + +/// Données par-xdg_toplevel : title, app_id, ref vers son xdg_surface. +#[derive(Default)] +struct XdgToplevelData { + title: Mutex>, + app_id: Mutex>, } #[derive(Debug)] @@ -135,6 +171,12 @@ pub struct WaylandFrontend { frame_callbacks: Vec, /// Counter monotone pour les `done` events (timestamp en ms simulé) frame_time_ms: u32, + /// Counter monotone pour les serials xdg_surface.configure. + /// Incrémenté à chaque envoi. + next_xdg_serial: u32, + /// Counter cascading des nouvelles fenêtres xdg_toplevel : la 1re est + /// placée à (60, 60), la 2e à (120, 120), etc. + next_toplevel_index: u32, } impl WaylandFrontend { @@ -147,6 +189,7 @@ impl WaylandFrontend { let mut dh = display.handle(); dh.create_global::(COMPOSITOR_VERSION, ()); dh.create_global::(SHM_VERSION, ()); + dh.create_global::(XDG_WM_BASE_VERSION, ()); let listener = wayland_server::ListeningSocket::bind_absolute(socket_path.to_path_buf())?; @@ -156,6 +199,8 @@ impl WaylandFrontend { listener, frame_callbacks: Vec::new(), frame_time_ms: 0, + next_xdg_serial: 1, + next_toplevel_index: 0, }) } @@ -254,6 +299,7 @@ impl wayland_server::Dispatch for WaylandFronte id: Mutex::new(Some(surface_id)), pending_buffer: Mutex::new(None), pending_frame_callbacks: Mutex::new(Vec::new()), + xdg_pending_initial_configure: Mutex::new(false), }; data_init.init(id, Arc::new(data)); } @@ -424,6 +470,16 @@ impl wayland_server::Dispatch> for Wayla Some(id) => id, None => return, }; + + // Si la surface a un rôle xdg_surface et n'a pas encore été + // ack-configure, on ignore le commit côté affichage (xdg-shell + // spec : "the surface is in an unconfigured state, the client + // can attach buffers but they are not visible"). + let pending_xdg = *data.xdg_pending_initial_configure.lock().unwrap(); + if pending_xdg { + return; + } + // Lire le buffer attaché (s'il y en a un) let bd_opt = data.pending_buffer.lock().unwrap().clone(); if let Some(bd) = bd_opt { @@ -473,3 +529,202 @@ impl wayland_server::Dispatch for WaylandFrontend { // wl_callback n'a pas de requests, juste l'event `done` } } + +// ===================================================================== +// xdg-shell (phase 7.1) +// ===================================================================== + +// ---- xdg_wm_base (global) ---- +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: &xdg_wm_base::XdgWmBase, + request: xdg_wm_base::Request, + _data: &(), + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, Self>, + ) { + match request { + xdg_wm_base::Request::GetXdgSurface { id, surface } => { + // Marquer la wl_surface comme ayant un rôle xdg : tant que + // le 1er ack_configure n'est pas reçu, ses commits ne sont + // pas affichés (cf wl_surface.commit ci-dessus). + if let Some(sd) = surface.data::>() { + *sd.xdg_pending_initial_configure.lock().unwrap() = true; + } + let xdg_data = Arc::new(XdgSurfaceData { + wl_surface: surface, + last_serial: Mutex::new(0), + acked_serial: Mutex::new(0), + }); + data_init.init(id, xdg_data); + } + xdg_wm_base::Request::CreatePositioner { id } => { + // Pour 7.1 : positioner no-op (popups hors scope). + data_init.init(id, ()); + } + xdg_wm_base::Request::Pong { .. } => { + // Réponse à un ping qu'on n'envoie jamais en 7.1 → ignore. + } + xdg_wm_base::Request::Destroy => { + // Le client se désengage. Pas d'action particulière côté + // serveur — les xdg_surface restantes seront détruites + // séparément. + } + _ => {} + } + } +} + +// ---- xdg_positioner (no-op pour 7.1, popups hors scope) ---- +impl wayland_server::Dispatch for WaylandFrontend { + fn request( + _state: &mut Self, + _client: &Client, + _r: &xdg_positioner::XdgPositioner, + _req: xdg_positioner::Request, + _: &(), + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, Self>, + ) { + } +} + +// ---- xdg_popup (no-op pour 7.1) ---- +impl wayland_server::Dispatch for WaylandFrontend { + fn request( + _state: &mut Self, + _client: &Client, + _r: &xdg_popup::XdgPopup, + _req: xdg_popup::Request, + _: &(), + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, Self>, + ) { + } +} + +// ---- xdg_surface ---- +impl wayland_server::Dispatch> for WaylandFrontend { + fn request( + state: &mut Self, + _client: &Client, + resource: &xdg_surface::XdgSurface, + request: xdg_surface::Request, + data: &Arc, + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, Self>, + ) { + match request { + xdg_surface::Request::GetToplevel { id } => { + let toplevel_data = Arc::new(XdgToplevelData::default()); + let toplevel = data_init.init(id, toplevel_data); + + // Position cascading pour que les fenêtres successives ne + // s'empilent pas toutes à (0, 0). + let idx = state.next_toplevel_index; + state.next_toplevel_index = state.next_toplevel_index.saturating_add(1); + let pos_x = CASCADE_OFFSET * (1 + idx as i32); + let pos_y = CASCADE_OFFSET * (1 + idx as i32); + + if let Some(sd) = data.wl_surface.data::>() { + if let Some(sid) = *sd.id.lock().unwrap() { + state.registry.modify_pending(sid, |s| { + s.x = pos_x; + s.y = pos_y; + }); + } + } + + // Initial configure : envoyer au client une taille suggérée + // et un serial. Le client doit ack_configure puis commit + // pour que la surface devienne visible. + let serial = state.next_xdg_serial; + state.next_xdg_serial = state.next_xdg_serial.wrapping_add(1); + *data.last_serial.lock().unwrap() = serial; + + toplevel.configure( + DEFAULT_TOPLEVEL_SIZE.0, + DEFAULT_TOPLEVEL_SIZE.1, + Vec::new(), // pas de states (Maximized, Activated, etc.) + ); + resource.configure(serial); + } + xdg_surface::Request::GetPopup { id, .. } => { + // Hors scope 7.1 — popups non supportés. On init avec + // un () pour ne pas violer le protocole. + data_init.init(id, ()); + } + xdg_surface::Request::SetWindowGeometry { .. } => { + // Stocké conceptuellement mais ignoré pour 7.1 (la geometry + // sert pour les decorations CSD que nous ne supportons pas). + } + xdg_surface::Request::AckConfigure { serial } => { + *data.acked_serial.lock().unwrap() = serial; + // Si c'était l'initial configure, débloquer l'affichage + if let Some(sd) = data.wl_surface.data::>() { + *sd.xdg_pending_initial_configure.lock().unwrap() = false; + } + } + xdg_surface::Request::Destroy => { + // Le client détruit son rôle xdg. La wl_surface peut survivre + // mais elle perd son rôle de fenêtre toplevel ; on cache la + // surface pour ne plus la dessiner. + if let Some(sd) = data.wl_surface.data::>() { + if let Some(sid) = *sd.id.lock().unwrap() { + state.registry.modify_pending(sid, |s| s.visible = false); + state.registry.commit(sid); + } + *sd.xdg_pending_initial_configure.lock().unwrap() = false; + } + } + _ => {} + } + } +} + +// ---- xdg_toplevel ---- +impl wayland_server::Dispatch> + for WaylandFrontend +{ + fn request( + _state: &mut Self, + _client: &Client, + _r: &xdg_toplevel::XdgToplevel, + request: xdg_toplevel::Request, + data: &Arc, + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, Self>, + ) { + match request { + xdg_toplevel::Request::SetTitle { title } => { + *data.title.lock().unwrap() = Some(title); + } + xdg_toplevel::Request::SetAppId { app_id } => { + *data.app_id.lock().unwrap() = Some(app_id); + } + xdg_toplevel::Request::Destroy => { + // wl_surface destroy gérera la suppression côté registry + } + // Tout le reste ignoré en 7.1 : + // SetParent, ShowWindowMenu, Move, Resize, SetMaxSize, SetMinSize, + // SetMaximized, UnsetMaximized, SetFullscreen, UnsetFullscreen, + // SetMinimized + _ => {} + } + } +} diff --git a/docs/phase7-1-xdg-shell.md b/docs/phase7-1-xdg-shell.md new file mode 100644 index 0000000..fba9224 --- /dev/null +++ b/docs/phase7-1-xdg-shell.md @@ -0,0 +1,202 @@ +# Phase 7.1 — xdg-shell minimal + +> Document produit le 2026-05-09 dans le cadre du plan directeur +> `REDOX_COSMIC_XWAYLAND_RS_PLAN.md`. +> +> **Scope strict** validé par user : +> - implémenter `xdg_wm_base` +> - gérer `get_xdg_surface(wl_surface)` +> - gérer `xdg_surface.get_toplevel` +> - envoyer `configure` (xdg_toplevel + xdg_surface) +> - recevoir `ack_configure` +> - gérer `commit` après configure +> - supporter `set_title` / `set_app_id` (stockés sans usage UI) +> - destroy propre via `xdg_toplevel.destroy` / `xdg_surface.destroy` +> +> **Hors scope 7.1** : focus, cursor, move/resize, decorations, +> multi-client avancé. + +## Verdict + +**✅ Cycle xdg-shell complet validé runtime.** + +Capture preuve : ![](phase7-1-xdg-toplevel.png) — fenêtre 320×240 +positionnée à `(60, 60)` par le compositor (cascade par défaut), avec +bandes verticales arc-en-ciel + bordure noire 2px peintes par le client. + +## Cycle de vie xdg-shell implémenté + +``` +client compositor +────── ────────── +Connection::connect_to_env +get_registry + ◄──── wl_compositor + wl_shm + xdg_wm_base + advertised +bind globals +compositor.create_surface ────► + SurfaceData { id: registry.create(), + xdg_pending: false } +wm_base.get_xdg_surface ────► + XdgSurfaceData { wl_surface, serial: 0 } + sd.xdg_pending_initial_configure = true +xdg_surface.get_toplevel ────► + XdgToplevelData::default() + next_toplevel_index += 1 + surface.x = +60, surface.y = +60 (cascade) + next_xdg_serial = 1 + ◄──── xdg_toplevel.configure(640, 480, []) + ◄──── xdg_surface.configure(serial=1) + ──────────────────────────────────────── +toplevel.set_title(...) ────► data.title = Some(...) +toplevel.set_app_id(...) ────► data.app_id = Some(...) +xdg_surface.ack_configure(1)────► data.acked_serial = 1 + sd.xdg_pending_initial_configure = false +shm.create_pool(fd, size) ────► ShmPool::new(fd) → mmap +pool.create_buffer(...) ────► BufferData { pool, offset, w, h, ... } +surface.attach(buffer) ────► sd.pending_buffer = Some(buf) +surface.damage_buffer(...) ────► (no-op tracking 7.1) +surface.commit ────► + !xdg_pending → continue + read shm pixels via mmap + SurfaceBuffer::from_pixels(...) + registry.modify_pending(id, |s| + s.buffer = Some(sb); s.visible = true) + registry.commit(id) + registry.raise(id) + +(toplevel.destroy) ────► no-op (wl_surface destroy fera le reste) +(xdg_surface.destroy) ────► surface.visible = false; commit +(surface.destroy) ────► registry.destroy(id) +``` + +## Modifications apportées + +### `redox-wl-wayland-frontend` + +Ajout dep `wayland-protocols` (feature `server`). + +Constants : +- `XDG_WM_BASE_VERSION: u32 = 5` +- `DEFAULT_TOPLEVEL_SIZE: (i32, i32) = (640, 480)` +- `CASCADE_OFFSET: i32 = 60` + +Nouveaux UserData : +- `XdgSurfaceData { wl_surface, last_serial, acked_serial }` +- `XdgToplevelData { title, app_id }` + +Modif `SurfaceData` : +- ajout `xdg_pending_initial_configure: Mutex` qui bloque l'affichage + tant que pas ack-configure (sémantique xdg-shell standard) + +Modif `WaylandFrontend` : +- `next_xdg_serial: u32` (counter monotone pour les configure serials) +- `next_toplevel_index: u32` (counter cascading) + +Dispatch ajoutés : +- `GlobalDispatch` + `Dispatch` +- `Dispatch` (no-op) +- `Dispatch` (no-op) +- `Dispatch>` : + - `GetToplevel` → cascade position + envoie initial configure + - `AckConfigure(serial)` → débloque affichage + - `Destroy` → cache la surface +- `Dispatch>` : + - `SetTitle`/`SetAppId` → stocke + - reste : ignoré (move, resize, set_*, etc.) + +### `redox-wl-test-client-shm` + +Ajout dep `wayland-protocols` (feature `client`). + +Bind 3 globals : `wl_compositor`, `wl_shm`, `xdg_wm_base`. + +Séquence runtime : +1. `compositor.create_surface()` +2. `wm_base.get_xdg_surface(&surface)` +3. `xdg_surface.get_toplevel()` +4. `toplevel.set_title("Phase 7.1 client")` +5. `toplevel.set_app_id("redox.wl.test.client.shm")` +6. `surface.commit()` — initial commit pour signaler "ready" +7. Attente du `xdg_surface.configure(serial)` event (avec timeout 5s) +8. `xdg_surface.ack_configure(serial)` +9. `shm.create_pool(fd)` + `pool.create_buffer` + pattern ARGB +10. `surface.attach(buffer)` + `damage_buffer` + `commit` +11. Boucle 25s vivante +12. `toplevel.destroy()` + `xdg_surface.destroy()` + `surface.destroy()` + +Bonus : Dispatch sur `XdgWmBase::Event::Ping` répond `pong(serial)` (le +compositor ne l'envoie pas en 7.1 mais c'est gratuit côté client). + +## Preuve runtime + +Logs serial QEMU complets : + +``` +[client] connect to compositor +[comp] CRTC pris +[comp] Wayland socket : /tmp/redox-wl-comp.sock +[client] globals : compositor=true shm=true xdg_wm_base=true +[client] xdg_toplevel créé, attente initial configure +[client] xdg_toplevel configure suggéré : 640x480 +[client] initial configure reçu, serial=1 +[client] ack_configure(1) +[client] buffer attach + damage + commit POST-ack +[comp] tick=30 surfaces=1 elapsed=1.2s +... (compositeur stable 18s avec 1 surface) ... +[comp] tick=450 surfaces=1 elapsed=18.2s +``` + +Capture frame T+14s : fenêtre client à `(60, 60)`, bandes RGB+orange+jaune+ +violet visibles avec bordure noire. + +## Limitations connues (à traiter en sous-tickets ultérieurs) + +- **Pas de focus** → toutes les surfaces sont "actives", pas de keyboard + enter/leave events. Phase 7.4. +- **Pas de cursor visible** côté écran. Phase 7.3. +- **Pas de move/resize interactifs** (xdg_toplevel.Move/Resize ignorés). + Phase 7.7. +- **Window geometry ignoré** (utilisé pour les decorations CSD que nous + ne supportons pas). +- **Multi-client non testé** (1 seul client en 7.1). Phase 7.6. +- **Pas de validation des serials** : si un client envoie un mauvais + ack_configure, on accepte sans broncher. À durcir en phase 7.5 + (robustesse). + +## Critère de fin 7.1 + +> Un client Wayland externe qui utilise xdg-shell peut créer une fenêtre +> toplevel visible, recevoir son configure, ack, commit, puis afficher +> via shm sans panic serveur. + +**✅ Validé.** Test runtime 18+ secondes, surface visible, aucun panic +serveur ni client, destroy propre à la fin. + +## Code + +``` +crates/redox-wl-wayland-frontend/ # +220 lignes pour xdg-shell (5 Dispatch + helpers) +└── Cargo.toml # + wayland-protocols/server + +crates/redox-wl-test-client-shm/ # adapté entièrement à xdg-shell +└── Cargo.toml # + wayland-protocols/client +``` + +Submodule `wayland-rs/wayland-protocols/protocols/` initialisé via +`git submodule update --init --depth 1`. + +## Suite phase 7.2 + +`wl_seat` + `wl_pointer` + `wl_keyboard` : +- Routage des events `InputBackend` vers la surface focalisée +- Décision XKB (recommandation : `xkeysym` Rust pur, layout US uniquement + au début ; xkbcommon C reportable) +- Premier client qui réagit aux events keyboard +- Tester via QEMU `sendkey` avec un client qui log les events reçus + +Estimé 2 sessions. + +--- + +*Fin du document de phase 7.1.* diff --git a/docs/phase7-1-xdg-toplevel.png b/docs/phase7-1-xdg-toplevel.png new file mode 100644 index 0000000000000000000000000000000000000000..29754373d62836cd849c7e4316a3602cd3c0ea76 GIT binary patch literal 1339 zcmeAS@N?(olHy`uVBq!ia0y~yU14Ba#1H&(% zP{RubhEf9thF1v;3|2E37{m+a>&}a4Hs8!T~0N3sZ zJqZ{7|NkGRP&pZ>j4{dE-G!s=-qb1}hrPtp*OmPhD<_W(V_4@gL7 zRdP`(kYX@0Ff!9MFx54%3^6paGBUR^HP$vTure@MTppW+q9HdwB{QuOw}wkvPp<(r zXuxeK$;?eHE=kNSK-XhoWnc)gWVLt0Nnoh_^mK6yskrs_k|Qrupa{#stsivPu<9f* zaz`AnOTX9HGQl^0f!6^UwY|AO3kZXnjJYv?`Po-93qP12{d@K40@=L7wG}+S%YQTd zDP>YobqE?IM#F&ebg