# Phase 13.2 — `wl_output` v3 + `wl_subcompositor`/`wl_subsurface` > Document produit le 2026-05-16, suite de > [`phase13-1-c-cursor.md`](phase13-1-c-cursor.md). > > **Scope** : combler 2 dettes protocolaires bloquantes pour les > toolkits clients tiers modernes : > > - **`wl_output` complet** — annonce le single display avec geometry/ > mode/scale/done, en respectant le gating par version (v1/v2/v3). > Plusieurs toolkits clients (sctk, GTK, Qt) considèrent ce global > implicitement requis — son absence en 13.1.b était une bombe à > retardement. > - **`wl_subcompositor` + `wl_subsurface`** — supporte les surfaces > parent-enfant et les rend visuellement à `parent_pos + offset`. > Pré-requis pour tout client multi-surface (popups, badges, splash > screens, decorations CSD, etc.). > > **Stratégie de validation hybride** : pour chaque sprint, un test > natif (cargo test, sans QEMU) valide la couche protocolaire ; un > test runtime visuel valide le rendu effectif. Le coût de QEMU > est ainsi réduit aux validations vraiment visuelles. > > **Verdict** : ✅ **13.2 entièrement validée** — 6 commits, 5 > sprints, 2 nouveaux clients de test, 3 scripts ion shortcuts. ## Sous-phases livrées ### 13.2.a — `wl_output` v3 complet ([commit `7413745`](../crates/redox-wl-wayland-frontend/src/lib.rs)) Avant : `wl_output` n'était PAS déclaré comme global. Les clients qui le bind-eraient implicitement (sctk, GTK) se retrouvaient sans output. Après : - Global `wl_output` à `OUTPUT_VERSION = 3` - `GlobalDispatch::bind` envoie au bind, dans l'ordre : 1. `geometry(0, 0, 0_mm, 0_mm, Subpixel::Unknown, "Redox", "redox-wl-output:0", Transform::Normal)` 2. `mode(CURRENT | PREFERRED, screen_w, screen_h, 60_000_mHz)` 3. `scale(1)` — **uniquement si version bound ≥ 3** (gating conservateur) 4. `done()` — **uniquement si version bound ≥ 2** - `Dispatch::request` accepte `Release` (v3+) en no-op Taille pixel issue de `screen_w / screen_h` qui sont settés par `set_screen_size()` (introduit en 13.1.b pour clamp curseur). Refresh hardcodé à 60 Hz : notre boucle main tourne à ~30 fps mais les apps modernes ignorent largement ce champ. **Test natif** (`crates/redox-wl-test-wl-output/tests/gating_native.rs`) : serveur + client en-process via `UnixStream::pair()`, 11 assertions vérifient v1 (geometry+mode), v2 (+done), v3 (+scale). PASS le 2026-05-16. **Test runtime** (`redox-wl-test-wl-output`, runnable via `test-out`) : bind aux 3 versions et reporte PASS/FAIL côté serial. Confirme le gating sur la vraie cible Redox. ### 13.2.b.1 — `wl_subcompositor` protocole ([commit `f9c3de1`](../crates/redox-wl-wayland-frontend/src/lib.rs)) Implémentation protocole uniquement (sans rendering, repoussé à .b.2) : - Global `wl_subcompositor` à `SUBCOMPOSITOR_VERSION = 1` - `GlobalDispatch::bind` no-op (resource init) - `Dispatch::request` : - `Destroy` : no-op - `GetSubsurface { id, surface, parent }` : crée un `Arc` avec parent ref, child ref, position pending `(0,0)`, sync mode `true` - `Dispatch::request` : - `SetPosition { x, y }` : update SubsurfaceData.position - `PlaceAbove`, `PlaceBelow` : no-op (single-subsurface use-case) - `SetSync`, `SetDesync` : update SubsurfaceData.sync - `Destroy` : no-op (resource cleanup par wayland-server) **SubsurfaceData** est une nouvelle struct portée par la wl_subsurface resource via `data_init.init(id, Arc)`. **Test natif** (`subcompositor_native.rs`) : cycle complet bind → get_subsurface → set_position → set_sync/desync → destroy. Aucune erreur protocole détectée. ### 13.2.b.2 — Rendering subsurface ([commit `bba2d7b`](../crates/redox-wl-wayland-frontend/src/lib.rs)) Wire la SubsurfaceData de .b.1 au pipeline de composition : 1. Nouveau champ `subsurface_link: Mutex>>` sur `SurfaceData` 2. À `GetSubsurface`, on link la SubsurfaceData à la child SurfaceData 3. Au commit du child wl_surface : - Si `subsurface_link` est Some : récupère le parent SurfaceId via `sub_data.parent.data::>()` - Lookup la position actuelle du parent dans le registry - `modify_pending(child_id, |s| s.x = px + ox; s.y = py + oy)` avant le `commit(id)` qui applique pending → current - Skip `raise()` et `set_focus()` pour les subsurfaces (elles ne grabbent pas le focus indépendamment, et leur z-order est géré comme cascade au-dessus du parent) `compose_into` (côté `redox-wl-compositor-core`) n'est PAS modifié — il itère le z_order et dessine chaque surface à son `(x, y)` absolu. Notre prep au commit assure que le child est à la bonne position. ### 13.2.b.3 — Client de test visuel ([commit `1dab6ff`](../crates/redox-wl-test-client-subcompositor/)) Nouveau crate `redox-wl-test-client-subcompositor` qui : 1. Connecte au socket `/tmp/redox-wl-comp.sock` 2. Bind 5 globals : `wl_compositor`, `wl_shm`, `xdg_wm_base`, `wl_seat`, `wl_subcompositor` 3. Crée un parent xdg_toplevel 300×200 bleu nuit avec bordure noire 2px, ack_configure puis attach + commit buffer ARGB 4. Crée une child wl_surface, `get_subsurface(child, parent)`, `set_position(50, 50)`, `set_desync`, attach + commit buffer 60×60 rouge avec bordure noire 5. ESC fermes le client proprement (le path keyboard étant routé au parent toplevel qui a le focus auto-grant) Reprend l'event-loop tolérant Interrupted/BrokenPipe de la phase 13.1.b. ### Fix bug visuel ([commit `dfb5c66`](../crates/redox-wl-wayland-frontend/src/lib.rs)) Détecté lors du runtime QEMU 13.2.b.3. Symptôme : le carré rouge est calculé à la bonne position au commit du child, mais reste invisible. Cause : `parent_surface.commit()` final dans le client (refresh post- subsurface) appelle `raise(parent_id)` dans le compositor, plaçant le parent **au-dessus** du child dans le z_order. Le parent overdraw le child à compose_into. Z-order observé : `[child, parent]` au lieu de `[parent, child]`. **Fix** : après `raise(parent_id)`, scanner `surfaces_by_id` pour trouver toutes les subsurfaces dont `subsurface_link.parent` pointe vers cette SurfaceId, et les `raise()` juste après. Le z-order devient `[parent, child]` (avec child raised en dernier = au-dessus). Coût O(N) par raise — N est typiquement petit (1-10 toplevels). À durcir avec un index parent_id → Vec si N explose. ### 13.2.b.4 — Scripts ion shortcuts ([commit `6fe3214`](../tools/redox-scripts/)) UX : pour éviter de retaper 3 lignes à chaque test runtime dans la fenêtre QEMU graphique (où le copier-coller depuis CachyOS n'est pas disponible), 3 scripts ion prêts à l'emploi déployés dans `/usr/bin` : - **`test-sw`** : compositor + simple_window upstream (phase 13.1.b) - **`test-subc`** : compositor + subcompositor visuel (phase 13.2.b.3) - **`test-out`** : compositor + wl_output gating test (phase 13.2.a) Tous suivent le même pattern : ```ion #!/usr/bin/ion rm -f /tmp/redox-wl-comp.sock redox-wl-compositor & sleep 1 ``` run-qemu.sh copie maintenant `tools/redox-scripts/*` dans `/usr/bin` de l'image (avec `cp -p` pour préserver le bit exécutable). ## Stratégie de tests : hybride natif + runtime Innovation méthodologique de cette phase. Pour chaque sprint : - **Test natif** (cargo test, sur l'hôte CachyOS, sans QEMU) : - Spawn un thread serveur avec wayland-server + la logique de gating identique au prod code - Connecte un client wayland-client via `UnixStream::pair()` - Exercise toute la séquence Wayland - Asserts précis (versions, events reçus, ordre, etc.) - Tourne en < 6 secondes - **Test runtime** (QEMU, visuel) : - Confirme le rendu effectif sur le framebuffer Redox - Confirme l'interaction utilisateur (clic, ESC, focus, etc.) - Validation finale "tout le pipeline marche" Gain : on n'a QEMU dans la boucle que pour ce qui est vraiment visuel. La couche protocolaire est testée en CI-friendly time. ## Limitations connues À traiter en phase ultérieure si besoin : - **Parent move ≠ propagation aux enfants** : si on drag un parent toplevel (phase 7.7), ses subsurfaces ne suivent pas tant que le client ne re-commit pas l'enfant. Pour des use-cases statiques c'est OK ; pour Wayland conforme strict il faudra walker les enfants au move du parent. - **wl_subsurface.Destroy ne unmap pas le child** : spec violation mineure. Si le client veut vraiment retirer la subsurface, il doit aussi destroy son wl_surface enfant. - **Pas de cascade sync** : un commit du parent en mode sync devrait flusher les pending states des subsurfaces sync. Pas implémenté. Notre client de test utilise `set_desync` donc le path n'est pas exercé. - **Pas de role-tracking** : spec exige `bad_surface` error si une wl_surface a déjà un rôle (xdg_toplevel) et qu'on appelle GetSubsurface dessus. Pour l'instant on log debug seulement. - **PlaceAbove/PlaceBelow no-op** : z-order relatif entre subsurfaces siblings non géré. Pas observé en pratique avec nos clients de test single-subsurface. - **Raise children scan O(N)** : naïf. Avec un index parent_id → Vec on passe O(1). À faire si N de surfaces visibles devient grand (> 50 ?). ## Fichiers livrés en 13.2 ``` # Code compositor (frontend Wayland) crates/redox-wl-wayland-frontend/src/lib.rs # globals + Dispatch + commit hook # Clients de test crates/redox-wl-test-wl-output/ # validation gating wl_output (13.2.a) ├── src/main.rs # runtime client └── tests/ ├── gating_native.rs # test natif v1/v2/v3 └── subcompositor_native.rs # test natif protocole crates/redox-wl-test-client-subcompositor/ # validation visuelle (13.2.b.3) └── src/main.rs # Tooling runtime tools/redox-scripts/test-sw # shortcut simple_window tools/redox-scripts/test-subc # shortcut subcompositor tools/redox-scripts/test-out # shortcut output gating # Build/deploy run-qemu.sh # copie binaires + scripts # Doc docs/phase13-2-output-subcompositor.md # ce document ``` ## Critère de fin 13.2 > 1. Le compositor expose `wl_output` v3 avec gating correct des events > par version bound (testé v1/v2/v3 en natif et runtime) > 2. Le compositor expose `wl_subcompositor` v1 et accepte tous les > requests `wl_subsurface` sans erreur protocole > 3. Un client multi-surface (parent + 1 subsurface) rend visuellement > les deux surfaces aux bonnes positions (parent à `cascade_offset`, > child à `parent + offset`) > 4. Le z-order reste cohérent même après recommit du parent (le child > reste au-dessus) > 5. Le focus clavier reste sur le parent (les subsurfaces ne grabbent > pas) **✅ Tous critères validés 2026-05-16.** ## Étapes suivantes possibles - **13.3** : extraction backend pour Smithay/COSMIC (Phase 13 du master plan). Auditer notre code pour identifier les primitives réutilisables comme backend Smithay (display, input, session, renderer logiciel). - **13.2.c** : retenter sctk simple_window — avec wl_output et wl_subcompositor maintenant présents, il manque encore xdg-decoration et un wayland-server qui charge libxkbcommon. À évaluer si le ROI vaut le coût d'implémentation. - **Durcissement 13.2** : reprendre les limitations listées ci-dessus (cascade sync, parent move propagation, role-tracking). - **Phase 8** : remplacement expérimental d'Orbital avec ce compositor. Préparé maintenant qu'on couvre les protocoles de base.