From f9c3de13da3dfb6ccfdb6e74a8fd1385be6c25d3 Mon Sep 17 00:00:00 2001 From: Votre Nom Date: Sat, 16 May 2026 13:01:45 +0200 Subject: [PATCH] =?UTF-8?q?Phase=2013.2.b.1=20=E2=80=94=20wl=5Fsubcomposit?= =?UTF-8?q?or=20protocole=20(sans=20rendering)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implémentation minimale du global wl_subcompositor v1 côté compositor. Couvre uniquement le protocole : bind, GetSubsurface, et tous les requests wl_subsurface (SetPosition, PlaceAbove/Below, SetSync/SetDesync, Destroy). Les données sont stockées dans SubsurfaceData mais ne sont pas encore consommées par le rendering — c'est le scope de 13.2.b.2. Ce qui marche maintenant : - Un client qui bind wl_subcompositor le trouve à v1 - get_subsurface(child, parent) ne crashe pas, retourne un wl_subsurface valide avec SubsurfaceData attaché (parent ref, child ref, position pending, sync mode default true) - Toutes les requests subséquentes sont acceptées sans erreur protocole - destroy : no-op propre (la resource est nettoyée par wayland-server) Limitations explicites pour 13.2.b.2 : - Pas de role-tracking (la spec exige bad_surface si la wl_surface enfant a déjà un rôle ; on log debug seulement) - Pas de cascade sync : un commit du parent ne propage pas les states pending des subsurfaces - PlaceAbove/Below : no-op (single-subsurface use-case suffit pour 13.2.b) - compose_into ne sait pas dessiner les subsurfaces Test natif (cargo test, sans QEMU) : - Vérifie l'annonce du global à v1 - Bind + create_surface ×2 + get_subsurface + tous les requests wl_subsurface successifs + destroy - Roundtrip à chaque étape pour capter d'éventuelles erreurs protocole - PASS le 2026-05-16 sur CachyOS Leyoda 2026 – GPLv3 --- .../tests/subcompositor_native.rs | 364 ++++++++++++++++++ crates/redox-wl-wayland-frontend/src/lib.rs | 155 +++++++- 2 files changed, 518 insertions(+), 1 deletion(-) create mode 100644 crates/redox-wl-test-wl-output/tests/subcompositor_native.rs diff --git a/crates/redox-wl-test-wl-output/tests/subcompositor_native.rs b/crates/redox-wl-test-wl-output/tests/subcompositor_native.rs new file mode 100644 index 0000000..40646c6 --- /dev/null +++ b/crates/redox-wl-test-wl-output/tests/subcompositor_native.rs @@ -0,0 +1,364 @@ +//! 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 37215ca..0e87f82 100644 --- a/crates/redox-wl-wayland-frontend/src/lib.rs +++ b/crates/redox-wl-wayland-frontend/src/lib.rs @@ -37,7 +37,7 @@ 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_surface, + wl_seat, wl_shm, wl_shm_pool, wl_subcompositor, wl_subsurface, wl_surface, }, Client, DataInit, Display as WlDisplay, DisplayHandle, GlobalDispatch, Resource, }; @@ -51,6 +51,11 @@ 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 @@ -193,6 +198,23 @@ 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 { @@ -442,6 +464,12 @@ impl WaylandFrontend { // 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())?; @@ -2247,6 +2275,131 @@ impl wayland_server::Dispatch for WaylandFrontend { } } +// ===================================================================== +// 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 13.2.b.2 pour + // l'enforcer car ça touche au design role-tracking. + if let Some(child_data) = surface.data::>() { + let id_lock = child_data.id.lock().unwrap(); + if id_lock.is_some() { + // La surface a déjà été registered comme toplevel ? + // Hmm, en fait avoir un SurfaceId ne signifie pas un + // rôle xdg — c'est juste qu'elle vit dans le registry. + // On ne fait rien de plus que log debug. + tracing::debug!( + "GetSubsurface: surface child déjà dans registry (id={:?})", + *id_lock + ); + } + } + let data = Arc::new(SubsurfaceData { + parent, + child: surface, + position: Mutex::new((0, 0)), + sync: AtomicBool::new(true), // default = sync per spec + }); + 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). //