🎉🎉🎉 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,11 @@
[package]
name = "redox-wl-compositor"
version = "0.1.0"
edition = "2021"
description = "Compositor binaire intégrant display + input + frontend Wayland"
[dependencies]
redox-wl-display = { path = "../redox-wl-display" }
redox-wl-input = { path = "../redox-wl-input" }
redox-wl-compositor-core = { path = "../redox-wl-compositor-core" }
redox-wl-wayland-frontend = { path = "../redox-wl-wayland-frontend" }

View file

@ -0,0 +1,192 @@
//! Phase 6.4 — Compositor binaire complet.
//!
//! Boucle main d'un mini compositor Wayland :
//! 1. Ouvre RedoxOutput (display) et take CRTC
//! 2. Ouvre InputBackend partageant le ConsumerHandle
//! 3. Bind un ListeningSocket Wayland sur `/tmp/redox-wl-comp.sock`
//! 4. Loop :
//! - accept_pending_clients()
//! - dispatch_clients() (lit les requests, appelle nos Dispatch impls)
//! - poll() input → log + raise on click éventuel
//! - clear bg + compose_into(output) + present
//! - notify_frame_done() pour les wl_callback en attente
//! - flush_clients()
//! - sleep ~16ms
//!
//! Tourne 60 secondes max, exit propre.
use std::env;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::PathBuf;
use std::process::{Command, ExitCode};
use std::sync::{Mutex, OnceLock};
use std::thread;
use std::time::{Duration, Instant};
use redox_wl_compositor_core::Framebuffer;
use redox_wl_display::RedoxOutput;
use redox_wl_input::{InputBackend, InputEvent};
use redox_wl_wayland_frontend::WaylandFrontend;
const SOCKET_PATH: &str = "/tmp/redox-wl-comp.sock";
const BG_COLOR: u32 = 0xFF101820;
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);
}
fn run() -> Result<(), Box<dyn std::error::Error>> {
dlog("[comp] Phase 6.4 — compositor Wayland démarrage");
// Display
let mut output = RedoxOutput::open()?;
let our_vt = output.vt();
let fb_w = output.width();
let fb_h = output.height();
dlog(&format!("[comp] display {fb_w}x{fb_h}, VT={our_vt}"));
let _ = Command::new("inputd")
.arg("-A")
.arg(our_vt.to_string())
.status();
thread::sleep(Duration::from_millis(300));
output.take_crtc()?;
dlog("[comp] CRTC pris");
// Clear initial → fond bleu nuit pour signaler "compositor up"
{
let pixels = <RedoxOutput as Framebuffer>::pixels_mut(&mut output);
for p in pixels.iter_mut() {
*p = BG_COLOR;
}
}
output.present_with_takeover()?;
// Input
let input = InputBackend::new(output.consumer());
// Wayland frontend
let socket_path = PathBuf::from(SOCKET_PATH);
let mut frontend = WaylandFrontend::bind_absolute(&socket_path)?;
dlog(&format!("[comp] Wayland socket : {SOCKET_PATH}"));
// Exporter WAYLAND_DISPLAY pour les clients lancés par l'OS qui regarderaient
// l'env. (Notre client de test va connecter explicitement au path.)
unsafe {
env::set_var("WAYLAND_DISPLAY", SOCKET_PATH);
}
// Boucle principale
let start = Instant::now();
let total = Duration::from_secs(60);
let frame_period = Duration::from_millis(33); // ~30 fps
let mut last_frame = Instant::now();
let mut tick: u32 = 0;
while start.elapsed() < total {
tick = tick.wrapping_add(1);
// 1. Accepter nouveaux clients Wayland
if let Err(e) = frontend.accept_pending_clients() {
dlog(&format!("[comp] accept err: {e}"));
}
// 2. Dispatch des requêtes Wayland en attente
if let Err(e) = frontend.dispatch_clients() {
dlog(&format!("[comp] dispatch err: {e}"));
}
// 3. Input
if let Ok(events) = input.poll() {
for ev in events {
match ev {
InputEvent::Key {
scancode, pressed, ..
} if pressed && scancode == 0x01 => {
// Esc → exit
dlog("[comp] Esc → exit");
let _ = frontend.flush_clients();
let _ = std::fs::remove_file(SOCKET_PATH);
return Ok(());
}
InputEvent::Quit => {
dlog("[comp] Quit reçu");
let _ = frontend.flush_clients();
let _ = std::fs::remove_file(SOCKET_PATH);
return Ok(());
}
_ => {}
}
}
}
// 4. Render
let nb = frontend.registry.len();
// Recompose tout à chaque frame pour 6.4 (pas de damage tracking)
{
let pixels = <RedoxOutput as Framebuffer>::pixels_mut(&mut output);
for p in pixels.iter_mut() {
*p = BG_COLOR;
}
}
frontend.registry.compose_into(&mut output);
if let Err(e) = output.present_with_takeover() {
dlog(&format!("[comp] present err: {e}"));
}
// 5. Frame callbacks done après le present
let elapsed_ms = last_frame.elapsed().as_millis() as u32;
last_frame = Instant::now();
frontend.notify_frame_done(elapsed_ms);
// 6. Flush vers les clients
if let Err(e) = frontend.flush_clients() {
dlog(&format!("[comp] flush err: {e}"));
}
// Log occasionnel
if tick % 30 == 0 {
dlog(&format!(
"[comp] tick={tick} surfaces={nb} elapsed={:.1}s",
start.elapsed().as_secs_f32()
));
}
thread::sleep(frame_period);
}
dlog("[comp] timeout 60s atteint, exit");
let _ = std::fs::remove_file(SOCKET_PATH);
Ok(())
}
fn main() -> ExitCode {
match run() {
Ok(()) => {
dlog("[comp] PASS");
ExitCode::SUCCESS
}
Err(e) => {
dlog(&format!("[comp] FAIL: {e}"));
ExitCode::FAILURE
}
}
}

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

