🎉🎉🎉 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,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`
}
}