Initial commit: phases 1-3 du portage Wayland Rust pour Redox OS

Plan directeur 14 phases / 5 ans (REDOX_COSMIC_XWAYLAND_RS_PLAN.md).

Phase 1 — Audit Redox (docs/existing-redox-gui.md, 486 lignes) :
- Orbital, graphics-ipc (API DRM compatible Linux subset KMS), inputd, vesad
- relibc support : AF_UNIX, SCM_RIGHTS, shm_open, mmap, poll
- 3 manques identifiés : memfd_create, keymap XKB, AT-SPI

Phase 2 — Validation primitives sur Redox via redoxer (5 tests + 1 POC) :
- test-unix-socket : SOCK_STREAM Wayland-shaped roundtrip
- test-fd-passing : SCM_RIGHTS mono-process (artefact kernel)
- test-fd-passing-fork : SCM_RIGHTS multi-process (validation Wayland critique)
- test-shm-open : shm_open + mmap + persistance + unlink
- test-poll-multifd : poll() multiplexing + POLLHUP
- poc-pixels : datapath shm + SCM_RIGHTS bout en bout (10000 pixels ARGB)

Phase 3 — wayland-rs sur Redox (compile + runtime) :
- wayland-{scanner,backend,server,client} compilent pour x86_64-unknown-redox
  sans patch upstream (rustix supporte Redox via libc backend)
- test-handshake : server/client wl_registry handshake roundtrip
- test-shm-pipeline : pipeline complet (ListeningSocket Unix réel + fd passing
  via wl_shm.create_pool + wl_shm_pool + wl_buffer + wl_surface + commit +
  serveur lit pixels via fd reçu, validation pixel-perfect)

Verdict phase 3 : wayland-rs upstream est viable sur Redox out-of-the-box,
le port "Wayland sur Redox" est désormais un problème de compositor à écrire,
pas de stack à porter.

Prérequis build : redoxer (pas cargo direct, car CMSG_NXTHDR/CMSG_DATA
ne sont pas linkés autrement vers librelibc.a).

Leyoda 2026 – GPLv3
This commit is contained in:
Votre Nom 2026-05-08 17:41:55 +02:00
commit 53e6626231
21 changed files with 3676 additions and 0 deletions

View file

