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
This commit is contained in:
Votre Nom 2026-05-16 14:19:53 +02:00
parent bba2d7beb6
commit 1dab6ff51b
3 changed files with 406 additions and 0 deletions

View file

@ -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"

View file

@ -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<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
}
}
}

View file

@ -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"