Phase 13.1 — port simple_window comme premier client tiers

Crate redox-wl-real-client-simple-window : port Redox de l'exemple
upstream wayland-rs/wayland-client/examples/simple_window.rs.
Premier client que nous n'avons PAS écrit pour valider notre compositor,
donc révélateur des paths protocolaires non anticipés.

Code upstream préservé verbatim sauf 4 adaptations Redox justifiées
dans le doc :
- Connection::connect_to_env() → UnixStream::connect(SOCKET_PATH)
  car WAYLAND_DISPLAY pas garanti dans l'init Redox.
- tempfile::tempfile() → libc::shm_open + ftruncate + mmap car
  O_TMPFILE/mkostemp non garantis par relibc.
- Attente initiale 50×100ms sur l'existence du socket (lancement
  parallèle compositor + client dans init script).
- Logs tee stdout + /scheme/debug pour observer côté serial host.

L'algorithme draw() gradient ARGB, les Dispatch impls, le delegate_noop!,
init_xdg_surface, Close/ESC sont conservés ligne pour ligne. Tout bug
observé sera donc attribuable au compositor, pas au port.

Cross-compile via `redoxer build --release` : 1.1 Mo, ELF statique,
0 warning, 0 erreur (testé 2026-05-15).

Doc phase13-1-real-client-simple-window.md : justifications des 4
adaptations, procédure runtime make qemu, 6 critères d'observation
pour la 13.1.b runtime à venir.

Runtime non validé (exige make qemu interactif) — sera consigné en
phase 13.1.b après test utilisateur.

Leyoda 2026 – GPLv3
This commit is contained in:
Votre Nom 2026-05-15 08:15:20 +02:00
parent 8795f39f08
commit 530d022840
3 changed files with 577 additions and 0 deletions

View file

