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:
parent
bba2d7beb6
commit
1dab6ff51b
3 changed files with 406 additions and 0 deletions
10
crates/redox-wl-test-client-subcompositor/Cargo.toml
Normal file
10
crates/redox-wl-test-client-subcompositor/Cargo.toml
Normal 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"
|
||||
390
crates/redox-wl-test-client-subcompositor/src/main.rs
Normal file
390
crates/redox-wl-test-client-subcompositor/src/main.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue