diff --git a/crates/redox-wl-real-client-simple-window/Cargo.toml b/crates/redox-wl-real-client-simple-window/Cargo.toml new file mode 100644 index 0000000..7facf46 --- /dev/null +++ b/crates/redox-wl-real-client-simple-window/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "redox-wl-real-client-simple-window" +version = "0.1.0" +edition = "2021" +description = "Port Redox du client wayland-rs/examples/simple_window.rs — premier client tiers (Phase 13.1)" + +[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-real-client-simple-window/src/main.rs b/crates/redox-wl-real-client-simple-window/src/main.rs new file mode 100644 index 0000000..3f1aff2 --- /dev/null +++ b/crates/redox-wl-real-client-simple-window/src/main.rs @@ -0,0 +1,370 @@ +//! Phase 13.1 — Port Redox de l'exemple upstream +//! `wayland-rs/wayland-client/examples/simple_window.rs`. +//! +//! Premier client **tiers** (= que nous n'avons pas écrit pour +//! satisfaire notre compositor). Objectif : faire émerger les manques +//! réels du compositor en exerçant les chemins protocolaires que nos +//! propres clients de test ne couvrent pas exhaustivement. +//! +//! Différences vs upstream (et seulement ces différences) : +//! +//! - `Connection::connect_to_env()` → `UnixStream::connect(SOCKET_PATH)` +//! + `Backend::connect(stream)` parce que `WAYLAND_DISPLAY` n'est pas +//! toujours posé dans l'environnement init Redox. +//! - `tempfile::tempfile()` → `libc::shm_open` + `ftruncate` + `mmap` +//! parce que `tempfile` dépend de `O_TMPFILE`/`mkostemp` non garantis +//! par relibc. +//! - Petite attente initiale sur l'existence du socket pour permettre +//! au compositor de démarrer en parallèle dans le même init script. +//! - Sortie sur `/scheme/debug` en plus de stdout (serial console host). +//! +//! Tout le reste — pattern Dispatch, `delegate_noop!`, `init_xdg_surface`, +//! le dessin (gradient ARGB), la gestion de Close/ESC — est conservé +//! verbatim depuis l'upstream pour bien isoler les manques du compositor +//! du code du client. + +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::{ + Dispatch, QueueHandle, WEnum, delegate_noop, + backend::Backend, + protocol::{ + wl_buffer, wl_compositor, wl_keyboard, wl_registry, wl_seat, wl_shm, wl_shm_pool, + wl_surface, + }, + Connection, +}; + +use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base}; + +const SOCKET_PATH: &str = "/tmp/redox-wl-comp.sock"; + +// ---------- Logging tee 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); +} + +// ---------- Buffer SHM (équivalent de tempfile + draw upstream) ---------- +const INIT_W: u32 = 320; +const INIT_H: u32 = 240; +const PIXEL_BYTES: usize = 4; +const BUFFER_SIZE: usize = (INIT_W as usize) * (INIT_H as usize) * PIXEL_BYTES; + +/// Équivalent du `draw()` upstream + création shm. +/// Garde le même algorithme de gradient pour que le rendu visuel +/// matche le screenshot upstream. +unsafe fn create_shm_buffer() -> Result { + let cname = CString::new("/redox-wl-real-client-simple-window").unwrap(); + let _ = libc::shm_unlink(cname.as_ptr()); + let fd = libc::shm_open(cname.as_ptr(), libc::O_RDWR | libc::O_CREAT, 0o600); + if fd < 0 { + return Err(format!( + "shm_open errno={}", + std::io::Error::last_os_error().raw_os_error().unwrap_or(0) + )); + } + if libc::ftruncate(fd, BUFFER_SIZE as _) != 0 { + libc::close(fd); + return Err("ftruncate failed".into()); + } + let p = libc::mmap( + ptr::null_mut(), + BUFFER_SIZE, + 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 u8, BUFFER_SIZE); + // Reproduction verbatim du draw() upstream : gradient ARGB. + // Layout = [B, G, R, A] little-endian (Argb8888 Wayland). + use std::cmp::min; + let buf_x = INIT_W; + let buf_y = INIT_H; + let mut idx = 0usize; + for y in 0..buf_y { + for x in 0..buf_x { + let a: u32 = 0xFF; + let r = min(((buf_x - x) * 0xFF) / buf_x, ((buf_y - y) * 0xFF) / buf_y); + let g = min((x * 0xFF) / buf_x, ((buf_y - y) * 0xFF) / buf_y); + let b = min(((buf_x - x) * 0xFF) / buf_x, (y * 0xFF) / buf_y); + pixels[idx] = b as u8; + pixels[idx + 1] = g as u8; + pixels[idx + 2] = r as u8; + pixels[idx + 3] = a as u8; + idx += 4; + } + } + libc::munmap(p, BUFFER_SIZE); + Ok(OwnedFd::from_raw_fd(fd)) +} + +// ---------- État client + Dispatch impls (verbatim upstream) ---------- +struct State { + running: bool, + base_surface: Option, + buffer: Option, + wm_base: Option, + xdg_surface: Option<(xdg_surface::XdgSurface, xdg_toplevel::XdgToplevel)>, + configured: bool, +} + +impl Dispatch for State { + fn event( + state: &mut Self, + registry: &wl_registry::WlRegistry, + event: wl_registry::Event, + _: &(), + _: &Connection, + qh: &QueueHandle, + ) { + if let wl_registry::Event::Global { name, interface, .. } = event { + match &interface[..] { + "wl_compositor" => { + let compositor = + registry.bind::(name, 1, qh, ()); + let surface = compositor.create_surface(qh, ()); + state.base_surface = Some(surface); + + if state.wm_base.is_some() && state.xdg_surface.is_none() { + state.init_xdg_surface(qh); + } + } + "wl_shm" => { + let shm = registry.bind::(name, 1, qh, ()); + let fd = unsafe { create_shm_buffer() }.unwrap_or_else(|e| { + dlog(&format!("[real-client] create_shm_buffer FAIL: {e}")); + std::process::exit(2); + }); + let pool = shm.create_pool(fd.as_fd(), BUFFER_SIZE as i32, qh, ()); + let buffer = pool.create_buffer( + 0, + INIT_W as i32, + INIT_H as i32, + (INIT_W as i32) * 4, + wl_shm::Format::Argb8888, + qh, + (), + ); + state.buffer = Some(buffer.clone()); + + if state.configured { + let surface = state.base_surface.as_ref().unwrap(); + surface.attach(Some(&buffer), 0, 0); + surface.commit(); + } + } + "wl_seat" => { + registry.bind::(name, 1, qh, ()); + } + "xdg_wm_base" => { + let wm_base = registry.bind::(name, 1, qh, ()); + state.wm_base = Some(wm_base); + + if state.base_surface.is_some() && state.xdg_surface.is_none() { + state.init_xdg_surface(qh); + } + } + _ => {} + } + } + } +} + +delegate_noop!(State: ignore wl_compositor::WlCompositor); +delegate_noop!(State: ignore wl_surface::WlSurface); +delegate_noop!(State: ignore wl_shm::WlShm); +delegate_noop!(State: ignore wl_shm_pool::WlShmPool); +delegate_noop!(State: ignore wl_buffer::WlBuffer); + +impl State { + fn init_xdg_surface(&mut self, qh: &QueueHandle) { + let wm_base = self.wm_base.as_ref().unwrap(); + let base_surface = self.base_surface.as_ref().unwrap(); + + let xdg_surface = wm_base.get_xdg_surface(base_surface, qh, ()); + let toplevel = xdg_surface.get_toplevel(qh, ()); + toplevel.set_title("A fantastic window!".into()); + + base_surface.commit(); + + self.xdg_surface = Some((xdg_surface, toplevel)); + } +} + +impl Dispatch for State { + fn event( + _: &mut Self, + wm_base: &xdg_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, + xdg_surface: &xdg_surface::XdgSurface, + event: xdg_surface::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + if let xdg_surface::Event::Configure { serial, .. } = event { + xdg_surface.ack_configure(serial); + state.configured = true; + let surface = state.base_surface.as_ref().unwrap(); + if let Some(ref buffer) = state.buffer { + surface.attach(Some(buffer), 0, 0); + surface.commit(); + } + } + } +} + +impl Dispatch for State { + fn event( + state: &mut Self, + _: &xdg_toplevel::XdgToplevel, + event: xdg_toplevel::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + if let xdg_toplevel::Event::Close = event { + dlog("[real-client] xdg_toplevel.close → exit"); + state.running = false; + } + } +} + +impl Dispatch for State { + fn event( + _: &mut Self, + seat: &wl_seat::WlSeat, + event: wl_seat::Event, + _: &(), + _: &Connection, + qh: &QueueHandle, + ) { + if let wl_seat::Event::Capabilities { + capabilities: WEnum::Value(capabilities), + } = event + { + if capabilities.contains(wl_seat::Capability::Keyboard) { + seat.get_keyboard(qh, ()); + } + } + } +} + +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 { + // ESC scancode (evdev) + dlog("[real-client] ESC → exit"); + state.running = false; + } + } + } +} + +fn run() -> Result<(), Box> { + dlog("[real-client] simple_window port Redox — start"); + + // Attente du socket compositor (init scripts lancent compositor + client + // en parallèle ; le client peut démarrer avant que le socket soit bound). + 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 = conn.new_event_queue(); + let qhandle = event_queue.handle(); + + let display = conn.display(); + display.get_registry(&qhandle, ()); + + let mut state = State { + running: true, + base_surface: None, + buffer: None, + wm_base: None, + xdg_surface: None, + configured: false, + }; + + dlog("[real-client] entering event loop"); + while state.running { + event_queue.blocking_dispatch(&mut state)?; + } + dlog("[real-client] loop exited cleanly"); + Ok(()) +} + +fn main() -> ExitCode { + match run() { + Ok(()) => { + dlog("[real-client] PASS"); + ExitCode::SUCCESS + } + Err(e) => { + dlog(&format!("[real-client] FAIL: {e}")); + ExitCode::FAILURE + } + } +} diff --git a/docs/phase13-1-real-client-simple-window.md b/docs/phase13-1-real-client-simple-window.md new file mode 100644 index 0000000..6575978 --- /dev/null +++ b/docs/phase13-1-real-client-simple-window.md @@ -0,0 +1,196 @@ +# Phase 13.1 — Premier client tiers : `simple_window` de wayland-rs + +> Document produit le 2026-05-15 dans le cadre du plan directeur +> `REDOX_COSMIC_XWAYLAND_RS_PLAN.md`. +> +> **Scope strict** : +> - Porter l'exemple `wayland-rs/wayland-client/examples/simple_window.rs` +> en crate Redox autonome (`redox-wl-real-client-simple-window`). +> - Cross-compiler via `redoxer build` pour `x86_64-unknown-redox`. +> - Documenter la procédure runtime (image bootable + `make qemu`). +> - **Cette phase ne valide pas encore le rendu** : elle prépare le test. +> Le runtime sera observé et l'observabilité des bugs sera consignée +> dans une phase 13.1.b suivante. +> +> **Hors scope 13.1** : modifier le code de l'exemple upstream pour +> contourner un manque du compositor. Si l'exemple échoue, le verdict +> est "le compositor doit s'adapter au client standard", pas l'inverse. +> Seules les 4 adaptations listées ci-dessous (justifiées par +> l'environnement Redox) sont autorisées. + +## Pourquoi cet exemple, et pas un autre + +`simple_window.rs` est : + +- **Officiel upstream wayland-rs** — donc représentatif du standard. +- **Pur-Rust** — cross-compilable via redoxer sans toucher au sysroot. +- **Minimal mais réaliste** : `wl_compositor` + `wl_shm` + `xdg_wm_base` + + `xdg_toplevel` + `wl_seat` + `wl_keyboard`. Tous nos chemins critiques + sont exercés. +- **Pas écrit par nous** : c'est sa principale qualité. Nos propres clients + de test (phases 6.x et 7.x) ont été conçus pour valider des paths + particuliers du compositor — ils ne révèlent pas les paths qu'on n'a + pas anticipés. Un client tiers le fait par construction. + +Alternatives écartées : +- `list_globals.rs` (74 lignes) : trop simple, ne stresse pas xdg-shell. +- `weston-simple-shm` (C) : exige libwayland-client.so, non porté sur Redox. +- `smithay-client-toolkit/examples/simple_window` : ajoute une dépendance + importante (sctk) qui dépasse le périmètre 13.1. À reprendre en 13.2. +- GTK4 client : ratera immédiatement sur dmabuf, decoration, subsurface. + À garder pour une phase 13.4+ après durcissement. + +## Les 4 adaptations Redox + +Toute différence vs l'upstream doit être justifiée par l'environnement, +pas par un manque du compositor. Liste exhaustive : + +1. **`Connection::connect_to_env()` → `UnixStream::connect(SOCKET_PATH)`**. + Justification : sous Redox, `WAYLAND_DISPLAY` n'est pas garanti dans + l'environnement des processus lancés par un script init. Connexion + explicite au socket connu. +2. **`tempfile::tempfile()` → `libc::shm_open` + `ftruncate` + `mmap`**. + Justification : `tempfile` repose sur `O_TMPFILE` ou `mkostemp` qui + ne sont pas garantis par relibc. On utilise le pattern que nos clients + de test 6.x/7.x utilisent déjà — c'est la voie Redox-stable. +3. **Petite attente sur l'existence du socket** (50×100 ms). Justification : + compositor et client sont lancés en parallèle par l'init Redox ; sans + poll, le client lose une race au démarrage. +4. **Logs tee sur stdout + `/scheme/debug`**. Justification : permet + d'observer le client depuis le terminal host (serial) même si on + lance le compositor sur un VT et le client sur un autre. + +Tout le reste — `Dispatch`, `delegate_noop!`, `init_xdg_surface`, +l'algorithme `draw()` gradient ARGB, la gestion `Close`/ESC — est +**verbatim upstream**. + +## Compilation + +```bash +cd ~/Projets/Redox/redox-wayland-compositor/crates/redox-wl-real-client-simple-window +redoxer build --release +``` + +Sortie attendue (testée 2026-05-15) : +- `target/x86_64-unknown-redox/release/redox-wl-real-client-simple-window` +- ~1.1 Mo, ELF 64-bit statically linked +- 0 warning, 0 erreur + +`cargo +nightly check` natif fonctionne aussi pour smoke-check syntaxique, +mais ne produit pas un binaire utilisable. + +## Procédure de test runtime + +Le compositor a besoin d'un vrai framebuffer Redox + d'inputd actif. +La voie validée est `make qemu` sur une image qui contient le compositor +ET le client copiés dans `/usr/bin`. + +### Étape 1 — copier les deux binaires dans l'image Redox + +```bash +mkdir -p /tmp/redox-mnt +~/Projets/Redox/redox-src/build/fstools/bin/redoxfs \ + ~/Projets/Redox/redox-src/build/x86_64/desktop/harddrive.img \ + /tmp/redox-mnt & +sleep 2 + +# compositor (déjà cross-compilé en phase 6.4) +cp ~/Projets/Redox/redox-wayland-compositor/crates/redox-wl-compositor/target/x86_64-unknown-redox/release/redox-wl-compositor \ + /tmp/redox-mnt/usr/bin/ + +# le client tiers +cp ~/Projets/Redox/redox-wayland-compositor/crates/redox-wl-real-client-simple-window/target/x86_64-unknown-redox/release/redox-wl-real-client-simple-window \ + /tmp/redox-mnt/usr/bin/ + +fusermount -u /tmp/redox-mnt +rmdir /tmp/redox-mnt +``` + +### Étape 2 — boot Redox sous QEMU + +```bash +cd ~/Projets/Redox/redox-src +make qemu audio=no QEMU_USER_FLAGS="-k fr" +``` + +Une fois Redox bootée : +- **Ctrl+Alt+F2** → bascule sur VT 2 (console texte, sans Orbital) +- login `root` / `password` + +### Étape 3 — lancer compositor + client + +Dans le shell Redox sur VT 2 : + +```bash +# compositor en background +RUST_LOG=info redox-wl-compositor & + +# attendre ~1 s pour que le socket soit bound +# (le client poll de toute façon — c'est juste pour rendre la séquence +# lisible dans les logs) +sleep 1 + +# le client +redox-wl-real-client-simple-window +``` + +### Étape 4 — observations à consigner + +À chaque session de test, noter dans une section "Observation" du +prochain doc (13.1.b) : + +1. **Connexion** : le client connecte-t-il (`[real-client] entering event loop` apparaît) ? +2. **Globals reçus** : le client log-t-il que `wl_compositor`, `wl_shm`, + `wl_seat`, `xdg_wm_base` sont bind ? (Côté compositor : `xdg_wm_base.create_xdg_surface` doit logger en `debug`.) +3. **Configure initial** : le client ack-t-il un `xdg_surface.configure` ? + Si non, on a un bug dans notre `GetToplevel` handler. +4. **Premier commit pixel** : la fenêtre apparaît-elle à l'écran ? Gradient + ARGB visible ? Si la fenêtre est noire ou corrompue, comparer le pattern + au screenshot upstream (gradient diagonal rouge/vert/bleu). +5. **Input** : `ESC` ferme-t-il le client proprement ? +6. **Sortie** : le client termine-t-il sur `[real-client] PASS` côté serial ? + +Lister explicitement les comportements qui dévient de l'attendu. + +## Risques techniques anticipés + +À voir avec le runtime — ne pas tirer de conclusion avant. + +- **`get_xdg_surface` sans `set_role`** : on n'envoie pas d'erreur pour + les rôles incompatibles, donc OK probablement. +- **Initial configure suggestion** : on envoie (640, 480) en suggestion, + l'exemple upstream commit son buffer 320×240 — c'est légal spec mais + notre logique de raise/positionnement utilise la taille de buffer, à + voir si ça pose problème. +- **`wl_keyboard.enter` requis pour que la key ESC remonte** : notre + routing input via `focused_client_id` (phase 7.6) devrait envoyer le + `keyboard.enter` à la fenêtre quand elle reçoit le focus initial. À + observer. +- **`xdg_toplevel.close` jamais émis par notre compositor** : l'exemple + upstream s'attend à recevoir un `Close` quand l'utilisateur clique + sur un bouton de fermeture. Comme nous n'avons pas de décoration, + ce path est mort-né — donc la seule sortie possible est ESC. +- **Buffer fd via shm_open** : la mémoire reste vivante après `munmap` + côté client tant que le compositor garde le fd. Notre `ShmPool::new` + fait un mmap côté serveur, donc OK. + +## Critère de fin 13.1 + +> Le binaire `redox-wl-real-client-simple-window` : +> 1. Compile pour `x86_64-unknown-redox` via `redoxer build --release`, +> sans warning ni erreur. +> 2. Vit dans `crates/redox-wl-real-client-simple-window/` avec son +> propre `Cargo.toml`. +> 3. Préserve le code upstream verbatim sauf pour les 4 adaptations +> Redox justifiées ci-dessus. +> 4. La procédure runtime est documentée et reproductible. + +**✅ Validé au niveau compilation et préparation** (2026-05-15). +Runtime à observer dans la phase 13.1.b après test interactif. + +## Code + +``` +crates/redox-wl-real-client-simple-window/ # nouvelle crate +docs/phase13-1-real-client-simple-window.md # ce document +```