From 1dab6ff51b63fa31cc424eba103ea420c32f3a72 Mon Sep 17 00:00:00 2001 From: Votre Nom Date: Sat, 16 May 2026 14:19:53 +0200 Subject: [PATCH] =?UTF-8?q?Phase=2013.2.b.3=20=E2=80=94=20client=20de=20te?= =?UTF-8?q?st=20visuel=20pour=20wl=5Fsubsurface?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nouveau crate redox-wl-test-client-subcompositor : client Wayland qui crée un toplevel parent 300×200 bleu nuit avec bordure noire, et lui attache une subsurface 60×60 rouge à l'offset (50, 50). Si le rendering 13.2.b.2 fonctionne, on doit voir visuellement dans la fenêtre QEMU : la fenêtre bleue avec un carré rouge incrusté en haut- gauche, exactement 50px depuis le bord du parent. Cycle complet : 1. Bind 5 globals : wl_compositor, wl_shm, xdg_wm_base, wl_seat, wl_subcompositor (le client échouera si compositor manque l'un d'eux) 2. Parent xdg_toplevel avec ack_configure 3. SHM buffer 300×200 bleu + attach + commit parent 4. wl_compositor.create_surface pour le child 5. wl_subcompositor.get_subsurface(child, parent) + set_position(50,50) + set_desync (pour qu'un commit child suffise à rendre les changements) 6. SHM buffer 60×60 rouge + attach + commit child 7. Re-commit parent (force redraw) 8. Event loop avec ESC → exit propre Reprend le pattern d'event-loop tolérant Interrupted/BrokenPipe de la phase 13.1.b (5e adaptation Redox côté client). run-qemu.sh copie maintenant aussi ce binaire dans /usr/bin de l'image (optionnel, skip silencieux si non compilé). Les 2 tests natifs continuent de PASSer en non-régression. Leyoda 2026 – GPLv3 --- .../Cargo.toml | 10 + .../src/main.rs | 390 ++++++++++++++++++ run-qemu.sh | 6 + 3 files changed, 406 insertions(+) create mode 100644 crates/redox-wl-test-client-subcompositor/Cargo.toml create mode 100644 crates/redox-wl-test-client-subcompositor/src/main.rs diff --git a/crates/redox-wl-test-client-subcompositor/Cargo.toml b/crates/redox-wl-test-client-subcompositor/Cargo.toml new file mode 100644 index 0000000..2aca781 --- /dev/null +++ b/crates/redox-wl-test-client-subcompositor/Cargo.toml @@ -0,0 +1,10 @@ +[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 new file mode 100644 index 0000000..fac5bcf --- /dev/null +++ b/crates/redox-wl-test-client-subcompositor/src/main.rs @@ -0,0 +1,390 @@ +//! 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/run-qemu.sh b/run-qemu.sh index 091e522..ee09e43 100755 --- a/run-qemu.sh +++ b/run-qemu.sh @@ -35,6 +35,8 @@ CLIENT_BIN="$ROOT/crates/redox-wl-real-client-simple-window/target/x86_64-unknow # 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 @@ -133,6 +135,10 @@ cp -v "$CLIENT_BIN" "$MOUNT/usr/bin/" 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 # --- 4. umount avant make qemu (sinon QEMU et FUSE se battent sur le même fichier) --- echo "==> démonter $MOUNT"