🎉🎉🎉 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:
parent
509aae7769
commit
8a897d975d
8 changed files with 1098 additions and 0 deletions
11
crates/redox-wl-compositor/Cargo.toml
Normal file
11
crates/redox-wl-compositor/Cargo.toml
Normal 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" }
|
||||
192
crates/redox-wl-compositor/src/main.rs
Normal file
192
crates/redox-wl-compositor/src/main.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
9
crates/redox-wl-test-client-shm/Cargo.toml
Normal file
9
crates/redox-wl-test-client-shm/Cargo.toml
Normal 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"
|
||||
229
crates/redox-wl-test-client-shm/src/main.rs
Normal file
229
crates/redox-wl-test-client-shm/src/main.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
11
crates/redox-wl-wayland-frontend/Cargo.toml
Normal file
11
crates/redox-wl-wayland-frontend/Cargo.toml
Normal 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"
|
||||
475
crates/redox-wl-wayland-frontend/src/lib.rs
Normal file
475
crates/redox-wl-wayland-frontend/src/lib.rs
Normal 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`
|
||||
}
|
||||
}
|
||||
BIN
docs/phase6-4-wayland-client-surface.png
Normal file
BIN
docs/phase6-4-wayland-client-surface.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7 KiB |
171
docs/phase6-4-wayland-frontend.md
Normal file
171
docs/phase6-4-wayland-frontend.md
Normal 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 :  — 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.*
|
||||
Loading…
Add table
Add a link
Reference in a new issue