🎉 Phase 7.1 — xdg-shell minimal validé runtime

Capture preuve : docs/phase7-1-xdg-toplevel.png — fenêtre 320x240 du
client externe positionnée à (60, 60) par le compositor (cascade par
défaut), avec bandes verticales arc-en-ciel + bordure noire 2px.

Différence visuelle vs phase 6.4 : la fenêtre est maintenant
positionnée par le compositor (pas plaquée à 0,0).

Modifications redox-wl-wayland-frontend :
- + dep wayland-protocols (feature server)
- xdg_wm_base global v5
- XdgSurfaceData { wl_surface, last_serial, acked_serial }
- XdgToplevelData { title, app_id }
- SurfaceData.xdg_pending_initial_configure : bloque l'affichage tant
  que ack-configure pas reçu (sémantique xdg-shell standard)
- WaylandFrontend.next_xdg_serial : counter monotone serial
- WaylandFrontend.next_toplevel_index : cascade position +60 par fenêtre
- DEFAULT_TOPLEVEL_SIZE = 640x480 envoyé en initial configure
- Dispatch xdg_wm_base : GetXdgSurface, CreatePositioner (no-op),
  Pong (no-op), Destroy (no-op)
- Dispatch xdg_positioner + xdg_popup : no-op (out of scope)
- Dispatch xdg_surface :
  - GetToplevel → cascade pos + envoie xdg_toplevel.configure(640,480)
    + xdg_surface.configure(serial)
  - AckConfigure(serial) → enregistre + débloque affichage
  - Destroy → cache la surface
- Dispatch xdg_toplevel :
  - SetTitle / SetAppId → stocke
  - tout le reste ignoré (move, resize, maximized, ...)

redox-wl-test-client-shm adapté :
- + dep wayland-protocols (feature client)
- bind 3 globals : wl_compositor, wl_shm, xdg_wm_base
- Dispatch XdgWmBase::Event::Ping → pong (au cas où)
- Dispatch XdgSurface::Event::Configure → store serial
- Dispatch XdgToplevel::Event::Configure / Close → log
- Workflow runtime :
  1. create_surface + get_xdg_surface + get_toplevel
  2. set_title / set_app_id
  3. surface.commit (initial empty)
  4. attente Configure event (timeout 5s)
  5. ack_configure(serial)
  6. shm + buffer + attach + commit POST-ack
  7. boucle 25s
  8. destroy propre

Init submodule wayland-rs/wayland-protocols/protocols (XML files
gitlab.freedesktop.org/wayland/wayland-protocols).

Validation runtime QEMU complète :
[client] globals : compositor=true shm=true xdg_wm_base=true
[client] xdg_toplevel configure suggéré : 640x480
[client] initial configure reçu, serial=1
[client] ack_configure(1)
[client] buffer attach + damage + commit POST-ack
[comp]  tick=30..450 surfaces=1 elapsed=1.2..18.2s
PNG capturée à T+14s : surface visible à (60,60), bandes RGB+jaune+
violet+orange comme attendu.

Critère de fin 7.1 validé : client xdg-shell crée toplevel, reçoit
configure, ack, commit, affiche via shm, sans panic serveur, 18s
stable, destroy propre.

Limitations connues : focus, cursor, move/resize, multi-client,
robustesse — sous-tickets 7.2-7.7.

Image Redox restaurée à boot Orbital normal.

docs/phase7-1-xdg-shell.md : compte-rendu complet, cycle de vie,
limitations, plan 7.2.

Leyoda 2026 – GPLv3
This commit is contained in:
Votre Nom 2026-05-09 14:34:45 +02:00
parent 8a897d975d
commit 4bff319c7f
6 changed files with 600 additions and 36 deletions

View file