@ -0,0 +1,11 @@
[package]
name = "redox-wl-real-client-simple-window"
version = "0.1.0"
edition = "2021"
description = "Port Redox du client wayland-rs/examples/simple_window.rs — premier client tiers (Phase 13.1)"
[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"

View file

@ -0,0 +1,370 @@
//! Phase 13.1 — Port Redox de l'exemple upstream
//! `wayland-rs/wayland-client/examples/simple_window.rs`.
//!
//! Premier client **tiers** (= que nous n'avons pas écrit pour
//! satisfaire notre compositor). Objectif : faire émerger les manques
//! réels du compositor en exerçant les chemins protocolaires que nos
//! propres clients de test ne couvrent pas exhaustivement.
//!
//! Différences vs upstream (et seulement ces différences) :
//!
//! - `Connection::connect_to_env()` → `UnixStream::connect(SOCKET_PATH)`
//! + `Backend::connect(stream)` parce que `WAYLAND_DISPLAY` n'est pas
//! toujours posé dans l'environnement init Redox.
//! - `tempfile::tempfile()` → `libc::shm_open` + `ftruncate` + `mmap`
//! parce que `tempfile` dépend de `O_TMPFILE`/`mkostemp` non garantis
//! par relibc.
//! - Petite attente initiale sur l'existence du socket pour permettre
//! au compositor de démarrer en parallèle dans le même init script.
//! - Sortie sur `/scheme/debug` en plus de stdout (serial console host).
//!
//! Tout le reste — pattern Dispatch, `delegate_noop!`, `init_xdg_surface`,
//! le dessin (gradient ARGB), la gestion de Close/ESC — est conservé
//! verbatim depuis l'upstream pour bien isoler les manques du compositor
//! du code du client.
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::{
Dispatch, QueueHandle, WEnum, delegate_noop,
backend::Backend,
protocol::{
wl_buffer, wl_compositor, wl_keyboard, wl_registry, wl_seat, wl_shm, wl_shm_pool,
wl_surface,
},
Connection,
};
use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base};
const SOCKET_PATH: &str = "/tmp/redox-wl-comp.sock";
// ---------- Logging tee stdout + /scheme/debug ----------
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);
}
// ---------- Buffer SHM (équivalent de tempfile + draw upstream) ----------
const INIT_W: u32 = 320;
const INIT_H: u32 = 240;
const PIXEL_BYTES: usize = 4;
const BUFFER_SIZE: usize = (INIT_W as usize) * (INIT_H as usize) * PIXEL_BYTES;
/// Équivalent du `draw()` upstream + création shm.
/// Garde le même algorithme de gradient pour que le rendu visuel
/// matche le screenshot upstream.
unsafe fn create_shm_buffer() -> Result<OwnedFd, String> {
let cname = CString::new("/redox-wl-real-client-simple-window").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, BUFFER_SIZE as _) != 0 {
libc::close(fd);
return Err("ftruncate failed".into());
}
let p = libc::mmap(
ptr::null_mut(),
BUFFER_SIZE,
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 u8, BUFFER_SIZE);
// Reproduction verbatim du draw() upstream : gradient ARGB.
// Layout = [B, G, R, A] little-endian (Argb8888 Wayland).
use std::cmp::min;
let buf_x = INIT_W;
let buf_y = INIT_H;
let mut idx = 0usize;
for y in 0..buf_y {
for x in 0..buf_x {
let a: u32 = 0xFF;
let r = min(((buf_x - x) * 0xFF) / buf_x, ((buf_y - y) * 0xFF) / buf_y);
let g = min((x * 0xFF) / buf_x, ((buf_y - y) * 0xFF) / buf_y);
let b = min(((buf_x - x) * 0xFF) / buf_x, (y * 0xFF) / buf_y);
pixels[idx] = b as u8;
pixels[idx + 1] = g as u8;
pixels[idx + 2] = r as u8;
pixels[idx + 3] = a as u8;
idx += 4;
}
}
libc::munmap(p, BUFFER_SIZE);
Ok(OwnedFd::from_raw_fd(fd))
}
// ---------- État client + Dispatch impls (verbatim upstream) ----------
struct State {
running: bool,
base_surface: Option<wl_surface::WlSurface>,
buffer: Option<wl_buffer::WlBuffer>,
wm_base: Option<xdg_wm_base::XdgWmBase>,
xdg_surface: Option<(xdg_surface::XdgSurface, xdg_toplevel::XdgToplevel)>,
configured: bool,
}
impl Dispatch<wl_registry::WlRegistry, ()> for State {
fn event(
state: &mut Self,
registry: &wl_registry::WlRegistry,
event: wl_registry::Event,
_: &(),
_: &Connection,
qh: &QueueHandle<Self>,
) {
if let wl_registry::Event::Global { name, interface, .. } = event {
match &interface[..] {
"wl_compositor" => {
let compositor =
registry.bind::<wl_compositor::WlCompositor, _, _>(name, 1, qh, ());
let surface = compositor.create_surface(qh, ());
state.base_surface = Some(surface);
if state.wm_base.is_some() && state.xdg_surface.is_none() {
state.init_xdg_surface(qh);
}
}
"wl_shm" => {
let shm = registry.bind::<wl_shm::WlShm, _, _>(name, 1, qh, ());
let fd = unsafe { create_shm_buffer() }.unwrap_or_else(|e| {
dlog(&format!("[real-client] create_shm_buffer FAIL: {e}"));
std::process::exit(2);
});
let pool = shm.create_pool(fd.as_fd(), BUFFER_SIZE as i32, qh, ());
let buffer = pool.create_buffer(
0,
INIT_W as i32,
INIT_H as i32,
(INIT_W as i32) * 4,
wl_shm::Format::Argb8888,
qh,
(),
);
state.buffer = Some(buffer.clone());
if state.configured {
let surface = state.base_surface.as_ref().unwrap();
surface.attach(Some(&buffer), 0, 0);
surface.commit();
}
}
"wl_seat" => {
registry.bind::<wl_seat::WlSeat, _, _>(name, 1, qh, ());
}
"xdg_wm_base" => {
let wm_base = registry.bind::<xdg_wm_base::XdgWmBase, _, _>(name, 1, qh, ());
state.wm_base = Some(wm_base);
if state.base_surface.is_some() && state.xdg_surface.is_none() {
state.init_xdg_surface(qh);
}
}
_ => {}
}
}
}
}
delegate_noop!(State: ignore wl_compositor::WlCompositor);
delegate_noop!(State: ignore wl_surface::WlSurface);
delegate_noop!(State: ignore wl_shm::WlShm);
delegate_noop!(State: ignore wl_shm_pool::WlShmPool);
delegate_noop!(State: ignore wl_buffer::WlBuffer);
impl State {
fn init_xdg_surface(&mut self, qh: &QueueHandle<State>) {
let wm_base = self.wm_base.as_ref().unwrap();
let base_surface = self.base_surface.as_ref().unwrap();
let xdg_surface = wm_base.get_xdg_surface(base_surface, qh, ());
let toplevel = xdg_surface.get_toplevel(qh, ());
toplevel.set_title("A fantastic window!".into());
base_surface.commit();
self.xdg_surface = Some((xdg_surface, toplevel));
}
}
impl Dispatch<xdg_wm_base::XdgWmBase, ()> for State {
fn event(
_: &mut Self,
wm_base: &xdg_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<xdg_surface::XdgSurface, ()> for State {
fn event(
state: &mut Self,
xdg_surface: &xdg_surface::XdgSurface,
event: xdg_surface::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
if let xdg_surface::Event::Configure { serial, .. } = event {
xdg_surface.ack_configure(serial);
state.configured = true;
let surface = state.base_surface.as_ref().unwrap();
if let Some(ref buffer) = state.buffer {
surface.attach(Some(buffer), 0, 0);
surface.commit();
}
}
}
}
impl Dispatch<xdg_toplevel::XdgToplevel, ()> for State {
fn event(
state: &mut Self,
_: &xdg_toplevel::XdgToplevel,
event: xdg_toplevel::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
if let xdg_toplevel::Event::Close = event {
dlog("[real-client] xdg_toplevel.close → exit");
state.running = false;
}
}
}
impl Dispatch<wl_seat::WlSeat, ()> for State {
fn event(
_: &mut Self,
seat: &wl_seat::WlSeat,
event: wl_seat::Event,
_: &(),
_: &Connection,
qh: &QueueHandle<Self>,
) {
if let wl_seat::Event::Capabilities {
capabilities: WEnum::Value(capabilities),
} = event
{
if capabilities.contains(wl_seat::Capability::Keyboard) {
seat.get_keyboard(qh, ());
}
}
}
}
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 {
// ESC scancode (evdev)
dlog("[real-client] ESC → exit");
state.running = false;
}
}
}
}
fn run() -> Result<(), Box<dyn std::error::Error>> {
dlog("[real-client] simple_window port Redox — start");
// Attente du socket compositor (init scripts lancent compositor + client
// en parallèle ; le client peut démarrer avant que le socket soit bound).
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 = conn.new_event_queue();
let qhandle = event_queue.handle();
let display = conn.display();
display.get_registry(&qhandle, ());
let mut state = State {
running: true,
base_surface: None,
buffer: None,
wm_base: None,
xdg_surface: None,
configured: false,
};
dlog("[real-client] entering event loop");
while state.running {
event_queue.blocking_dispatch(&mut state)?;
}
dlog("[real-client] loop exited cleanly");
Ok(())
}
fn main() -> ExitCode {
match run() {
Ok(()) => {
dlog("[real-client] PASS");
ExitCode::SUCCESS
}
Err(e) => {
dlog(&format!("[real-client] FAIL: {e}"));
ExitCode::FAILURE
}
}
}