🎉🎉🎉 Phase 6.4 — Wayland complet : un client externe affiche ses pixels

Capture preuve : docs/phase6-4-wayland-client-surface.png — pattern ARGB
320x240 écrit par un binaire client Wayland externe affiché par notre
compositor sur le framebuffer Redox dans QEMU.

Crates ajoutés :

redox-wl-wayland-frontend (lib, ~430 lignes) :
- WaylandFrontend struct avec SurfaceRegistry intégré + Display<Self>
  + ListeningSocket
- bind_absolute(path), accept_pending_clients(), dispatch_clients(),
  flush_clients(), notify_frame_done()
- ShmPool : mmap + munmap on drop
- BufferData : Arc<Mutex<ShmPool>> + offset/w/h/stride/format
- SurfaceData : Arc<...> qui contient SurfaceId + pending_buffer
  + pending_frame_callbacks
- Dispatch impls : wl_compositor v5, wl_shm v1 (advertise ARGB+XRGB),
  wl_shm_pool, wl_buffer, wl_surface (attach/damage/commit/frame/destroy),
  wl_callback, wl_region (no-op)

Sémantique commit : copy-on-commit (lit pixels via mmap, copie dans
SurfaceBuffer owned). Plus simple que de garder le mmap vivant. Au
commit, raise auto la surface (politique simple).

redox-wl-compositor (bin, ~150 lignes) :
- ouvre RedoxOutput + InputBackend partagé
- bind WaylandFrontend sur /tmp/redox-wl-comp.sock
- export WAYLAND_DISPLAY env var
- boucle main 30 fps : accept clients → dispatch → input → render →
  notify_frame_done → flush
- Esc = exit propre

redox-wl-test-client-shm (bin, ~170 lignes) :
- attente du socket compositor (50 retries × 100ms)
- Connection::from_backend après UnixStream::connect
- Dispatch handlers minimal pour wl_registry, compositor, shm, pool,
  buffer, surface
- shm_open + ftruncate + mmap + pattern ARGB déterministe (orange
  + bandes diagonales)
- shm.create_pool(fd) + pool.create_buffer + compositor.create_surface
- surface.attach + damage_buffer + commit
- reste connecté 25s pour qu'on capture l'écran

Validation runtime : compositor en init VT=2, client lancé en parallèle
via 30_console. Logs serial montrent toute la séquence :
  [client] globals : compositor=true shm=true
  [client] shm créé, peint 320x240 ARGB
  [client] surface attach + damage + commit envoyés
  [comp]  tick=30 surfaces=1 elapsed=1.2s
  [comp]  tick=510 surfaces=1 elapsed=20.7s    ← surface persiste 20+s

PNG capturée à T+12s montre la surface du client visible sur le
framebuffer. Position (0,0) parce que xdg-shell absent (placement
absent). Reportable phase 7.

Image Redox restaurée à boot Orbital normal.

docs/phase6-4-wayland-frontend.md : compte-rendu complet, archi,
sémantique commit, limitations, plan phase 7.

Phase 6 entièrement close. Le compositor naissant fonctionne avec un
vrai client Wayland externe sur Redox.

Leyoda 2026 – GPLv3
This commit is contained in:
Votre Nom 2026-05-09 13:30:05 +02:00
parent 509aae7769
commit 8a897d975d
8 changed files with 1098 additions and 0 deletions

View file

@ -0,0 +1,9 @@
[package]
name = "redox-wl-test-client-shm"
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 }
libc = "0.2"

View file

