redox-wayland-compositor/docs/phase7-3-cursor.md
Votre Nom 5f7587e79e 🎉 Phase 7.3 — curseur software validé runtime
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
2026-05-13 09:06:56 +02:00

11 KiB
Raw Permalink Blame History

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 :

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

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

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.