redox-wayland-compositor/docs/phase7-6-multi-clients.md
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

273 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Phase 7.6 — Multi-clients paquet B
> Document produit le 2026-05-13 dans le cadre du plan directeur
> `REDOX_COSMIC_XWAYLAND_RS_PLAN.md`.
>
> **Scope strict (3 livrables)** :
> 1. Cleanup post-disconnect : nettoyer surfaces / pointers / keyboards
> d'un client mort, même si exit brutal sans destroy (corrige le
> sub-bug 7.5 surface fantôme).
> 2. `wl_buffer.release` envoyé par le serveur après chaque
> commit-copy (sémantique Wayland correcte : sans ça les toolkits
> sérieux finissent par bloquer faute de buffer libre).
> 3. Filtrage events pointer / keyboard par client focused (au lieu
> du broadcast 7.2 qui envoyait à tous les clients).
>
> **Hors scope 7.6** : move/resize interactifs (7.7), focus
> follows-mouse, support multi-seat, frame callbacks throttled.
## Verdict
**✅ 3 livrables validés runtime.**
### Capture 1 — cleanup post-disconnect
![](phase7-6-cleanup-no-ghost.png)
État du framebuffer 30 s après la fin du fuzz protocol binary. Le
rectangle noir 320×240 qui persistait en 7.5 (surface fantôme du
fuzz1 qui a commit + exit brutal sans destroy) a **complètement
disparu**. Curseur software toujours actif au centre, fond
compositor propre. Logs frontend :
```
[frontend] focus change: None → Some(SurfaceId(0))
[frontend] garbage_collect: client InnerClientId { id: 0, serial: 1 } → destroyed 1 surfaces
[frontend] xdg_surface.ack_configure: serial 99999 > last_sent 2, ignoring
[frontend] garbage_collect: client InnerClientId { id: 0, serial: 2 } → destroyed 0 surfaces
[frontend] wl_shm_pool.create_buffer rejected: ...
[frontend] garbage_collect: client InnerClientId { id: 0, serial: 3 } → destroyed 0 surfaces
[frontend] wl_shm_pool.create_buffer rejected: ...
[frontend] garbage_collect: client InnerClientId { id: 0, serial: 4 } → destroyed 0 surfaces
```
Le `garbage_collect` a destroyed 1 surface pour le client du fuzz1
(brutal exit sans destroy = surface orpheline), et 0 pour les
fuzz2-4 (cleanup propre via `surface.destroy` avant exit, donc
registry déjà vidée). Compositor `surfaces=0` stable pendant 30 s+.
### Capture 2 — 2 clients en parallèle
![](phase7-6-two-clients.png)
Reprise du test `redox-wl-test-client-shm-two` (parent + fork)
sous le compositor 7.6 : les 2 fenêtres A (vert, pyramide) et B
(magenta, double cercle) s'affichent correctement. `surfaces=2`
stable. Un `mouse_button 1` au curseur central est reçu par le
compositor (`[comp] 1 input events from inputd`) sans crash : hit
test ne trouve aucune surface à (640, 400), donc pas de
raise/focus change, et le button event est ignoré côté filtrage
(aucun pointer du `focused_surface` n'existe car `focused_surface`
est `None` à ce moment).
## Modifications apportées
### `redox-wl-wayland-frontend`
**`SurfaceData`** : nouveau champ
```rust
client_id: Mutex<Option<ClientId>>,
```
peuplé au `wl_compositor.create_surface` avec `Some(_client.id())`
**pendant que `_client` est encore vivant**. À la déconnexion,
`surf.client()` retourne déjà `None`, donc on ne pourrait plus
déduire le ClientId rétroactivement. C'est le piège n°1 de l'API
wayland-server (cf §pièges).
**`DumbClientData`** : nouveau champ
```rust
dead_clients: Arc<Mutex<Vec<ClientId>>>,
```
partagé entre tous les clients (cloné à `accept_pending_clients`).
La méthode `ClientData::disconnected(client_id, _reason)` push le
client_id dans cette queue (synchrone, sans tenter de modifier
l'état du frontend — wayland-server ne nous donne pas accès au
`&mut WaylandFrontend` ici).
**`WaylandFrontend::garbage_collect_dead_clients`** (nouvelle
méthode publique) : drain la queue `dead_clients` et nettoie pour
chaque ClientId mort :
- les `SurfaceId` dont `client_id` matche : `registry.destroy(sid)`
+ `surfaces_by_id.remove(sid)` + clear `focused_surface` et
`cursor_surface_id` si applicable
- les `wl_pointer` et `wl_keyboard` orphelins (filtrés via
`p.client().is_some()` — les Resources d'un client mort
retournent `None`)
Appelée à chaque tick du compositor binaire après `dispatch_clients`.
**`wl_surface.commit`** : envoie `buf.release()` après la
copy-on-commit (ou même si le buffer est invalide). Sémantique
Wayland : le serveur signale au client qu'il peut réutiliser le
buffer. Sans ça, des toolkits comme GTK4 finissent par bloquer en
attendant un buffer libre.
Refactor associé : `SurfaceData.pending_buffer` est passé de
`Option<BufferData>` à `Option<wl_buffer::WlBuffer>` (le Resource
proxy au lieu d'une copie de ses params). On lit les params via
`buf.data::<BufferData>().cloned()` au moment du commit.
**`forward_input`** : filtrage par client focused (fin du broadcast
7.2). Pour chaque type d'event, on calcule
```rust
let focused_cid = self.focused_surface
.as_ref().and_then(|s| s.client().map(|c| c.id()));
```
et on n'envoie l'event qu'aux `wl_pointer` / `wl_keyboard` dont le
`client().map(|c| c.id())` matche. Si pas de focus, aucun event
n'est envoyé aux clients (le hit_test au clic gauche peut quand
même focaliser quelqu'un avant l'envoi du button).
Pour `PointerButton`, on recalcule `focused_cid` **après** le bloc
`hit_test → raise → set_focus`, parce que le clic a pu changer
le focus (et donc le client cible).
### `redox-wl-compositor`
Une ligne ajoutée dans la boucle main :
```rust
frontend.garbage_collect_dead_clients();
```
juste après `dispatch_clients()`. Coût négligeable (1 lock + check
empty + drain si non-vide).
## Pièges trouvés
### `Resource::client_id()` n'existe pas — il faut `Resource::client().map(|c| c.id())`
Wayland-server n'a pas de méthode `client_id()` directe sur les
Resource. L'API est :
- `Resource::client(&self) -> Option<Client>` — retourne le client
propriétaire **s'il est encore alive**
- `Client::id() -> ClientId`
Donc `surf.client_id()` → erreur de compile. Correction :
`surf.client().map(|c| c.id())`.
### `surf.client()` retourne `None` dans `disconnected`
À l'instant où `ClientData::disconnected(client_id, reason)` est
appelé, wayland-server a **déjà** marqué les Resources de ce client
comme dead. Tous les `surf.client()` retournent `None`. Donc on ne
peut PAS faire le mapping inverse "ClientId → SurfaceIds" à ce
moment-là.
**Solution adoptée** : capturer le `ClientId` au moment du
`wl_compositor.create_surface` (où `_client: &Client` est passé
en paramètre, et est vivant). Stocker dans
`SurfaceData.client_id`. Le `garbage_collect` itère
`surfaces_by_id` et compare au `client_id` stocké, pas au
`surf.client()` courant.
### `nowait sh -c "..."` ne marche pas dans l'init Redox
Rappel 7.4 + 7.5 : l'init parse incorrectement les guillemets dans
une directive `nowait sh -c "..."`. Toujours utiliser un script
wrapper sur disque (chmod +x) puis `nowait /usr/bin/launch_X.sh`.
## Validation runtime
### Test 1 : cleanup fuzz1 brutal exit
```
[boot Redox + service phase76_fuzz lance compositor + fuzz binary]
[fuzz1 : create_surface + xdg_toplevel + commit + buffer + commit + process::exit(0)]
Avant 7.6 : surfaces=1 persiste pour toujours après exit du fuzz1
Après 7.6 : [frontend] garbage_collect: client X → destroyed 1 surfaces
surfaces=0 stable post-fuzz
```
Capture `phase7-6-cleanup-no-ghost.png` confirme visuellement.
### Test 2 : 2 clients en parallèle
```
[redox-wl-test-client-shm-two : parent + fork de redox-wl-test-client-shm-two]
[A : compositor surface vert pyramide à (60, 60)]
[B : compositor surface magenta cercle à (120, 120)]
[mouse_button 1 au centre → hit_test=None → button event ignoré
par le filtrage (focused_surface est None à ce moment)]
Compositor surfaces=2 stable, ticks continus, aucun crash.
```
Capture `phase7-6-two-clients.png` confirme.
### Logs frontend extraits
```
[frontend] xdg_surface.ack_configure: serial 99999 > last_sent 2, ignoring # 7.5 garde
[frontend] wl_shm_pool.create_buffer rejected: offset=0 width=0 height=0 # 7.5 garde
[frontend] wl_shm_pool.create_buffer rejected: offset=0 width=100 ... stride=10
[frontend] garbage_collect: client InnerClientId { ..., serial: 1 } → destroyed 1 surfaces
[frontend] garbage_collect: client InnerClientId { ..., serial: 2 } → destroyed 0 surfaces
[frontend] garbage_collect: client InnerClientId { ..., serial: 3 } → destroyed 0 surfaces
[frontend] garbage_collect: client InnerClientId { ..., serial: 4 } → destroyed 0 surfaces
```
## Limitations connues (à traiter en sous-tickets ultérieurs)
- **wl_buffer.release : pas de mécanisme pour ne pas envoyer plusieurs
fois sur le même buffer** : si le client attach le même wl_buffer
+ commit deux fois consécutivement sans réutiliser, on enverra
release deux fois. Wayland-server gère probablement ça via le
refcount du Resource — à confirmer en 7.7.
- **Pas de protection contre buffer attaché à plusieurs surfaces en
même temps** : la spec dit "un buffer ne doit être attaché qu'à
une seule surface à la fois". On ne vérifie pas. À durcir si on
observe des artefacts.
- **Filtrage pointer.enter/leave non concerné** : le `set_focus`
envoie enter/leave aux `wl_pointer` et `wl_keyboard` du client de
la nouvelle / ancienne focused_surface, ce qui est déjà
semi-filtré (les leave/enter visent une surface précise et donc
un client précis). Le filtrage 7.6 concerne uniquement les events
*de mouvement et de touche* hors enter/leave.
- **Frame callbacks restent globaux** : `notify_frame_done` signale
tous les callbacks en queue, peu importe le client. Reportable
7.7 si on veut throttler par client.
- **Pas de test "kill -9 du client"** : on a testé l'exit brutal
via `process::exit(0)`, mais pas via SIGKILL. Le comportement
devrait être identique (le kernel ferme les fds, wayland-server
détecte la fin du socket). À confirmer si jamais.
## Critère de fin 7.6
> Le compositor (1) nettoie automatiquement les surfaces des
> clients déconnectés (y compris brutal exit), (2) envoie
> wl_buffer.release après chaque commit-copy, (3) ne broadcast plus
> les events pointer/keyboard mais les filtre par client focused.
**✅ Validé.** Le sub-bug 7.5 (surface fantôme) est résolu, le
compositor tourne stable avec 2 clients en parallèle, les logs
frontend confirment les 3 chemins.
## Code
```
crates/redox-wl-wayland-frontend/ # ~+90 lignes
crates/redox-wl-compositor/ # +1 ligne (gc dans boucle main)
docs/phase7-6-multi-clients.md
docs/phase7-6-cleanup-no-ghost.png
docs/phase7-6-two-clients.png
```
## Suite phase 7.7
Move / resize interactifs via `xdg_toplevel`. À l'envoi de
`xdg_toplevel.Move { seat, serial }` ou
`xdg_toplevel.Resize { seat, serial, edges }` par un client en
réponse à un événement pointer (typiquement clic sur la titlebar
côté toolkit), le compositor doit :
1. Vérifier la validité du serial (correspond à un button récent)
2. Entrer en mode "interactive_move" ou "interactive_resize"
3. Sur les `PointerMotion` suivants, déplacer ou redimensionner la
surface (et envoyer `configure` avec la nouvelle taille pour
resize)
4. Sortir du mode au release du bouton
Estimé : 1-2 sessions.
---
*Fin du document de phase 7.6.*