@ -0,0 +1,229 @@
//! Phase 6.4 — Client Wayland test.
//!
//! Se connecte au socket Wayland exposé par le compositor à
//! `/tmp/redox-wl-comp.sock`, crée une surface 320x240, peint un
//! pattern ARGB déterministe (dégradé orangé), commit. Reste connecté
//! 30s pour qu'on puisse capturer l'écran.
//!
//! C'est le test critique de phase 6.4 : si le compositor affiche
//! ce qui suit dans la frame QEMU, on a un VRAI compositor Wayland
//! qui rend des pixels venant d'un client externe.
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::{
Connection, Dispatch, EventQueue, Proxy, QueueHandle,
backend::Backend,
protocol::{
wl_buffer::WlBuffer, wl_compositor::WlCompositor, wl_registry, wl_shm::WlShm,
wl_shm_pool::WlShmPool, wl_surface::WlSurface,
},
};
const SOCKET_PATH: &str = "/tmp/redox-wl-comp.sock";
const W: i32 = 320;
const H: i32 = 240;
const STRIDE: i32 = W * 4;
const SIZE: i32 = STRIDE * H;
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 ClientState {
compositor: Option<WlCompositor>,
shm: Option<WlShm>,
}
impl Dispatch<wl_registry::WlRegistry, ()> for ClientState {
fn event(
state: &mut Self,
registry: &wl_registry::WlRegistry,
event: wl_registry::Event,
_data: &(),
_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, version.min(1), qh, ()));
}
_ => {}
}
}
}
}
macro_rules! noop {
($ty:ty) => {
impl Dispatch<$ty, ()> for ClientState {
fn event(
_state: &mut Self,
_r: &$ty,
_ev: <$ty as Proxy>::Event,
_: &(),
_conn: &Connection,
_qh: &QueueHandle<Self>,
) {
}
}
};
}
noop!(WlCompositor);
noop!(WlShm);
noop!(WlShmPool);
noop!(WlBuffer);
noop!(WlSurface);
unsafe fn create_shm_with_pattern(name: &str) -> 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(format!(
"shm_open: errno {}",
std::io::Error::last_os_error().raw_os_error().unwrap_or(0)
));
}
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());
}
// Pattern ARGB : dégradé orangé + bandes diagonales pour reconnaître
let pixels = std::slice::from_raw_parts_mut(p as *mut u32, (W * H) as usize);
for y in 0..H {
for x in 0..W {
let r: u32 = (200 + (x * 55 / W)) as u32 & 0xFF;
let g: u32 = (80 + (y * 100 / H)) as u32 & 0xFF;
let b: u32 = (((x + y) & 0xFF) as u32).saturating_sub(100);
pixels[(y * W + x) as usize] = (0xFF << 24) | (r << 16) | (g << 8) | b;
}
}
libc::munmap(p, SIZE as usize);
Ok(OwnedFd::from_raw_fd(fd))
}
fn run() -> Result<(), Box<dyn std::error::Error>> {
dlog("[client] connect to compositor");
// Attendre que le socket existe (compositor démarré)
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<ClientState> = conn.new_event_queue();
let qh = event_queue.handle();
let _registry = conn.display().get_registry(&qh, ());
let mut state = ClientState::default();
event_queue.roundtrip(&mut state)?;
dlog(&format!(
"[client] globals : compositor={} shm={}",
state.compositor.is_some(),
state.shm.is_some()
));
let compositor = state
.compositor
.clone()
.ok_or("no wl_compositor global")?;
let shm = state.shm.clone().ok_or("no wl_shm global")?;
// Crée shm + pattern
let fd = unsafe { create_shm_with_pattern("/redox-wl-client-shm") }?;
dlog(&format!("[client] shm créé, peint {}x{} ARGB", W, H));
// wl_shm_pool + wl_buffer
let pool = shm.create_pool(fd.as_fd(), SIZE, &qh, ());
let buffer = pool.create_buffer(0, W, H, STRIDE, wayland_client::protocol::wl_shm::Format::Argb8888, &qh, ());
// wl_surface
let surface = compositor.create_surface(&qh, ());
surface.attach(Some(&buffer), 0, 0);
surface.damage_buffer(0, 0, W, H);
surface.commit();
dlog("[client] surface attach + damage + commit envoyés");
event_queue.flush()?;
let _ = event_queue.roundtrip(&mut state);
// Reste connecté ~25s pour qu'on puisse capturer
let start = std::time::Instant::now();
while start.elapsed() < Duration::from_secs(25) {
let _ = event_queue.dispatch_pending(&mut state);
let _ = event_queue.flush();
thread::sleep(Duration::from_millis(50));
}
dlog("[client] done, exit propre");
let _ = surface; // tag explicite pour rappeler que la surface vit jusqu'à la fin
let _ = buffer;
let _ = pool;
Ok(())
}
fn main() -> ExitCode {
match run() {
Ok(()) => {
dlog("[client] PASS");
ExitCode::SUCCESS
}
Err(e) => {
dlog(&format!("[client] FAIL: {e}"));
ExitCode::FAILURE
}
}
}