@ -26,14 +26,27 @@ use std::path::Path;
use std::sync::{Arc, Mutex};
use redox_wl_compositor_core::{SurfaceBuffer, SurfaceId, SurfaceRegistry};
use wayland_protocols::xdg::shell::server::{
xdg_popup, xdg_positioner, xdg_surface, xdg_toplevel, xdg_wm_base,
};
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},
protocol::{wl_buffer, wl_callback, wl_compositor, wl_region, wl_shm, wl_shm_pool, wl_surface},
};
const COMPOSITOR_VERSION: u32 = 5;
const SHM_VERSION: u32 = 1;
const XDG_WM_BASE_VERSION: u32 = 5;
/// Taille suggérée par défaut pour les nouvelles fenêtres xdg_toplevel.
/// Le client peut respecter ou non ; on utilise sa propre taille de buffer
/// quand il fait son commit.
const DEFAULT_TOPLEVEL_SIZE: (i32, i32) = (640, 480);
/// Décalage cascading entre fenêtres successives, pour qu'elles
/// n'apparaissent pas toutes à (0, 0).
const CASCADE_OFFSET: i32 = 60;
/// 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.
@ -115,6 +128,29 @@ struct SurfaceData {
pending_buffer: Mutex<Option<BufferData>>,
/// Frame callbacks en attente (à signaler après le prochain present).
pending_frame_callbacks: Mutex<Vec<wl_callback::WlCallback>>,
/// Si une xdg_surface a été créée pour cette wl_surface, true tant
/// que le premier ack_configure n'a pas été reçu. Pendant cette
/// période, les buffers attachés ne doivent pas être affichés
/// (xdg-shell spec : le rôle xdg_surface "désactive" le rendu
/// jusqu'au premier configure-ack).
xdg_pending_initial_configure: Mutex<bool>,
}
/// Données par-xdg_surface : référence à la wl_surface sous-jacente +
/// dernier serial envoyé + ack reçu.
struct XdgSurfaceData {
wl_surface: wl_surface::WlSurface,
last_serial: Mutex<u32>,
/// Le dernier serial qui a été ack par le client. Tant que c'est
/// inférieur à `last_serial`, le compositor doit ignorer les commits.
acked_serial: Mutex<u32>,
}
/// Données par-xdg_toplevel : title, app_id, ref vers son xdg_surface.
#[derive(Default)]
struct XdgToplevelData {
title: Mutex<Option<String>>,
app_id: Mutex<Option<String>>,
}
#[derive(Debug)]
@ -135,6 +171,12 @@ pub struct WaylandFrontend {
frame_callbacks: Vec<wl_callback::WlCallback>,
/// Counter monotone pour les `done` events (timestamp en ms simulé)
frame_time_ms: u32,
/// Counter monotone pour les serials xdg_surface.configure.
/// Incrémenté à chaque envoi.
next_xdg_serial: u32,
/// Counter cascading des nouvelles fenêtres xdg_toplevel : la 1re est
/// placée à (60, 60), la 2e à (120, 120), etc.
next_toplevel_index: u32,
}
impl WaylandFrontend {
@ -147,6 +189,7 @@ impl WaylandFrontend {
let mut dh = display.handle();
dh.create_global::<Self, wl_compositor::WlCompositor, _>(COMPOSITOR_VERSION, ());
dh.create_global::<Self, wl_shm::WlShm, _>(SHM_VERSION, ());
dh.create_global::<Self, xdg_wm_base::XdgWmBase, _>(XDG_WM_BASE_VERSION, ());
let listener = wayland_server::ListeningSocket::bind_absolute(socket_path.to_path_buf())?;
@ -156,6 +199,8 @@ impl WaylandFrontend {
listener,
frame_callbacks: Vec::new(),
frame_time_ms: 0,
next_xdg_serial: 1,
next_toplevel_index: 0,
})
}
@ -254,6 +299,7 @@ impl wayland_server::Dispatch<wl_compositor::WlCompositor, ()> for WaylandFronte
id: Mutex::new(Some(surface_id)),
pending_buffer: Mutex::new(None),
pending_frame_callbacks: Mutex::new(Vec::new()),
xdg_pending_initial_configure: Mutex::new(false),
};
data_init.init(id, Arc::new(data));
}
@ -424,6 +470,16 @@ impl wayland_server::Dispatch<wl_surface::WlSurface, Arc<SurfaceData>> for Wayla
Some(id) => id,
None => return,
};
// Si la surface a un rôle xdg_surface et n'a pas encore été
// ack-configure, on ignore le commit côté affichage (xdg-shell
// spec : "the surface is in an unconfigured state, the client
// can attach buffers but they are not visible").
let pending_xdg = *data.xdg_pending_initial_configure.lock().unwrap();
if pending_xdg {
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 {
@ -473,3 +529,202 @@ impl wayland_server::Dispatch<wl_callback::WlCallback, ()> for WaylandFrontend {
// wl_callback n'a pas de requests, juste l'event `done`
}
}
// =====================================================================
// xdg-shell (phase 7.1)
// =====================================================================
// ---- xdg_wm_base (global) ----
impl GlobalDispatch<xdg_wm_base::XdgWmBase, ()> for WaylandFrontend {
fn bind(
_state: &mut Self,
_handle: &DisplayHandle,
_client: &Client,
resource: wayland_server::New<xdg_wm_base::XdgWmBase>,
_data: &(),
data_init: &mut DataInit<'_, Self>,
) {
data_init.init(resource, ());
}
}
impl wayland_server::Dispatch<xdg_wm_base::XdgWmBase, ()> for WaylandFrontend {
fn request(
_state: &mut Self,
_client: &Client,
_r: &xdg_wm_base::XdgWmBase,
request: xdg_wm_base::Request,
_data: &(),
_dh: &DisplayHandle,
data_init: &mut DataInit<'_, Self>,
) {
match request {
xdg_wm_base::Request::GetXdgSurface { id, surface } => {
// Marquer la wl_surface comme ayant un rôle xdg : tant que
// le 1er ack_configure n'est pas reçu, ses commits ne sont
// pas affichés (cf wl_surface.commit ci-dessus).
if let Some(sd) = surface.data::<Arc<SurfaceData>>() {
*sd.xdg_pending_initial_configure.lock().unwrap() = true;
}
let xdg_data = Arc::new(XdgSurfaceData {
wl_surface: surface,
last_serial: Mutex::new(0),
acked_serial: Mutex::new(0),
});
data_init.init(id, xdg_data);
}
xdg_wm_base::Request::CreatePositioner { id } => {
// Pour 7.1 : positioner no-op (popups hors scope).
data_init.init(id, ());
}
xdg_wm_base::Request::Pong { .. } => {
// Réponse à un ping qu'on n'envoie jamais en 7.1 → ignore.
}
xdg_wm_base::Request::Destroy => {
// Le client se désengage. Pas d'action particulière côté
// serveur — les xdg_surface restantes seront détruites
// séparément.
}
_ => {}
}
}
}
// ---- xdg_positioner (no-op pour 7.1, popups hors scope) ----
impl wayland_server::Dispatch<xdg_positioner::XdgPositioner, ()> for WaylandFrontend {
fn request(
_state: &mut Self,
_client: &Client,
_r: &xdg_positioner::XdgPositioner,
_req: xdg_positioner::Request,
_: &(),
_dh: &DisplayHandle,
_data_init: &mut DataInit<'_, Self>,
) {
}
}
// ---- xdg_popup (no-op pour 7.1) ----
impl wayland_server::Dispatch<xdg_popup::XdgPopup, ()> for WaylandFrontend {
fn request(
_state: &mut Self,
_client: &Client,
_r: &xdg_popup::XdgPopup,
_req: xdg_popup::Request,
_: &(),
_dh: &DisplayHandle,
_data_init: &mut DataInit<'_, Self>,
) {
}
}
// ---- xdg_surface ----
impl wayland_server::Dispatch<xdg_surface::XdgSurface, Arc<XdgSurfaceData>> for WaylandFrontend {
fn request(
state: &mut Self,
_client: &Client,
resource: &xdg_surface::XdgSurface,
request: xdg_surface::Request,
data: &Arc<XdgSurfaceData>,
_dh: &DisplayHandle,
data_init: &mut DataInit<'_, Self>,
) {
match request {
xdg_surface::Request::GetToplevel { id } => {
let toplevel_data = Arc::new(XdgToplevelData::default());
let toplevel = data_init.init(id, toplevel_data);
// Position cascading pour que les fenêtres successives ne
// s'empilent pas toutes à (0, 0).
let idx = state.next_toplevel_index;
state.next_toplevel_index = state.next_toplevel_index.saturating_add(1);
let pos_x = CASCADE_OFFSET * (1 + idx as i32);
let pos_y = CASCADE_OFFSET * (1 + idx as i32);
if let Some(sd) = data.wl_surface.data::<Arc<SurfaceData>>() {
if let Some(sid) = *sd.id.lock().unwrap() {
state.registry.modify_pending(sid, |s| {
s.x = pos_x;
s.y = pos_y;
});
}
}
// Initial configure : envoyer au client une taille suggérée
// et un serial. Le client doit ack_configure puis commit
// pour que la surface devienne visible.
let serial = state.next_xdg_serial;
state.next_xdg_serial = state.next_xdg_serial.wrapping_add(1);
*data.last_serial.lock().unwrap() = serial;
toplevel.configure(
DEFAULT_TOPLEVEL_SIZE.0,
DEFAULT_TOPLEVEL_SIZE.1,
Vec::new(), // pas de states (Maximized, Activated, etc.)
);
resource.configure(serial);
}
xdg_surface::Request::GetPopup { id, .. } => {
// Hors scope 7.1 — popups non supportés. On init avec
// un () pour ne pas violer le protocole.
data_init.init(id, ());
}
xdg_surface::Request::SetWindowGeometry { .. } => {
// Stocké conceptuellement mais ignoré pour 7.1 (la geometry
// sert pour les decorations CSD que nous ne supportons pas).
}
xdg_surface::Request::AckConfigure { serial } => {
*data.acked_serial.lock().unwrap() = serial;
// Si c'était l'initial configure, débloquer l'affichage
if let Some(sd) = data.wl_surface.data::<Arc<SurfaceData>>() {
*sd.xdg_pending_initial_configure.lock().unwrap() = false;
}
}
xdg_surface::Request::Destroy => {
// Le client détruit son rôle xdg. La wl_surface peut survivre
// mais elle perd son rôle de fenêtre toplevel ; on cache la
// surface pour ne plus la dessiner.
if let Some(sd) = data.wl_surface.data::<Arc<SurfaceData>>() {
if let Some(sid) = *sd.id.lock().unwrap() {
state.registry.modify_pending(sid, |s| s.visible = false);
state.registry.commit(sid);
}
*sd.xdg_pending_initial_configure.lock().unwrap() = false;
}
}
_ => {}
}
}
}
// ---- xdg_toplevel ----
impl wayland_server::Dispatch<xdg_toplevel::XdgToplevel, Arc<XdgToplevelData>>
for WaylandFrontend
{
fn request(
_state: &mut Self,
_client: &Client,
_r: &xdg_toplevel::XdgToplevel,
request: xdg_toplevel::Request,
data: &Arc<XdgToplevelData>,
_dh: &DisplayHandle,
_data_init: &mut DataInit<'_, Self>,
) {
match request {
xdg_toplevel::Request::SetTitle { title } => {
*data.title.lock().unwrap() = Some(title);
}
xdg_toplevel::Request::SetAppId { app_id } => {
*data.app_id.lock().unwrap() = Some(app_id);
}
xdg_toplevel::Request::Destroy => {
// wl_surface destroy gérera la suppression côté registry
}
// Tout le reste ignoré en 7.1 :
// SetParent, ShowWindowMenu, Move, Resize, SetMaxSize, SetMinSize,
// SetMaximized, UnsetMaximized, SetFullscreen, UnsetFullscreen,
// SetMinimized
_ => {}
}
}
}