@ -0,0 +1,589 @@
//! Test 6 — Pipeline complet wayland-rs sur Redox.
//!
//! C'est ce que fait un compositor Wayland minimal :
//! - serveur expose un socket Unix (ListeningSocket)
//! - client se connecte via WAYLAND_SOCKET (vrai pattern Wayland)
//! - client crée wl_shm_pool depuis un fd shm
//! - client crée wl_buffer depuis le pool, peint un pattern ARGB
//! - client crée wl_surface, attach, damage, commit
//! - serveur reçoit le commit, mappe le buffer fd, lit les pixels, vérifie
//!
//! Si ce test passe, le pipeline shm Wayland complet fonctionne sur Redox.
//! C'est la preuve technique que la phase 4-7 du plan directeur (display
//! backend + input + surfaces shm + compositor minimal) est réalisable
//! sans surprise majeure.
use std::ffi::CString;
use std::io;
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, IntoRawFd, OwnedFd};
use std::os::unix::net::{UnixListener, UnixStream};
use std::process::ExitCode;
use std::ptr;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use wayland_client::{
Connection as ClientConnection, Dispatch as ClientDispatch, EventQueue, Proxy as _,
QueueHandle as ClientQueueHandle,
backend::Backend as ClientBackend,
protocol::{
wl_buffer::WlBuffer as ClientBuffer, wl_compositor::WlCompositor as ClientCompositor,
wl_registry, wl_shm::WlShm as ClientShm, wl_shm_pool::WlShmPool as ClientShmPool,
wl_surface::WlSurface as ClientSurface,
},
};
use wayland_server::{
Client, DataInit, Display as ServerDisplay, DisplayHandle, GlobalDispatch,
backend::ClientData,
protocol::{
wl_buffer, wl_callback, wl_compositor, wl_shm, wl_shm_pool, wl_surface,
},
};
const W: i32 = 100;
const H: i32 = 100;
const STRIDE: i32 = W * 4;
const POOL_SIZE: i32 = STRIDE * H;
const SOCKET_NAME: &str = "wayland-test-06";
fn expected_pixel(x: i32, y: i32) -> u32 {
let a: u32 = 0xFF;
let r: u32 = (x as u32) & 0xFF;
let g: u32 = (y as u32) & 0xFF;
let b: u32 = ((x + y) as u32) & 0xFF;
(a << 24) | (r << 16) | (g << 8) | b
}
fn errno_str() -> String {
let e = io::Error::last_os_error();
format!("{e} (errno={})", e.raw_os_error().unwrap_or(0))
}
// =====================================================================
// Server side
// =====================================================================
#[derive(Default)]
struct ServerObservation {
surface_created: bool,
pool_created_with_fd: Option<i32>,
buffer_created: Option<(i32, i32)>, // (w, h)
surface_attached: bool,
surface_committed: bool,
/// Set by main loop after reading pixels successfully
pixels_verified: bool,
}
struct ServerState {
obs: Arc<Mutex<ServerObservation>>,
}
#[derive(Debug)]
struct DumbClientData;
impl ClientData for DumbClientData {}
// ---- wl_compositor ----
impl GlobalDispatch<wl_compositor::WlCompositor, ()> for ServerState {
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 ServerState {
fn request(
state: &mut Self,
_client: &Client,
_resource: &wl_compositor::WlCompositor,
request: wl_compositor::Request,
_data: &(),
_dh: &DisplayHandle,
data_init: &mut DataInit<'_, Self>,
) {
if let wl_compositor::Request::CreateSurface { id } = request {
data_init.init(id, ());
state.obs.lock().unwrap().surface_created = true;
eprintln!("[server] wl_compositor.create_surface");
}
}
}
// ---- wl_shm ----
impl GlobalDispatch<wl_shm::WlShm, ()> for ServerState {
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, ());
// Advertise the format
shm.format(wl_shm::Format::Argb8888);
}
}
impl wayland_server::Dispatch<wl_shm::WlShm, ()> for ServerState {
fn request(
state: &mut Self,
_client: &Client,
_resource: &wl_shm::WlShm,
request: wl_shm::Request,
_data: &(),
_dh: &DisplayHandle,
data_init: &mut DataInit<'_, Self>,
) {
if let wl_shm::Request::CreatePool { id, fd, size } = request {
let raw_fd = fd.as_raw_fd();
// Dup the fd so we keep it after the OwnedFd in the request is dropped
let our_fd = unsafe { libc::dup(raw_fd) };
state.obs.lock().unwrap().pool_created_with_fd = Some(our_fd);
data_init.init(id, PoolData { fd: our_fd, size });
eprintln!("[server] wl_shm.create_pool fd={raw_fd} size={size} (server dup={our_fd})");
}
}
}
// ---- wl_shm_pool ----
struct PoolData {
fd: i32,
size: i32,
}
impl wayland_server::Dispatch<wl_shm_pool::WlShmPool, PoolData> for ServerState {
fn request(
state: &mut Self,
_client: &Client,
_resource: &wl_shm_pool::WlShmPool,
request: wl_shm_pool::Request,
_data: &PoolData,
_dh: &DisplayHandle,
data_init: &mut DataInit<'_, Self>,
) {
if let wl_shm_pool::Request::CreateBuffer {
id,
offset,
width,
height,
stride,
format,
} = request
{
data_init.init(id, ());
state.obs.lock().unwrap().buffer_created = Some((width, height));
eprintln!(
"[server] wl_shm_pool.create_buffer offset={offset} {width}x{height} stride={stride} format={format:?}"
);
}
}
}
// ---- wl_buffer ----
impl wayland_server::Dispatch<wl_buffer::WlBuffer, ()> for ServerState {
fn request(
_state: &mut Self,
_client: &Client,
_resource: &wl_buffer::WlBuffer,
_request: wl_buffer::Request,
_data: &(),
_dh: &DisplayHandle,
_data_init: &mut DataInit<'_, Self>,
) {
}
}
// ---- wl_surface ----
impl wayland_server::Dispatch<wl_surface::WlSurface, ()> for ServerState {
fn request(
state: &mut Self,
_client: &Client,
_resource: &wl_surface::WlSurface,
request: wl_surface::Request,
_data: &(),
_dh: &DisplayHandle,
_data_init: &mut DataInit<'_, Self>,
) {
match request {
wl_surface::Request::Attach { .. } => {
state.obs.lock().unwrap().surface_attached = true;
eprintln!("[server] wl_surface.attach");
}
wl_surface::Request::Damage { .. }
| wl_surface::Request::DamageBuffer { .. } => {
eprintln!("[server] wl_surface.damage");
}
wl_surface::Request::Commit => {
state.obs.lock().unwrap().surface_committed = true;
eprintln!("[server] wl_surface.commit");
}
_ => {}
}
}
}
impl wayland_server::Dispatch<wl_callback::WlCallback, ()> for ServerState {
fn request(
_state: &mut Self,
_client: &Client,
_resource: &wl_callback::WlCallback,
_request: wl_callback::Request,
_data: &(),
_dh: &DisplayHandle,
_data_init: &mut DataInit<'_, Self>,
) {
}
}
// =====================================================================
// Client side
// =====================================================================
#[derive(Default)]
struct ClientState {
compositor: Option<ClientCompositor>,
shm: Option<ClientShm>,
}
impl ClientDispatch<wl_registry::WlRegistry, ()> for ClientState {
fn event(
state: &mut Self,
registry: &wl_registry::WlRegistry,
event: wl_registry::Event,
_data: &(),
_conn: &ClientConnection,
qh: &ClientQueueHandle<Self>,
) {
if let wl_registry::Event::Global { name, interface, version } = event {
eprintln!("[client] global #{name}: {interface} v{version}");
match interface.as_str() {
"wl_compositor" => {
state.compositor = Some(registry.bind(name, version, qh, ()));
}
"wl_shm" => {
state.shm = Some(registry.bind(name, version, qh, ()));
}
_ => {}
}
}
}
}
macro_rules! noop_dispatch {
($iface:ty) => {
impl ClientDispatch<$iface, ()> for ClientState {
fn event(
_state: &mut Self,
_r: &$iface,
_event: <$iface as Proxy>::Event,
_data: &(),
_conn: &ClientConnection,
_qh: &ClientQueueHandle<Self>,
) {
}
}
};
}
use wayland_client::Proxy;
noop_dispatch!(ClientCompositor);
noop_dispatch!(ClientShm);
noop_dispatch!(ClientShmPool);
noop_dispatch!(ClientBuffer);
noop_dispatch!(ClientSurface);
// =====================================================================
// Process orchestration
// =====================================================================
unsafe fn create_shm_buffer(name: &str, size: usize) -> Result<(OwnedFd, *mut u8), 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_str()));
}
if libc::ftruncate(fd, size as _) != 0 {
libc::close(fd);
return Err(format!("ftruncate: {}", errno_str()));
}
let p = libc::mmap(
ptr::null_mut(),
size,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_SHARED,
fd,
0,
);
if p == libc::MAP_FAILED {
libc::close(fd);
return Err(format!("mmap: {}", errno_str()));
}
Ok((unsafe { OwnedFd::from_raw_fd(fd) }, p as *mut u8))
}
use std::os::fd::FromRawFd;
unsafe fn child_main(server_path: &str) -> ! {
eprintln!("[client pid={}] connecting to {server_path}", libc::getpid());
// Connect to the server socket
let stream = match UnixStream::connect(server_path) {
Ok(s) => s,
Err(e) => {
eprintln!("[client] connect: {e}");
libc::_exit(2);
}
};
let fd = stream.into_raw_fd();
// wayland-client connect_to_env reads WAYLAND_SOCKET
std::env::set_var("WAYLAND_SOCKET", fd.to_string());
let conn = match ClientConnection::connect_to_env() {
Ok(c) => c,
Err(e) => {
eprintln!("[client] connect_to_env: {e}");
libc::_exit(3);
}
};
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();
// Roundtrip to receive globals
if let Err(e) = event_queue.roundtrip(&mut state) {
eprintln!("[client] roundtrip 1: {e}");
libc::_exit(4);
}
if state.compositor.is_none() || state.shm.is_none() {
eprintln!("[client] missing compositor/shm globals");
libc::_exit(5);
}
eprintln!("[client] got compositor + shm globals");
// Create the shm buffer
let (shm_fd, map) = match create_shm_buffer("/wl-test-06-buffer", POOL_SIZE as usize) {
Ok(v) => v,
Err(e) => {
eprintln!("[client] shm: {e}");
libc::_exit(6);
}
};
// Paint pattern
let pixels = std::slice::from_raw_parts_mut(map as *mut u32, (W * H) as usize);
for y in 0..H {
for x in 0..W {
pixels[(y * W + x) as usize] = expected_pixel(x, y);
}
}
eprintln!("[client] painted {}x{} ARGB pattern in shm", W, H);
// Create wl_shm_pool from the fd
let shm = state.shm.clone().unwrap();
let pool = shm.create_pool(shm_fd.as_fd(), POOL_SIZE, &qh, ());
// Create wl_buffer
let buffer = pool.create_buffer(
0, W, H, STRIDE,
wayland_client::protocol::wl_shm::Format::Argb8888,
&qh, (),
);
// Create wl_surface
let compositor = state.compositor.clone().unwrap();
let surface = compositor.create_surface(&qh, ());
// attach, damage, commit
surface.attach(Some(&buffer), 0, 0);
surface.damage_buffer(0, 0, W, H);
surface.commit();
eprintln!("[client] attached + damaged + committed");
// Flush all pending writes
if let Err(e) = event_queue.flush() {
eprintln!("[client] flush: {e}");
libc::_exit(7);
}
// Roundtrip to ensure server has processed everything
let _ = event_queue.roundtrip(&mut state);
eprintln!("[client] done, exit 0");
libc::_exit(0);
}
fn run() -> Result<(), String> {
println!("[test-06] Wayland shm pipeline on Redox");
// Pick a socket path. We won't use bind_auto because XDG_RUNTIME_DIR
// is not guaranteed inside the redoxer mini-VM.
let socket_path = format!("/tmp/{SOCKET_NAME}.sock");
let _ = std::fs::remove_file(&socket_path);
let _ = std::fs::remove_file(format!("/tmp/{SOCKET_NAME}.sock.lock"));
let listener = UnixListener::bind(&socket_path)
.map_err(|e| format!("UnixListener::bind({socket_path}): {e}"))?;
listener
.set_nonblocking(true)
.map_err(|e| format!("set_nonblocking: {e}"))?;
println!("[server] listening on {socket_path}");
let obs = Arc::new(Mutex::new(ServerObservation::default()));
let mut display: ServerDisplay<ServerState> = ServerDisplay::new()
.map_err(|e| format!("Display::new: {e:?}"))?;
let mut dh = display.handle();
dh.create_global::<ServerState, wl_compositor::WlCompositor, _>(5, ());
dh.create_global::<ServerState, wl_shm::WlShm, _>(1, ());
// Fork the client
let pid = unsafe { libc::fork() };
if pid < 0 {
return Err(format!("fork: {}", errno_str()));
}
if pid == 0 {
unsafe { child_main(&socket_path) };
}
let mut state = ServerState { obs: obs.clone() };
let start = Instant::now();
let timeout = Duration::from_secs(5);
let mut accepted_pid: Option<libc::pid_t> = None;
while start.elapsed() < timeout {
// Accept new client
if accepted_pid.is_none() {
match listener.accept() {
Ok((stream, _addr)) => {
stream.set_nonblocking(true).ok();
dh.insert_client(stream, Arc::new(DumbClientData))
.map_err(|e| format!("insert_client: {e:?}"))?;
accepted_pid = Some(pid);
println!("[server] accepted client connection");
}
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {}
Err(e) => return Err(format!("accept: {e}")),
}
}
display
.dispatch_clients(&mut state)
.map_err(|e| format!("dispatch_clients: {e:?}"))?;
display
.flush_clients()
.map_err(|e| format!("flush_clients: {e:?}"))?;
let snap = obs.lock().unwrap();
if snap.surface_committed && snap.pool_created_with_fd.is_some() {
// We have all we need to verify pixels
break;
}
drop(snap);
std::thread::sleep(Duration::from_millis(10));
}
// Capture observation state
let surface_committed;
let buffer_created;
let surface_attached;
let pool_fd;
{
let s = obs.lock().unwrap();
surface_committed = s.surface_committed;
buffer_created = s.buffer_created;
surface_attached = s.surface_attached;
pool_fd = s.pool_created_with_fd;
}
if !surface_committed {
return Err("server did not observe wl_surface.commit within 5s".into());
}
if buffer_created.is_none() {
return Err("server did not observe wl_shm_pool.create_buffer".into());
}
if !surface_attached {
return Err("server did not observe wl_surface.attach".into());
}
let pool_fd = pool_fd.ok_or("server did not receive shm fd")?;
println!("[server] all expected protocol events observed; mapping fd to verify pixels");
// Map the received fd and verify pixels
let map = unsafe {
libc::mmap(
ptr::null_mut(),
POOL_SIZE as usize,
libc::PROT_READ,
libc::MAP_SHARED,
pool_fd,
0,
)
};
if map == libc::MAP_FAILED {
return Err(format!("server mmap: {}", errno_str()));
}
let pixels = unsafe { std::slice::from_raw_parts(map as *const u32, (W * H) as usize) };
let mut mismatches: Vec<(i32, i32, u32, u32)> = Vec::new();
for y in 0..H {
for x in 0..W {
let got = pixels[(y * W + x) as usize];
let want = expected_pixel(x, y);
if got != want && mismatches.len() < 5 {
mismatches.push((x, y, got, want));
}
}
}
unsafe { libc::munmap(map, POOL_SIZE as usize) };
if !mismatches.is_empty() {
for (x, y, got, want) in &mismatches {
eprintln!("[server] mismatch at ({x},{y}): got {got:#010x} want {want:#010x}");
}
return Err(format!("{} pixel mismatches in {} total", mismatches.len(), W * H));
}
println!(
"[server] all {} pixels match expected pattern (read via fd received through Wayland protocol)",
W * H
);
// Reap child
let mut status: libc::c_int = 0;
unsafe { libc::waitpid(pid, &mut status, 0) };
let exit_code = unsafe { libc::WEXITSTATUS(status) };
if exit_code != 0 {
return Err(format!("child exited with status {exit_code}"));
}
println!("[server] child reaped cleanly");
// Cleanup
let _ = std::fs::remove_file(&socket_path);
let _ = std::fs::remove_file(format!("{socket_path}.lock"));
let cname = CString::new("/wl-test-06-buffer").unwrap();
unsafe { libc::shm_unlink(cname.as_ptr()) };
Ok(())
}
fn main() -> ExitCode {
match run() {
Ok(()) => {
println!("[test-06] PASS: full Wayland shm pipeline works on Redox");
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("[test-06] FAIL: {e}");
ExitCode::FAILURE
}
}
}