redox-wayland-compositor/crates/redox-wl-test-client-subcompositor/src/main.rs
Votre Nom 1dab6ff51b Phase 13.2.b.3 — client de test visuel pour wl_subsurface
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
2026-05-16 14:19:53 +02:00

390 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 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<Option<std::fs::File>>);
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<DebugSink> = OnceLock::new();
SINK.get_or_init(DebugSink::new).writeln(s);
}
#[derive(Default)]
struct State {
compositor: Option<WlCompositor>,
shm: Option<WlShm>,
wm_base: Option<XdgWmBase>,
seat: Option<WlSeat>,
subcompositor: Option<WlSubcompositor>,
pending_serial: Option<u32>,
configured: bool,
running: bool,
}
impl Dispatch<wl_registry::WlRegistry, ()> for State {
fn event(
state: &mut Self,
registry: &wl_registry::WlRegistry,
event: wl_registry::Event,
_: &(),
_conn: &Connection,
qh: &QueueHandle<Self>,
) {
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<Self>,
) {
}
}
};
}
noop!(WlCompositor);
noop!(WlShm);
noop!(WlShmPool);
noop!(WlBuffer);
noop!(WlSurface);
noop!(WlSeat);
noop!(WlSubcompositor);
noop!(WlSubsurface);
impl Dispatch<XdgWmBase, ()> for State {
fn event(
_: &mut Self,
wm_base: &XdgWmBase,
event: xdg_wm_base::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
if let xdg_wm_base::Event::Ping { serial } = event {
wm_base.pong(serial);
}
}
}
impl Dispatch<XdgSurface, ()> for State {
fn event(
state: &mut Self,
_: &XdgSurface,
event: xdg_surface::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
if let xdg_surface::Event::Configure { serial } = event {
state.pending_serial = Some(serial);
state.configured = true;
}
}
}
impl Dispatch<XdgToplevel, ()> for State {
fn event(
state: &mut Self,
_: &XdgToplevel,
event: xdg_toplevel::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
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<wl_keyboard::WlKeyboard, ()> for State {
fn event(
state: &mut Self,
_: &wl_keyboard::WlKeyboard,
event: wl_keyboard::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
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<OwnedFd, String> {
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<dyn std::error::Error>> {
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<State> = 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
}
}
}