redox-wayland-compositor/crates/redox-wl-compositor/src/main.rs
Votre Nom a87de02555 🎉 Phase 7.6 — multi-clients paquet B validé runtime
3 livrables :

1. Cleanup post-disconnect (corrige sub-bug 7.5)
   - DumbClientData::disconnected push dans Arc<Mutex<Vec<ClientId>>>
     partagé (peuplé à accept_pending_clients)
   - SurfaceData.client_id: Mutex<Option<ClientId>> capturé au
     wl_compositor.create_surface pendant que _client: &Client est
     encore vivant (à la déconnexion surf.client() retourne None,
     on ne pourrait plus déduire le mapping)
   - WaylandFrontend.garbage_collect_dead_clients drain la queue
     et nettoie surfaces_by_id + registry + focused_surface +
     cursor_surface_id + pointers/keyboards orphelins
   - Appelée à chaque tick depuis le compositor binaire après
     dispatch_clients

2. wl_buffer.release après commit-copy
   - SurfaceData.pending_buffer passé de Option<BufferData> à
     Option<wl_buffer::WlBuffer> pour avoir le Resource sous la main
   - Au commit, après la lecture des params via
     buf.data::<BufferData>().cloned() et la copie des pixels,
     appel buf.release() qui signale au client qu'il peut réutiliser
     son buffer

3. Filtrage events par client focused
   - forward_input calcule focused_client_id depuis
     focused_surface.client().map(|c| c.id())
   - wl_pointer.{motion,button,axis,frame} et wl_keyboard.key
     n'arrivent qu'aux Resources dont client_id matche le focused
   - PointerButton recalcule focused_cid APRÈS le hit_test+set_focus
     pour que le clic atterrisse bien sur le nouveau client

Pièges trouvés :
- Resource n'a pas de client_id() direct → utiliser
  client().map(|c| c.id())
- À l'instant du disconnected(), surf.client() retourne déjà None
  → capturer le ClientId au CreateSurface, pas après

Validation runtime :
- Test fuzz : surface fantôme du fuzz1 (brutal exit) nettoyée,
  surfaces=0 stable post-fuzz, capture phase7-6-cleanup-no-ghost.png
  confirme visuellement (vs rectangle noir 7.5)
- Test 2 clients : redox-wl-test-client-shm-two avec parent + fork
  affiche A vert + B magenta en parallèle, surfaces=2 stable,
  capture phase7-6-two-clients.png
- Log frontend : [frontend] garbage_collect: client X → destroyed
  1 surfaces (fuzz1), 0 surfaces (fuzz2-4 qui ont cleanup propre)

Doc complète : docs/phase7-6-multi-clients.md

Leyoda 2026 – GPLv3
2026-05-13 18:51:33 +02:00

207 lines
6.7 KiB
Rust

//! 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)?;
// Phase 7.3 : curseur visible dès le démarrage, placé au centre du fb.
frontend.set_cursor_initial_position((fb_w as i32) / 2, (fb_h as i32) / 2);
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(180);
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}"));
}
// 2.5. Phase 7.6 : nettoyer les surfaces des clients déconnectés.
// Sans ça les surfaces persistent après un close socket brutal
// (sub-bug 7.5).
frontend.garbage_collect_dead_clients();
// 3. Input
if let Ok(events) = input.poll() {
if !events.is_empty() {
dlog(&format!("[comp] {} input events from inputd", events.len()));
}
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(());
}
_ => {}
}
// Phase 7.2 : forward tous les events vers les clients
// Wayland (keyboard.key, pointer.button, etc.)
frontend.forward_input(&ev);
}
}
// 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);
// Phase 7.3 : curseur software par-dessus la composition.
frontend.draw_cursor(&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 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
}
}
}