Sprite curseur 16x16 ARGB dessiné par-dessus la composition après `SurfaceRegistry::compose_into()`, avec alpha blending non prémultiplié (`out = src + dst * (1 - src.a)`) et hot-spot configurable. Frontend additions : - `cursor_surface_id` / `cursor_hot_x` / `cursor_hot_y` / `cursor_visible` dans `WaylandFrontend` - `is_cursor: AtomicBool` dans `SurfaceData` - `default_cursor_sprite()` : flèche hardcoded 16x16 - `blend_argb_over(src, dst)` avec fast paths a=0/a=255 - `draw_cursor<F: Framebuffer>(target)` : clip aux bords du fb, blit pixel par pixel - `set_cursor_initial_position` / `set_cursor_position` / `cursor_position` publiques - `wl_pointer.set_cursor` handler : store la surface client, marque `is_cursor = true`, l'exclut du Z-order (visible=false) - `wl_surface.commit` lit `is_cursor` → si curseur, pas de raise/focus et reste invisible dans la composition normale - `cursor_visible = true` au premier PointerMotion(Relative) Binaire compositor : - `set_cursor_initial_position(fb_w/2, fb_h/2)` au boot - `frontend.draw_cursor(&mut output)` après `compose_into` - timeout porté de 60s à 180s pour validation visuelle confortable Test client SHM : - timeout porté de 25s à 170s pour rester aligné avec le compositor Validation runtime : 5 screendumps à 5 positions distinctes prouvent que `draw_cursor` est appelé correctement quel que soit `(cursor_x, cursor_y)`, dont 2 captures par-dessus la fenêtre client SHM (overlay alpha-blended sur les bandes arc-en-ciel). Note runtime : Redox n'a pas de driver USB tablet opérationnel sous QEMU. `mouse_move` PS/2 du monitor QEMU ne produit pas non plus de PointerMotion côté inputd. Validation faite en mode programmatique via un cycle temporaire `set_cursor_position`, retiré du binaire après screendumps. À investiguer ps2d/vesad en phase 7.5 ou plus tard. Doc complète : `docs/phase7-3-cursor.md`. Leyoda 2026 – GPLv3
267 lines
11 KiB
Markdown
267 lines
11 KiB
Markdown
# Phase 7.3 — Curseur software
|
||
|
||
> Document produit le 2026-05-09 dans le cadre du plan directeur
|
||
> `REDOX_COSMIC_XWAYLAND_RS_PLAN.md`.
|
||
>
|
||
> **Scope strict** :
|
||
> - dessiner un sprite curseur 16×16 par-dessus la composition après
|
||
> `compose_into()`, avec alpha blending
|
||
> - position basée sur `cursor_x/y` du frontend (déjà tracké via
|
||
> `PointerMotion` / `PointerMotionRelative` en 7.2)
|
||
> - hot-spot configurable
|
||
> - implémenter `wl_pointer.set_cursor` : stocker la surface client comme
|
||
> curseur ; dessiner son buffer si fourni, sinon le sprite par défaut
|
||
> - exclure la surface curseur du Z-order normal (pas de raise/focus)
|
||
>
|
||
> **Hors scope 7.3** : focus + raise on click (7.4), robustesse paquet A
|
||
> (7.5), multi-clients (7.6), move/resize interactifs (7.7).
|
||
|
||
## Verdict
|
||
|
||
**✅ Curseur software validé runtime sur Redox.**
|
||
|
||
5 captures à 5 positions distinctes, dont 2 par-dessus la fenêtre client
|
||
SHM (overlay alpha-blended) :
|
||
|
||
| Capture | Position cursor | Affichage |
|
||
|---|---|---|
|
||
| `phase7-3-cursor-pos-center.png` | (640, 400) | curseur au centre, fenêtre client en haut-gauche |
|
||
| `phase7-3-cursor-over-window-200x150.png` | (200, 150) | curseur **par-dessus** la bande verte de la fenêtre client |
|
||
| `phase7-3-cursor-over-window-250x200.png` | (250, 200) | curseur **par-dessus** la bande violette de la fenêtre client |
|
||
| `phase7-3-cursor-pos-900x600.png` | (900, 600) | curseur en bas-droite sur fond |
|
||
| `phase7-3-cursor-pos-50x750.png` | (50, 750) | curseur en bas-gauche sur fond |
|
||
|
||
Les 5 captures montrent le sprite par défaut (flèche blanche + contour
|
||
noir, 16×16, hot-spot (0,0)) dessiné après `compose_into()` et avant
|
||
`present_with_takeover()`.
|
||
|
||
## Modifications apportées
|
||
|
||
### `redox-wl-wayland-frontend`
|
||
|
||
**`SurfaceData`** :
|
||
- ajout `is_cursor: AtomicBool` — marque les surfaces désignées comme
|
||
curseur par `wl_pointer.set_cursor`. Atomic plutôt que `Mutex<bool>`
|
||
pour éviter de prendre un mutex sur le hot path de la composition.
|
||
|
||
**`WaylandFrontend`** :
|
||
- nouveaux champs :
|
||
- `cursor_surface_id: Option<SurfaceId>` — surface curseur du dernier
|
||
`set_cursor`, ou `None` pour le sprite par défaut
|
||
- `cursor_hot_x: i32`, `cursor_hot_y: i32` — hot-spot du sprite
|
||
- `cursor_visible: bool` — false avant le premier `PointerMotion`,
|
||
ou si `set_cursor(None)` (hide cursor explicite)
|
||
- nouvelles méthodes publiques :
|
||
- `set_cursor_initial_position(x, y)` — pour le compositor : place le
|
||
curseur au centre du framebuffer au boot
|
||
- `set_cursor_position(x, y)` — setter générique (utile pour tests
|
||
programmatiques en attendant un driver mouse fonctionnel)
|
||
- `cursor_position() -> (i32, i32)` — getter
|
||
- `draw_cursor<F: Framebuffer + ?Sized>(target)` — dessine le sprite
|
||
par-dessus le framebuffer avec alpha blending
|
||
|
||
**Sprite par défaut** : `default_cursor_sprite()` retourne `(Vec<u32>,
|
||
16, 16, 0, 0)`. Layout en code Rust :
|
||
|
||
```text
|
||
K . . . . . . . . . . . . . . . K=noir 0xFF000000
|
||
K K . . . . . . . . . . . . . . W=blanc 0xFFFFFFFF
|
||
K W K . . . . . . . . . . . . . .=transparent 0x00000000
|
||
K W W K . . . . . . . . . . . .
|
||
K W W W K . . . . . . . . . . .
|
||
K W W W W K . . . . . . . . . .
|
||
K W W W W W K . . . . . . . . .
|
||
K W W W W W W K . . . . . . . .
|
||
K W W W W W W W K . . . . . . .
|
||
K W W W W W W W W K . . . . . .
|
||
K W W W W W K K K K K . . . . .
|
||
K W W K W W K . . . . . . . . .
|
||
K W K . K W W K . . . . . . . .
|
||
K K . . K W W K . . . . . . . .
|
||
. . . . . K W W K . . . . . . .
|
||
. . . . . . K K . . . . . . . .
|
||
```
|
||
|
||
**Alpha blending** : `blend_argb_over(src, dst) -> u32` implémente la
|
||
formule standard `out = src + dst * (1 - src.a)`. Fast paths pour
|
||
`src.a == 0` (passthrough) et `src.a == 255` (overwrite). Pas de
|
||
prémultiplication ; cohérent avec le sprite par défaut (pixels
|
||
transparents ont RGB=0) et fonctionnel sur les buffers ARGB non-
|
||
prémultipliés des clients.
|
||
|
||
### `wl_pointer.set_cursor` handler
|
||
|
||
```rust
|
||
wl_pointer::Request::SetCursor { surface, hotspot_x, hotspot_y, .. } => {
|
||
match surface {
|
||
Some(surf) => {
|
||
// Marque la surface comme curseur
|
||
sd.is_cursor.store(true, Ordering::Relaxed);
|
||
state.cursor_surface_id = Some(sid);
|
||
// La masque dans la composition normale
|
||
state.registry.modify_pending(sid, |s| s.visible = false);
|
||
state.registry.commit(sid);
|
||
state.cursor_hot_x = hotspot_x;
|
||
state.cursor_hot_y = hotspot_y;
|
||
}
|
||
None => {
|
||
// Hide cursor explicite (spec Wayland)
|
||
state.cursor_surface_id = None;
|
||
state.cursor_visible = false;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### `wl_surface.commit` modifié
|
||
|
||
La branche commit lit maintenant `data.is_cursor` :
|
||
- si **non-curseur** : comportement 7.2 (modify_pending visible=true,
|
||
commit, raise au top, set_focus à cette surface)
|
||
- si **curseur** : modify_pending visible=**false** (exclu de la
|
||
composition normale), commit, **pas** de raise, **pas** de set_focus
|
||
|
||
Cela garantit qu'une surface curseur ne pollue ni le Z-order normal ni
|
||
le routage clavier/souris du toolkit, tout en préservant le buffer
|
||
dans le registry pour que `draw_cursor()` puisse le lire.
|
||
|
||
### `draw_cursor` pipeline
|
||
|
||
```rust
|
||
pub fn draw_cursor<F: Framebuffer + ?Sized>(&self, target: &mut F) {
|
||
if !self.cursor_visible { return; }
|
||
let (pixels, sw, sh, hot_x, hot_y) = self.current_cursor_sprite();
|
||
// surf_x0 = cursor_x - hot_x, idem y
|
||
// clip aux bords du framebuffer
|
||
// pour chaque pixel : dst = blend_argb_over(src, dst)
|
||
}
|
||
```
|
||
|
||
`current_cursor_sprite()` retourne soit le buffer du client (via
|
||
`registry.get(cursor_surface_id).current().buffer`), soit le sprite par
|
||
défaut. Le sprite par défaut est reconstruit à chaque appel (16×16 =
|
||
256 alloc + push, négligeable à 30fps).
|
||
|
||
### `redox-wl-compositor` (binaire)
|
||
|
||
- `frontend.set_cursor_initial_position(fb_w/2, fb_h/2)` au démarrage,
|
||
pour que le curseur soit visible au centre avant tout event souris
|
||
- `frontend.draw_cursor(&mut output)` après `compose_into`, avant
|
||
`present_with_takeover`
|
||
- timeout du runloop porté à 180s (vs 60s en 7.2) pour rendre les tests
|
||
de validation visuelle multi-positions plus confortables
|
||
|
||
## Méthode de validation visuelle
|
||
|
||
QEMU monitor `mouse_move dx dy` envoie un mouvement *relatif* via le
|
||
PS/2 par défaut. **Redox ne semble pas avoir de driver USB tablet
|
||
opérationnel** : malgré `-device usb-tablet`, aucun `PointerMotion`
|
||
absolu n'arrive jusqu'à `inputd` (vérifié via grep sur le serial log).
|
||
Seuls les `mouse_button` PS/2 génèrent des events visibles côté
|
||
compositor (1 `input event from inputd` par sendkey).
|
||
|
||
Comme `mouse_move dx dy` avec de gros deltas ne fait pas bouger le
|
||
curseur côté Redox non plus (les deltas sont probablement saturés ou
|
||
ignorés par le driver PS/2 ps2d), la validation a été faite en
|
||
**mode programmatique** : un cycle temporaire dans le binaire compositor
|
||
faisait osciller `frontend.set_cursor_position(x, y)` entre 5 positions
|
||
toutes les 4 secondes. 5 screendumps QEMU pris à intervalles synchrones
|
||
confirment :
|
||
|
||
1. le sprite est rendu après la composition (overlay)
|
||
2. `draw_cursor` réagit correctement à n'importe quel `(cursor_x,
|
||
cursor_y)` du frontend
|
||
3. l'alpha blending fonctionne (la pointe blanc+contour noir se découpe
|
||
nettement sur fond uni comme sur la fenêtre client multicolore)
|
||
4. la surface client SHM reste correctement composée — curseur et
|
||
surface coexistent
|
||
|
||
Le cycle de validation a été **retiré du binaire après validation**.
|
||
Le compositor commité ne fait que `set_cursor_initial_position` au
|
||
boot + `draw_cursor` par frame. Quand un vrai driver mouse Redox
|
||
(ou un client `wl_pointer.set_cursor`) prendra le relais, ce sera via
|
||
`forward_input(PointerMotion...)` qui met déjà à jour `cursor_x/y`.
|
||
|
||
## Logs serial typiques
|
||
|
||
```
|
||
[comp] Phase 6.4 — compositor Wayland démarrage
|
||
[comp] display 1280x800, VT=3
|
||
[comp] CRTC pris
|
||
[comp] Wayland socket : /tmp/redox-wl-comp.sock
|
||
[client] connect to compositor
|
||
[client] xdg_toplevel créé, attente initial configure
|
||
[client] initial configure reçu, serial=1
|
||
[client] ack_configure(1)
|
||
[client] buffer attach + damage + commit POST-ack
|
||
[comp] tick=30 surfaces=1 elapsed=1.2s
|
||
... (compositeur stable, 30fps, 1 surface client)
|
||
[comp] tick=4170 surfaces=1 elapsed=169.4s
|
||
[client] done, destroy propre
|
||
[client] PASS
|
||
[comp] tick=4410 surfaces=0 elapsed=179.1s
|
||
[comp] timeout atteint, exit
|
||
[comp] PASS
|
||
```
|
||
|
||
Aucun panic. Aucun warning runtime côté frontend.
|
||
|
||
## Limitations connues (à traiter en sous-tickets ultérieurs)
|
||
|
||
- **wl_pointer.set_cursor non testé runtime** : aucun client de test
|
||
n'utilise encore set_cursor. Le store côté serveur est implémenté et
|
||
exclu du Z-order, le sprite par défaut est la fallback. À durcir en
|
||
phase 7.5/7.6 avec un client qui fournit son propre sprite.
|
||
- **Driver mouse Redox non opérationnel sous QEMU avec virtio-vga** :
|
||
`mouse_move dx dy` du monitor QEMU ne produit pas de PointerMotion
|
||
côté `inputd`. À investiguer côté `ps2d` / vesad / configuration
|
||
QEMU. Pas bloquant car la phase 7.3 dessine bien le curseur dès qu'on
|
||
fixe sa position via API ; le câblage `forward_input → cursor_x/y`
|
||
est unchanged depuis 7.2 et testable dès qu'on aura mouse events.
|
||
- **Pas de cursor theme / thèmes XCursor** : un seul sprite hardcoded.
|
||
Les clients qui voudront un curseur "I-beam" ou "wait" devront
|
||
envoyer leur propre buffer via set_cursor.
|
||
- **Pas de damage tracking sur la zone curseur** : on recompose tout
|
||
et on blend par-dessus à chaque frame. À 30fps + 16×16 = 256
|
||
pixels/frame, c'est négligeable. À optimiser quand on aura damage
|
||
tracking complet (phase 7+ tardive).
|
||
- **Alpha blending non prémultiplié** : fonctionne pour le sprite par
|
||
défaut et pour les buffers ARGB simples des clients, mais la spec
|
||
wl_shm.Argb8888 dit "premultiplied". À durcir en 7.5 si on rencontre
|
||
un client qui se plaint.
|
||
|
||
## Critère de fin 7.3
|
||
|
||
> Un sprite curseur 16×16 est dessiné par-dessus la composition à la
|
||
> position `(cursor_x, cursor_y)` du frontend, avec alpha blending, hot-
|
||
> spot configurable, sans déstabiliser la composition ni le routage
|
||
> input existants.
|
||
|
||
**✅ Validé.** 5 captures à 5 positions distinctes prouvent que :
|
||
- le sprite est rendu (visible sur fond uni comme sur surface client)
|
||
- la position est paramétrable via `set_cursor_position`
|
||
- l'alpha blending est correct (silhouette nette sur tous fonds)
|
||
- la surface client reste correctement composée en coexistence
|
||
- aucun panic, aucun warning runtime côté frontend ou compositor
|
||
|
||
## Code
|
||
|
||
```
|
||
crates/redox-wl-wayland-frontend/ # +~180 lignes (sprite, blend, draw_cursor, set_cursor handler)
|
||
crates/redox-wl-compositor/ # +3 lignes (set_cursor_initial_position + draw_cursor + timeout 180s)
|
||
```
|
||
|
||
## Suite phase 7.4
|
||
|
||
Focus + raise on click via `hit_test()`. Quand un client clique dans
|
||
une surface, le compositor doit :
|
||
1. appeler `registry.hit_test(cursor_x, cursor_y)` pour trouver la
|
||
surface ciblée
|
||
2. la `raise()` au top du Z-order
|
||
3. envoyer `set_focus(Some(target))` pour transférer le keyboard focus
|
||
|
||
Estimé : 1 session.
|
||
|
||
---
|
||
|
||
*Fin du document de phase 7.3.*
|