274 lines
11 KiB
Markdown
274 lines
11 KiB
Markdown
|
|
# 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
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
É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
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
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.*
|