View file

@ -0,0 +1,11 @@
[package]
name = "redox-wl-wayland-frontend"
version = "0.1.0"
edition = "2021"
description = "Wayland protocol frontend that maps wl_compositor/wl_shm/wl_surface to redox-wl-compositor-core SurfaceRegistry"
[dependencies]
redox-wl-compositor-core = { path = "../redox-wl-compositor-core" }
wayland-server = { path = "../../../wayland-rs/wayland-server", default-features = false }
wayland-backend = { path = "../../../wayland-rs/wayland-backend", default-features = false }
libc = "0.2"

View file

@ -0,0 +1,475 @@
//! Phase 6.4 — Frontend Wayland.
//!
//! Implémente les protocoles Wayland minimaux nécessaires pour qu'un
//! client externe puisse créer une surface, allouer un buffer shm,
//! peindre dedans et le commiter, en se connectant au socket exposé
//! par le compositor.
//!
//! Globals exposés :
//! - `wl_compositor` v5 : `create_surface`, `create_region` (no-op)
//! - `wl_shm` v1 : advertise `Argb8888` + `Xrgb8888`
//!
//! Resources gérés :
//! - `wl_surface` : `attach`, `damage`, `damage_buffer`, `commit`,
//! `frame` (callback), `destroy`
//! - `wl_shm_pool` : `create_buffer`, `destroy`, `resize`
//! - `wl_buffer` : `destroy`, envoi de `release` après usage
//!
//! Au commit, le buffer SHM est lu et **copié** dans un `SurfaceBuffer`
//! owned du compositor-core. C'est plus simple que de garder une
//! référence vivante au mmap (qui peut être unmappé par le client à tout
//! moment). À optimiser plus tard avec des buffer attaché-non-libéré.
use std::collections::HashMap;
use std::os::fd::{AsRawFd, OwnedFd};
use std::path::Path;
use std::sync::{Arc, Mutex};
use redox_wl_compositor_core::{SurfaceBuffer, SurfaceId, SurfaceRegistry};
use wayland_server::{
Client, DataInit, Display as WlDisplay, DisplayHandle, GlobalDispatch, Resource,
backend::{ClientData, ClientId, DisconnectReason},
protocol::{wl_buffer, wl_compositor, wl_region, wl_shm, wl_shm_pool, wl_surface, wl_callback},
};
const COMPOSITOR_VERSION: u32 = 5;
const SHM_VERSION: u32 = 1;
/// Pool SHM mmap'd côté compositor. Garde le `OwnedFd` pour qu'inputd ou
/// le kernel ne libère pas la zone tant qu'on a des buffers en référence.
struct ShmPool {
fd: OwnedFd,
map: *mut u8,
size: usize,
}
unsafe impl Send for ShmPool {}
unsafe impl Sync for ShmPool {}
impl ShmPool {
unsafe fn new(fd: OwnedFd, size: i32) -> std::io::Result<Self> {
let size = size as usize;
let p = libc::mmap(
std::ptr::null_mut(),
size,
libc::PROT_READ,
libc::MAP_SHARED,
fd.as_raw_fd(),
0,
);
if p == libc::MAP_FAILED {
return Err(std::io::Error::last_os_error());
}
Ok(Self {
fd,
map: p as *mut u8,
size,
})
}
/// Lit les pixels du buffer à partir de l'offset, taille connue.
/// Pas de validation alignment ; le caller doit fournir des params
/// cohérents (offset + h * stride <= self.size, stride == w*4).
unsafe fn read_argb(&self, offset: usize, w: u32, h: u32, stride: i32) -> Vec<u32> {
let stride = stride as usize;
let n = (w as usize) * (h as usize);
let mut out = Vec::with_capacity(n);
for y in 0..h as usize {
let row_ptr = self.map.add(offset + y * stride) as *const u32;
for x in 0..w as usize {
out.push(unsafe { *row_ptr.add(x) });
}
}
out
}
}
impl Drop for ShmPool {
fn drop(&mut self) {
unsafe {
let _ = libc::munmap(self.map as *mut _, self.size);
}
// self.fd droppé ensuite
}
}
/// Données par-buffer côté serveur : référence au pool + paramètres pour relire.
#[derive(Clone)]
struct BufferData {
pool: Arc<Mutex<ShmPool>>,
offset: i32,
width: u32,
height: u32,
stride: i32,
/// Format Wayland brut (Argb8888 = 0, Xrgb8888 = 1)
format: wl_shm::Format,
}
/// Données par-surface : SurfaceId du compositor + buffer attaché en pending.
#[derive(Default)]
struct SurfaceData {
/// SurfaceId associé dans le SurfaceRegistry.
/// Initialisé par `wl_compositor.create_surface` via Mutex<Option>.
id: Mutex<Option<SurfaceId>>,
/// Buffer attaché en pending (avant commit).
pending_buffer: Mutex<Option<BufferData>>,
/// Frame callbacks en attente (à signaler après le prochain present).
pending_frame_callbacks: Mutex<Vec<wl_callback::WlCallback>>,
}
#[derive(Debug)]
struct DumbClientData;
impl ClientData for DumbClientData {
fn initialized(&self, _client_id: ClientId) {}
fn disconnected(&self, _client_id: ClientId, _reason: DisconnectReason) {}
}
/// État du frontend, qui est aussi l'état Dispatch côté wayland-server.
pub struct WaylandFrontend {
pub registry: SurfaceRegistry,
display: WlDisplay<Self>,
listener: wayland_server::ListeningSocket,
/// Frame callbacks dont le compositor doit signaler l'achèvement
/// après le prochain `present`. Renseigné par les clients via
/// `wl_surface.frame()`. Vidé par `notify_frame_done()`.
frame_callbacks: Vec<wl_callback::WlCallback>,
/// Counter monotone pour les `done` events (timestamp en ms simulé)
frame_time_ms: u32,
}
impl WaylandFrontend {
pub fn bind_absolute(socket_path: &Path) -> Result<Self, Box<dyn std::error::Error>> {
// Cleanup tout ancien socket
let _ = std::fs::remove_file(socket_path);
let _ = std::fs::remove_file(socket_path.with_extension("sock.lock"));
let mut display: WlDisplay<Self> = WlDisplay::new()?;
let mut dh = display.handle();
dh.create_global::<Self, wl_compositor::WlCompositor, _>(COMPOSITOR_VERSION, ());
dh.create_global::<Self, wl_shm::WlShm, _>(SHM_VERSION, ());
let listener = wayland_server::ListeningSocket::bind_absolute(socket_path.to_path_buf())?;
Ok(Self {
registry: SurfaceRegistry::new(),
display,
listener,
frame_callbacks: Vec::new(),
frame_time_ms: 0,
})
}
/// Accepte tous les clients en attente sur le socket.
pub fn accept_pending_clients(&mut self) -> std::io::Result<()> {
loop {
match self.listener.accept() {
Ok(Some(stream)) => {
stream.set_nonblocking(true).ok();
let _ = self
.display
.handle()
.insert_client(stream, Arc::new(DumbClientData));
}
Ok(None) => break, // pas de client en attente
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => break,
Err(e) => return Err(e),
}
}
Ok(())
}
/// Traite les requêtes en attente côté serveur. Met à jour `self.registry`
/// au passage (les Dispatch handlers ont accès à `self`).
pub fn dispatch_clients(&mut self) -> Result<(), Box<dyn std::error::Error>> {
// SAFETY/HACK: on prend Display par valeur temporairement pour libérer
// l'emprunt sur self. wayland-server::Display::dispatch_clients prend
// &mut Display<S> + &mut S, mais Display<S> EST self.display, donc
// double borrow. Solution : extract le Display, dispatch, le remettre.
// C'est exactement ce que fait notre test phase 3 — voir là-bas pour
// une référence. Ici on inline la même danse.
let mut display = std::mem::replace(&mut self.display, WlDisplay::new()?);
let res = display.dispatch_clients(self);
self.display = display;
res?;
Ok(())
}
pub fn flush_clients(&mut self) -> Result<(), Box<dyn std::error::Error>> {
self.display.flush_clients()?;
Ok(())
}
/// Le compositor doit appeler ça APRÈS chaque present complet, pour
/// signaler aux clients que leurs frame callbacks sont done.
/// Les callbacks sont signalés une seule fois et retirés.
pub fn notify_frame_done(&mut self, time_ms_delta: u32) {
self.frame_time_ms = self.frame_time_ms.wrapping_add(time_ms_delta);
for cb in self.frame_callbacks.drain(..) {
cb.done(self.frame_time_ms);
}
}
pub fn socket_path(&self) -> Option<&std::ffi::OsStr> {
self.listener.socket_name()
}
}
// =====================================================================
// Dispatch impls
// =====================================================================
// ---- wl_compositor (global) ----
impl GlobalDispatch<wl_compositor::WlCompositor, ()> for WaylandFrontend {
fn bind(
_state: &mut Self,
_handle: &DisplayHandle,
_client: &Client,
resource: wayland_server::New<wl_compositor::WlCompositor>,
_data: &(),
data_init: &mut DataInit<'_, Self>,
) {
data_init.init(resource, ());
}
}
impl wayland_server::Dispatch<wl_compositor::WlCompositor, ()> for WaylandFrontend {
fn request(
state: &mut Self,
_client: &Client,
_resource: &wl_compositor::WlCompositor,
request: wl_compositor::Request,
_data: &(),
_dh: &DisplayHandle,
data_init: &mut DataInit<'_, Self>,
) {
match request {
wl_compositor::Request::CreateSurface { id } => {
// Allouer un SurfaceId dans notre registry
let surface_id = state.registry.create();
// Marquer visible+positionable par défaut (sera commité)
state.registry.modify_pending(surface_id, |s| {
s.visible = true;
});
let data = SurfaceData {
id: Mutex::new(Some(surface_id)),
pending_buffer: Mutex::new(None),
pending_frame_callbacks: Mutex::new(Vec::new()),
};
data_init.init(id, Arc::new(data));
}
wl_compositor::Request::CreateRegion { id } => {
// Région no-op : on alloue la resource pour que le client
// ne reçoive pas un Bad Request, mais on n'utilise pas les
// régions pour l'instant (input/opaque region ignorés).
data_init.init(id, ());
}
_ => {}
}
}
}
// ---- wl_region (no-op) ----
impl wayland_server::Dispatch<wl_region::WlRegion, ()> for WaylandFrontend {
fn request(
_state: &mut Self,
_client: &Client,
_r: &wl_region::WlRegion,
_req: wl_region::Request,
_: &(),
_dh: &DisplayHandle,
_data_init: &mut DataInit<'_, Self>,
) {
}
}
// ---- wl_shm (global) ----
impl GlobalDispatch<wl_shm::WlShm, ()> for WaylandFrontend {
fn bind(
_state: &mut Self,
_handle: &DisplayHandle,
_client: &Client,
resource: wayland_server::New<wl_shm::WlShm>,
_data: &(),
data_init: &mut DataInit<'_, Self>,
) {
let shm = data_init.init(resource, ());
shm.format(wl_shm::Format::Argb8888);
shm.format(wl_shm::Format::Xrgb8888);
}
}
impl wayland_server::Dispatch<wl_shm::WlShm, ()> for WaylandFrontend {
fn request(
_state: &mut Self,
_client: &Client,
_r: &wl_shm::WlShm,
request: wl_shm::Request,
_: &(),
_dh: &DisplayHandle,
data_init: &mut DataInit<'_, Self>,
) {
if let wl_shm::Request::CreatePool { id, fd, size } = request {
// mmap immédiatement le fd
match unsafe { ShmPool::new(fd, size) } {
Ok(p) => {
data_init.init(id, Arc::new(Mutex::new(p)));
}
Err(e) => {
eprintln!("[frontend] shm CreatePool mmap failed: {e}");
// En cas d'échec on ne peut pas init le pool. Le client va
// probablement avoir un fd resource leaked, mais c'est
// moins grave qu'un crash. Implementation simple pour 6.4.
}
}
}
}
}
// ---- wl_shm_pool ----
impl wayland_server::Dispatch<wl_shm_pool::WlShmPool, Arc<Mutex<ShmPool>>> for WaylandFrontend {
fn request(
_state: &mut Self,
_client: &Client,
_r: &wl_shm_pool::WlShmPool,
request: wl_shm_pool::Request,
pool: &Arc<Mutex<ShmPool>>,
_dh: &DisplayHandle,
data_init: &mut DataInit<'_, Self>,
) {
match request {
wl_shm_pool::Request::CreateBuffer {
id,
offset,
width,
height,
stride,
format,
} => {
let format = match format.into_result() {
Ok(f) => f,
Err(_) => return, // format inconnu, on ignore
};
let bd = BufferData {
pool: Arc::clone(pool),
offset,
width: width as u32,
height: height as u32,
stride,
format,
};
data_init.init(id, bd);
}
wl_shm_pool::Request::Destroy => {}
wl_shm_pool::Request::Resize { .. } => {
// TODO: re-mmap with new size. Pour l'instant on ignore.
}
_ => {}
}
}
}
// ---- wl_buffer ----
impl wayland_server::Dispatch<wl_buffer::WlBuffer, BufferData> for WaylandFrontend {
fn request(
_state: &mut Self,
_client: &Client,
_r: &wl_buffer::WlBuffer,
_request: wl_buffer::Request,
_data: &BufferData,
_dh: &DisplayHandle,
_data_init: &mut DataInit<'_, Self>,
) {
// wl_buffer.destroy : pas grand-chose à faire, l'Arc<Mutex<ShmPool>> est
// partagé via clone côté BufferData et libéré quand toutes les références
// tombent.
}
}
// ---- wl_surface ----
impl wayland_server::Dispatch<wl_surface::WlSurface, Arc<SurfaceData>> for WaylandFrontend {
fn request(
state: &mut Self,
_client: &Client,
_r: &wl_surface::WlSurface,
request: wl_surface::Request,
data: &Arc<SurfaceData>,
_dh: &DisplayHandle,
data_init: &mut DataInit<'_, Self>,
) {
match request {
wl_surface::Request::Attach { buffer, x: _, y: _ } => {
// x/y sont le hint de placement par rapport à l'ancien buffer
// (Wayland-spec) ; pour 6.4 on ignore et on garde la position
// courante de la surface.
let bd = match buffer {
Some(buf) => match buf.data::<BufferData>() {
Some(d) => Some(d.clone()),
None => None,
},
None => None,
};
*data.pending_buffer.lock().unwrap() = bd;
}
wl_surface::Request::Damage { .. } | wl_surface::Request::DamageBuffer { .. } => {
// Damage tracking minimal pour 6.4 : on recompose tout. À
// optimiser plus tard.
}
wl_surface::Request::Frame { callback } => {
let cb = data_init.init(callback, ());
data.pending_frame_callbacks.lock().unwrap().push(cb);
}
wl_surface::Request::Commit => {
// Récupérer le SurfaceId compositor-core associé
let id = match *data.id.lock().unwrap() {
Some(id) => id,
None => return,
};
// Lire le buffer attaché (s'il y en a un)
let bd_opt = data.pending_buffer.lock().unwrap().clone();
if let Some(bd) = bd_opt {
// Lire les pixels et créer un SurfaceBuffer compositor-core
let pool = bd.pool.lock().unwrap();
let pixels = unsafe {
pool.read_argb(bd.offset as usize, bd.width, bd.height, bd.stride)
};
let sb = SurfaceBuffer::from_pixels(bd.width, bd.height, pixels);
state.registry.modify_pending(id, |s| {
s.buffer = Some(sb);
s.visible = true;
});
}
state.registry.commit(id);
// Promouvoir au top du Z-order au commit (politique simple :
// dernière surface qui commit = au-dessus). À raffiner en
// phase 7 (focus, raise on click, etc.).
state.registry.raise(id);
// Frame callbacks en attente → bump dans la queue globale
let mut cbs = data.pending_frame_callbacks.lock().unwrap();
state.frame_callbacks.append(&mut *cbs);
}
wl_surface::Request::Destroy => {
let mut id_lock = data.id.lock().unwrap();
if let Some(id) = id_lock.take() {
state.registry.destroy(id);
}
}
_ => {}
}
}
}
// ---- wl_callback (frame) ----
impl wayland_server::Dispatch<wl_callback::WlCallback, ()> for WaylandFrontend {
fn request(
_state: &mut Self,
_client: &Client,
_r: &wl_callback::WlCallback,
_req: wl_callback::Request,
_: &(),
_dh: &DisplayHandle,
_data_init: &mut DataInit<'_, Self>,
) {
// wl_callback n'a pas de requests, juste l'event `done`
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7 KiB

View file

@ -0,0 +1,171 @@
# Phase 6.4 — Frontend Wayland : un client externe affiche ses pixels
> Document produit le 2026-05-09 dans le cadre du plan directeur
> `REDOX_COSMIC_XWAYLAND_RS_PLAN.md`.
>
> **Scope** : un client Wayland externe (process séparé, code Rust pur
> via wayland-rs) se connecte au compositor binaire, lui envoie un
> buffer shm peint, et le compositor l'affiche.
## Verdict
**✅ Pipeline Wayland complet sur Redox, end-to-end, par-dessus
notre stack `compositor-core` + `redox-wl-display` + `redox-wl-input`.**
Capture preuve : ![](phase6-4-wayland-client-surface.png) — le pattern
ARGB 320x240 visible dans le coin haut-gauche est écrit par le client
externe et affiché par le compositor sur le display Redox.
## Architecture finale 6.4
```
┌──────────────────────────────────────┐ ┌─────────────────────────────┐
│ redox-wl-compositor (bin) │ │ redox-wl-test-client-shm │
│ │ │ (bin séparé) │
│ ┌────────────────┐ │ │ │
│ │ RedoxOutput │ │ │ - shm_open + pattern ARGB │
│ └────┬───────────┘ │ │ - wl_compositor │
│ │ Arc<ConsumerHandle> │ │ - wl_shm │
│ ▼ │ │ - wl_shm_pool.create │
│ ┌────────────────┐ │ │ - wl_buffer │
│ │ InputBackend │ │ │ - wl_surface.attach │
│ └────────────────┘ │ │ - wl_surface.commit │
│ │ │ │
│ ┌────────────────┐ │ └─────────────┬───────────────┘
│ │WaylandFrontend │ │ │ Unix socket
│ │ - registry │ ◄────────────────┼──────────────────┘ + SCM_RIGHTS
│ │ (compositor- │ │
│ │ core) │ │
│ │ - Display<Self>│ │
│ │ - Listener │ │
│ └────────┬───────┘ │
│ │ compose_into │
│ ▼ │
│ framebuffer Redox → écran │
└──────────────────────────────────────┘
```
## Globaux exposés
| Global | Version | Comportement |
|---|---|---|
| `wl_compositor` | 5 | `create_surface``registry.create()` ; `create_region` no-op |
| `wl_shm` | 1 | advertise `Argb8888` + `Xrgb8888` ; `create_pool(fd, size)` → mmap immédiat |
| `wl_shm_pool` | — | `create_buffer``BufferData` (offset/w/h/stride/format) ; `resize` no-op (TODO) |
| `wl_buffer` | — | (pas de request à traiter, juste destroy implicite) |
| `wl_surface` | 5 | `attach`, `damage`/`damage_buffer` (no-op tracking 6.4), `commit`, `frame`, `destroy` |
| `wl_callback` | — | utilisé pour `wl_surface.frame` ; `done` envoyé par `notify_frame_done` |
| `wl_region` | — | no-op (pas d'input region utilisée) |
## Sémantique commit Wayland implémentée
Quand un client envoie `wl_surface.commit` :
1. Récupération du `BufferData` attaché en pending (via `wl_surface.attach`)
2. Lecture des pixels du shm via `mmap` côté serveur
3. Création d'un `SurfaceBuffer` (Arc<Vec<u32>>) côté `compositor-core`
4. `registry.modify_pending(id, |s| s.buffer = Some(...))`
5. `registry.commit(id)` — pending → current
6. `registry.raise(id)` — politique simple : dernière surface commitée passe au top
7. Frame callbacks pending → queue globale, traité au `notify_frame_done` après le prochain present
**Approche "copy on commit"** : on copie les pixels du shm vers un Vec<u32>
owned. Plus simple que de garder une référence vivante au mmap qui peut
être unmappé par le client à tout moment. Coût ≈ 320×240×4 = 300 KiB par
commit pour notre client de test, négligeable.
## Validation runtime
Configuration :
```toml
# init.d/20_orbital → nowait VT=2 redox-wl-compositor
# init.d/30_console → nowait redox-wl-test-client-shm
```
Le compositor démarre, expose le socket `/tmp/redox-wl-comp.sock`, le
client a une boucle de connexion qui retry 50× × 100 ms et finit par
se connecter quand le socket apparaît.
Logs capturés via /scheme/debug → serial QEMU stdio :
```
[comp] Phase 6.4 — compositor Wayland démarrage
[comp] display 1280x800, VT=3
[comp] CRTC pris
[comp] Wayland socket : /tmp/redox-wl-comp.sock
[client] connect to compositor
[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 20s
```
## Limitations / hors scope
### Pas de placement (xdg-shell absent)
La surface du client est affichée à `(0, 0)` parce qu'aucun protocole
de placement n'est implémenté. Pour des fenêtres positionnables /
redimensionnables, il faudra `xdg_wm_base` + `xdg_toplevel` (phase 7
ou plus tard).
### Pas de damage tracking effectif
`wl_surface.damage` et `damage_buffer` sont reçus mais ignorés. Chaque
frame recompose tout. Pour 1-3 surfaces de petite taille c'est
imperceptible ; à optimiser quand on aura beaucoup de surfaces.
### Pas de wl_buffer.release explicite
Le `release` Wayland indique au client qu'il peut réutiliser un buffer.
Notre approche copy-on-commit rend ce protocole inutile (on n'a plus
besoin du shm après commit). Mais des clients sophistiqués pourraient
attendre `release` avant d'écrire à nouveau — à vérifier au cas par cas.
### Pas de seat / input vers les clients
`wl_seat`, `wl_keyboard`, `wl_pointer` ne sont pas exposés. Les events
input sont seulement consommés côté compositor (pour Esc=quit). Phase
7 ajoutera la propagation des events vers la surface focalisée.
### Pas de subcompositor
`wl_subcompositor` non exposé. Pas critique pour des clients simples.
## Code source
```
crates/redox-wl-wayland-frontend/ # lib (~430 lignes)
├── Cargo.toml
└── src/lib.rs # WaylandFrontend, Dispatch impls
crates/redox-wl-compositor/ # bin (~150 lignes)
├── Cargo.toml
└── src/main.rs # boucle main display+input+frontend
crates/redox-wl-test-client-shm/ # bin (~170 lignes)
├── Cargo.toml
└── src/main.rs # client wayland-rs qui peint + commit
```
## Suite phase 7
Si on veut un compositor utilisable au quotidien, il manque (par
ordre de priorité) :
1. **xdg-shell** (`xdg_wm_base` + `xdg_toplevel` + `xdg_surface`) →
placement, redimensionnement, fermeture propre, titres de fenêtres
2. **wl_seat + wl_keyboard + wl_pointer** → propager les events input
vers la surface focalisée. Décision XKB à prendre (porter
libxkbcommon / impl Rust pur / strings minimales).
3. **Curseur software** → afficher le pointeur souris à l'écran (le
compositor en a déjà la position via InputBackend, mais ne le
dessine pas)
4. **Gestion focus + raise on click** → utiliser hit_test +
wl_seat.keyboard_enter/leave events
5. **Damage tracking effectif** → réduire le coût de composition
6. **Clipboard** → wl_data_device_manager
7. **Multiple clients simultanés**, fermeture propre, recover sur crash client
Estimé phase 7 complète : 5-8 sessions.
---
*Fin du document de phase 6.4.*