🎉 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
This commit is contained in:
parent
baa94701bf
commit
5f7587e79e
9 changed files with 536 additions and 16 deletions
|
|
@ -86,6 +86,8 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
// Wayland frontend
|
// Wayland frontend
|
||||||
let socket_path = PathBuf::from(SOCKET_PATH);
|
let socket_path = PathBuf::from(SOCKET_PATH);
|
||||||
let mut frontend = WaylandFrontend::bind_absolute(&socket_path)?;
|
let mut frontend = WaylandFrontend::bind_absolute(&socket_path)?;
|
||||||
|
// Phase 7.3 : curseur visible dès le démarrage, placé au centre du fb.
|
||||||
|
frontend.set_cursor_initial_position((fb_w as i32) / 2, (fb_h as i32) / 2);
|
||||||
dlog(&format!("[comp] Wayland socket : {SOCKET_PATH}"));
|
dlog(&format!("[comp] Wayland socket : {SOCKET_PATH}"));
|
||||||
|
|
||||||
// Exporter WAYLAND_DISPLAY pour les clients lancés par l'OS qui regarderaient
|
// Exporter WAYLAND_DISPLAY pour les clients lancés par l'OS qui regarderaient
|
||||||
|
|
@ -96,7 +98,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
|
||||||
// Boucle principale
|
// Boucle principale
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let total = Duration::from_secs(60);
|
let total = Duration::from_secs(180);
|
||||||
let frame_period = Duration::from_millis(33); // ~30 fps
|
let frame_period = Duration::from_millis(33); // ~30 fps
|
||||||
let mut last_frame = Instant::now();
|
let mut last_frame = Instant::now();
|
||||||
let mut tick: u32 = 0;
|
let mut tick: u32 = 0;
|
||||||
|
|
@ -154,6 +156,8 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
frontend.registry.compose_into(&mut output);
|
frontend.registry.compose_into(&mut output);
|
||||||
|
// Phase 7.3 : curseur software par-dessus la composition.
|
||||||
|
frontend.draw_cursor(&mut output);
|
||||||
if let Err(e) = output.present_with_takeover() {
|
if let Err(e) = output.present_with_takeover() {
|
||||||
dlog(&format!("[comp] present err: {e}"));
|
dlog(&format!("[comp] present err: {e}"));
|
||||||
}
|
}
|
||||||
|
|
@ -179,7 +183,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
thread::sleep(frame_period);
|
thread::sleep(frame_period);
|
||||||
}
|
}
|
||||||
|
|
||||||
dlog("[comp] timeout 60s atteint, exit");
|
dlog("[comp] timeout atteint, exit");
|
||||||
let _ = std::fs::remove_file(SOCKET_PATH);
|
let _ = std::fs::remove_file(SOCKET_PATH);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -304,9 +304,9 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
event_queue.flush()?;
|
event_queue.flush()?;
|
||||||
let _ = event_queue.roundtrip(&mut state);
|
let _ = event_queue.roundtrip(&mut state);
|
||||||
|
|
||||||
// 5. Boucle vivante 25 secondes
|
// 5. Boucle vivante 170 secondes
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
while start.elapsed() < Duration::from_secs(25) {
|
while start.elapsed() < Duration::from_secs(170) {
|
||||||
let _ = event_queue.dispatch_pending(&mut state);
|
let _ = event_queue.dispatch_pending(&mut state);
|
||||||
let _ = event_queue.flush();
|
let _ = event_queue.flush();
|
||||||
thread::sleep(Duration::from_millis(50));
|
thread::sleep(Duration::from_millis(50));
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,10 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::os::fd::{AsRawFd, OwnedFd};
|
use std::os::fd::{AsRawFd, OwnedFd};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use redox_wl_compositor_core::{SurfaceBuffer, SurfaceId, SurfaceRegistry};
|
use redox_wl_compositor_core::{Framebuffer, SurfaceBuffer, SurfaceId, SurfaceRegistry};
|
||||||
use wayland_protocols::xdg::shell::server::{
|
use wayland_protocols::xdg::shell::server::{
|
||||||
xdg_popup, xdg_positioner, xdg_surface, xdg_toplevel, xdg_wm_base,
|
xdg_popup, xdg_positioner, xdg_surface, xdg_toplevel, xdg_wm_base,
|
||||||
};
|
};
|
||||||
|
|
@ -140,6 +141,12 @@ struct SurfaceData {
|
||||||
/// (xdg-shell spec : le rôle xdg_surface "désactive" le rendu
|
/// (xdg-shell spec : le rôle xdg_surface "désactive" le rendu
|
||||||
/// jusqu'au premier configure-ack).
|
/// jusqu'au premier configure-ack).
|
||||||
xdg_pending_initial_configure: Mutex<bool>,
|
xdg_pending_initial_configure: Mutex<bool>,
|
||||||
|
/// Phase 7.3 : true si la surface a été désignée comme curseur via
|
||||||
|
/// `wl_pointer.set_cursor`. Une telle surface est exclue du Z-order
|
||||||
|
/// normal (pas de compose_into) et dessinée par-dessus via
|
||||||
|
/// `draw_cursor()`. Atomic pour éviter de prendre un Mutex sur le hot
|
||||||
|
/// path de la composition.
|
||||||
|
is_cursor: AtomicBool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Données par-xdg_surface : référence à la wl_surface sous-jacente +
|
/// Données par-xdg_surface : référence à la wl_surface sous-jacente +
|
||||||
|
|
@ -202,6 +209,20 @@ pub struct WaylandFrontend {
|
||||||
next_input_serial: u32,
|
next_input_serial: u32,
|
||||||
/// Timestamp incrémental pour les events seat (ms-like).
|
/// Timestamp incrémental pour les events seat (ms-like).
|
||||||
input_time_ms: u32,
|
input_time_ms: u32,
|
||||||
|
|
||||||
|
// ----- Phase 7.3 : cursor software --------------------------------
|
||||||
|
/// SurfaceId compositor-core de la surface curseur fournie par le
|
||||||
|
/// dernier `wl_pointer.set_cursor`. `None` = pas de curseur custom,
|
||||||
|
/// on dessine le sprite par défaut (flèche 16x16).
|
||||||
|
cursor_surface_id: Option<SurfaceId>,
|
||||||
|
/// Hot-spot du curseur (offset à soustraire à cursor_x/y pour le placement).
|
||||||
|
cursor_hot_x: i32,
|
||||||
|
cursor_hot_y: i32,
|
||||||
|
/// Si false, le curseur n'est pas dessiné du tout (utile si jamais
|
||||||
|
/// l'utilisateur veut le masquer, et naturellement avant la première
|
||||||
|
/// PointerMotion). Devient true dès qu'on a reçu un PointerMotion ou
|
||||||
|
/// PointerMotionRelative.
|
||||||
|
cursor_visible: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WaylandFrontend {
|
impl WaylandFrontend {
|
||||||
|
|
@ -234,6 +255,10 @@ impl WaylandFrontend {
|
||||||
cursor_y: 0,
|
cursor_y: 0,
|
||||||
next_input_serial: 1,
|
next_input_serial: 1,
|
||||||
input_time_ms: 0,
|
input_time_ms: 0,
|
||||||
|
cursor_surface_id: None,
|
||||||
|
cursor_hot_x: 0,
|
||||||
|
cursor_hot_y: 0,
|
||||||
|
cursor_visible: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -391,6 +416,7 @@ impl WaylandFrontend {
|
||||||
RedoxInputEvent::PointerMotion { x, y } => {
|
RedoxInputEvent::PointerMotion { x, y } => {
|
||||||
self.cursor_x = *x;
|
self.cursor_x = *x;
|
||||||
self.cursor_y = *y;
|
self.cursor_y = *y;
|
||||||
|
self.cursor_visible = true;
|
||||||
let time = self.alloc_input_time();
|
let time = self.alloc_input_time();
|
||||||
if let Some(focus) = self.focused_surface.clone() {
|
if let Some(focus) = self.focused_surface.clone() {
|
||||||
let (sx, sy) = self.surface_local_cursor(&focus);
|
let (sx, sy) = self.surface_local_cursor(&focus);
|
||||||
|
|
@ -405,6 +431,7 @@ impl WaylandFrontend {
|
||||||
RedoxInputEvent::PointerMotionRelative { dx, dy } => {
|
RedoxInputEvent::PointerMotionRelative { dx, dy } => {
|
||||||
self.cursor_x = self.cursor_x.saturating_add(*dx);
|
self.cursor_x = self.cursor_x.saturating_add(*dx);
|
||||||
self.cursor_y = self.cursor_y.saturating_add(*dy);
|
self.cursor_y = self.cursor_y.saturating_add(*dy);
|
||||||
|
self.cursor_visible = true;
|
||||||
let time = self.alloc_input_time();
|
let time = self.alloc_input_time();
|
||||||
if let Some(focus) = self.focused_surface.clone() {
|
if let Some(focus) = self.focused_surface.clone() {
|
||||||
let (sx, sy) = self.surface_local_cursor(&focus);
|
let (sx, sy) = self.surface_local_cursor(&focus);
|
||||||
|
|
@ -484,6 +511,175 @@ fn fixed_from_int(v: i32) -> f64 {
|
||||||
v as f64
|
v as f64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sprite curseur par défaut : flèche 16x16 ARGB8888.
|
||||||
|
///
|
||||||
|
/// Layout (16 lignes de 16 colonnes) :
|
||||||
|
/// - `K` = noir opaque (0xFF000000) — contour
|
||||||
|
/// - `W` = blanc opaque (0xFFFFFFFF) — intérieur
|
||||||
|
/// - `.` = transparent (0x00000000) — alpha 0
|
||||||
|
///
|
||||||
|
/// Hot-spot : (0, 0), pointe en haut-gauche (curseur "north-west arrow"
|
||||||
|
/// classique). Utilisé quand aucun client n'a fourni `wl_pointer.set_cursor`.
|
||||||
|
fn default_cursor_sprite() -> (Vec<u32>, u32, u32, i32, i32) {
|
||||||
|
const K: u32 = 0xFF000000;
|
||||||
|
const W: u32 = 0xFFFFFFFF;
|
||||||
|
const T: u32 = 0x00000000;
|
||||||
|
let pat: [&[u8; 16]; 16] = [
|
||||||
|
b"K...............",
|
||||||
|
b"KK..............",
|
||||||
|
b"KWK.............",
|
||||||
|
b"KWWK............",
|
||||||
|
b"KWWWK...........",
|
||||||
|
b"KWWWWK..........",
|
||||||
|
b"KWWWWWK.........",
|
||||||
|
b"KWWWWWWK........",
|
||||||
|
b"KWWWWWWWK.......",
|
||||||
|
b"KWWWWWWWWK......",
|
||||||
|
b"KWWWWWKKKKK.....",
|
||||||
|
b"KWWKWWK.........",
|
||||||
|
b"KWK.KWWK........",
|
||||||
|
b"KK..KWWK........",
|
||||||
|
b".....KWWK.......",
|
||||||
|
b"......KK........",
|
||||||
|
];
|
||||||
|
let mut pixels = Vec::with_capacity(16 * 16);
|
||||||
|
for row in pat.iter() {
|
||||||
|
for &b in row.iter() {
|
||||||
|
pixels.push(match b {
|
||||||
|
b'K' => K,
|
||||||
|
b'W' => W,
|
||||||
|
_ => T,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(pixels, 16, 16, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Alpha blending d'un pixel ARGB8888 source par-dessus un pixel
|
||||||
|
/// ARGB8888 destination. Formule : `out = src + dst * (1 - src.a)`.
|
||||||
|
/// Pas de prémultiplication : src est interprété comme RGB séparé puis
|
||||||
|
/// scalé par alpha, en cohérence avec ce que produit le sprite par défaut
|
||||||
|
/// et la majorité des toolkits Wayland qui envoient des buffers
|
||||||
|
/// non-prémultipliés via `wl_shm` Argb8888 (le format wl_shm Argb8888
|
||||||
|
/// est en pratique prémultiplié dans la spec, mais notre sprite hardcoded
|
||||||
|
/// non — quand un client fournit un buffer prémultiplié, on l'écrase via
|
||||||
|
/// le même calcul ; le résultat est correct dans les deux cas tant que
|
||||||
|
/// les valeurs RGB d'un pixel transparent sont 0 dans le sprite).
|
||||||
|
#[inline]
|
||||||
|
fn blend_argb_over(src: u32, dst: u32) -> u32 {
|
||||||
|
let sa = ((src >> 24) & 0xFF) as u32;
|
||||||
|
if sa == 0 {
|
||||||
|
return dst;
|
||||||
|
}
|
||||||
|
if sa == 255 {
|
||||||
|
return src;
|
||||||
|
}
|
||||||
|
let inv = 255 - sa;
|
||||||
|
let sr = (src >> 16) & 0xFF;
|
||||||
|
let sg = (src >> 8) & 0xFF;
|
||||||
|
let sb = src & 0xFF;
|
||||||
|
let dr = (dst >> 16) & 0xFF;
|
||||||
|
let dg = (dst >> 8) & 0xFF;
|
||||||
|
let db = dst & 0xFF;
|
||||||
|
// Approx (sr*sa + dr*inv) / 255 via *257>>16 pour éviter une vraie /255
|
||||||
|
let r = (sr * sa + dr * inv) / 255;
|
||||||
|
let g = (sg * sa + dg * inv) / 255;
|
||||||
|
let b = (sb * sa + db * inv) / 255;
|
||||||
|
let da = ((dst >> 24) & 0xFF) as u32;
|
||||||
|
let oa = sa + da * inv / 255;
|
||||||
|
(oa << 24) | (r << 16) | (g << 8) | b
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WaylandFrontend {
|
||||||
|
/// Position courante du curseur. Exposé pour le compositor binaire
|
||||||
|
/// qui pourrait vouloir logger ou raise on click via hit_test.
|
||||||
|
pub fn cursor_position(&self) -> (i32, i32) {
|
||||||
|
(self.cursor_x, self.cursor_y)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Force la position initiale du curseur, par ex. au centre du
|
||||||
|
/// framebuffer au boot pour qu'il soit visible avant le premier mouvement.
|
||||||
|
pub fn set_cursor_initial_position(&mut self, x: i32, y: i32) {
|
||||||
|
self.cursor_x = x;
|
||||||
|
self.cursor_y = y;
|
||||||
|
self.cursor_visible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Force la position du curseur à tout moment. Utile pour tests
|
||||||
|
/// programmatiques qui veulent simuler un mouvement souris en dehors
|
||||||
|
/// du circuit `forward_input`.
|
||||||
|
pub fn set_cursor_position(&mut self, x: i32, y: i32) {
|
||||||
|
self.cursor_x = x;
|
||||||
|
self.cursor_y = y;
|
||||||
|
self.cursor_visible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Récupère le buffer curseur courant (client custom si fourni, sinon
|
||||||
|
/// sprite par défaut). Retourne `(pixels, w, h, hot_x, hot_y)`.
|
||||||
|
fn current_cursor_sprite(&self) -> (Arc<Vec<u32>>, u32, u32, i32, i32) {
|
||||||
|
if let Some(sid) = self.cursor_surface_id {
|
||||||
|
if let Some(s) = self.registry.get(sid) {
|
||||||
|
if let Some(buf) = &s.current().buffer {
|
||||||
|
return (
|
||||||
|
Arc::clone(&buf.pixels),
|
||||||
|
buf.width,
|
||||||
|
buf.height,
|
||||||
|
self.cursor_hot_x,
|
||||||
|
self.cursor_hot_y,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let (pixels, w, h, hx, hy) = default_cursor_sprite();
|
||||||
|
(Arc::new(pixels), w, h, hx, hy)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dessine le curseur par-dessus le framebuffer, avec alpha blending.
|
||||||
|
/// À appeler APRÈS `registry.compose_into(&mut output)` et AVANT
|
||||||
|
/// `output.present_with_takeover()`.
|
||||||
|
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();
|
||||||
|
let fb_w = target.width() as i32;
|
||||||
|
let fb_h = target.height() as i32;
|
||||||
|
if fb_w <= 0 || fb_h <= 0 || sw == 0 || sh == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let s_w = sw as i32;
|
||||||
|
let s_h = sh as i32;
|
||||||
|
let surf_x0 = self.cursor_x.saturating_sub(hot_x);
|
||||||
|
let surf_y0 = self.cursor_y.saturating_sub(hot_y);
|
||||||
|
let surf_x1 = surf_x0.saturating_add(s_w);
|
||||||
|
let surf_y1 = surf_y0.saturating_add(s_h);
|
||||||
|
let dst_x0 = surf_x0.max(0);
|
||||||
|
let dst_y0 = surf_y0.max(0);
|
||||||
|
let dst_x1 = surf_x1.min(fb_w);
|
||||||
|
let dst_y1 = surf_y1.min(fb_h);
|
||||||
|
if dst_x0 >= dst_x1 || dst_y0 >= dst_y1 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let src_x0 = (dst_x0 - surf_x0) as usize;
|
||||||
|
let src_y0 = (dst_y0 - surf_y0) as usize;
|
||||||
|
let copy_w = (dst_x1 - dst_x0) as usize;
|
||||||
|
let copy_h = (dst_y1 - dst_y0) as usize;
|
||||||
|
let s_w_us = sw as usize;
|
||||||
|
let fb_w_us = fb_w as usize;
|
||||||
|
let dst_pixels = target.pixels_mut();
|
||||||
|
for row in 0..copy_h {
|
||||||
|
let src_row = (src_y0 + row) * s_w_us + src_x0;
|
||||||
|
let dst_y = (dst_y0 as usize) + row;
|
||||||
|
let dst_row = dst_y * fb_w_us + (dst_x0 as usize);
|
||||||
|
for col in 0..copy_w {
|
||||||
|
let src = pixels[src_row + col];
|
||||||
|
let dst = dst_pixels[dst_row + col];
|
||||||
|
dst_pixels[dst_row + col] = blend_argb_over(src, dst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
// Dispatch impls
|
// Dispatch impls
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
|
|
@ -525,6 +721,7 @@ impl wayland_server::Dispatch<wl_compositor::WlCompositor, ()> for WaylandFronte
|
||||||
pending_buffer: Mutex::new(None),
|
pending_buffer: Mutex::new(None),
|
||||||
pending_frame_callbacks: Mutex::new(Vec::new()),
|
pending_frame_callbacks: Mutex::new(Vec::new()),
|
||||||
xdg_pending_initial_configure: Mutex::new(false),
|
xdg_pending_initial_configure: Mutex::new(false),
|
||||||
|
is_cursor: AtomicBool::new(false),
|
||||||
};
|
};
|
||||||
data_init.init(id, Arc::new(data));
|
data_init.init(id, Arc::new(data));
|
||||||
}
|
}
|
||||||
|
|
@ -705,6 +902,8 @@ impl wayland_server::Dispatch<wl_surface::WlSurface, Arc<SurfaceData>> for Wayla
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let is_cursor = data.is_cursor.load(Ordering::Relaxed);
|
||||||
|
|
||||||
// Lire le buffer attaché (s'il y en a un)
|
// Lire le buffer attaché (s'il y en a un)
|
||||||
let bd_opt = data.pending_buffer.lock().unwrap().clone();
|
let bd_opt = data.pending_buffer.lock().unwrap().clone();
|
||||||
if let Some(bd) = bd_opt {
|
if let Some(bd) = bd_opt {
|
||||||
|
|
@ -714,26 +913,34 @@ impl wayland_server::Dispatch<wl_surface::WlSurface, Arc<SurfaceData>> for Wayla
|
||||||
pool.read_argb(bd.offset as usize, bd.width, bd.height, bd.stride)
|
pool.read_argb(bd.offset as usize, bd.width, bd.height, bd.stride)
|
||||||
};
|
};
|
||||||
let sb = SurfaceBuffer::from_pixels(bd.width, bd.height, pixels);
|
let sb = SurfaceBuffer::from_pixels(bd.width, bd.height, pixels);
|
||||||
|
// Pour une surface curseur, on stocke le buffer mais on
|
||||||
|
// garde visible=false (la surface ne doit pas apparaître
|
||||||
|
// dans la composition normale, seulement via draw_cursor).
|
||||||
state.registry.modify_pending(id, |s| {
|
state.registry.modify_pending(id, |s| {
|
||||||
s.buffer = Some(sb);
|
s.buffer = Some(sb);
|
||||||
s.visible = true;
|
s.visible = !is_cursor;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
state.registry.commit(id);
|
state.registry.commit(id);
|
||||||
// Promouvoir au top du Z-order au commit (politique simple :
|
if !is_cursor {
|
||||||
// dernière surface qui commit = au-dessus). À raffiner en
|
// Promouvoir au top du Z-order au commit (politique simple :
|
||||||
// phase 7 (focus, raise on click, etc.).
|
// dernière surface qui commit = au-dessus). À raffiner en
|
||||||
state.registry.raise(id);
|
// phase 7 (focus, raise on click, etc.).
|
||||||
|
state.registry.raise(id);
|
||||||
|
}
|
||||||
|
|
||||||
// Frame callbacks en attente → bump dans la queue globale
|
// Frame callbacks en attente → bump dans la queue globale
|
||||||
let mut cbs = data.pending_frame_callbacks.lock().unwrap();
|
let mut cbs = data.pending_frame_callbacks.lock().unwrap();
|
||||||
state.frame_callbacks.append(&mut *cbs);
|
state.frame_callbacks.append(&mut *cbs);
|
||||||
drop(cbs);
|
drop(cbs);
|
||||||
|
|
||||||
// Phase 7.2 : la surface qui vient de commiter et raise
|
if !is_cursor {
|
||||||
// devient automatiquement la surface focalisée. Envoie les
|
// Phase 7.2 : la surface qui vient de commiter et raise
|
||||||
// events keyboard/pointer enter/leave en conséquence.
|
// devient automatiquement la surface focalisée. Envoie les
|
||||||
state.set_focus(Some(_resource.clone()));
|
// events keyboard/pointer enter/leave en conséquence.
|
||||||
|
// Une surface curseur n'a évidemment pas le focus — on skip.
|
||||||
|
state.set_focus(Some(_resource.clone()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
wl_surface::Request::Destroy => {
|
wl_surface::Request::Destroy => {
|
||||||
let mut id_lock = data.id.lock().unwrap();
|
let mut id_lock = data.id.lock().unwrap();
|
||||||
|
|
@ -1011,8 +1218,50 @@ impl wayland_server::Dispatch<wl_pointer::WlPointer, ()> for WaylandFrontend {
|
||||||
_data_init: &mut DataInit<'_, Self>,
|
_data_init: &mut DataInit<'_, Self>,
|
||||||
) {
|
) {
|
||||||
match request {
|
match request {
|
||||||
wl_pointer::Request::SetCursor { .. } => {
|
wl_pointer::Request::SetCursor {
|
||||||
// Pour 7.3 (cursor visible). Ignoré ici.
|
surface,
|
||||||
|
hotspot_x,
|
||||||
|
hotspot_y,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
// Phase 7.3 : enregistre la surface curseur du client.
|
||||||
|
// Si surface=None → on revient au sprite par défaut.
|
||||||
|
match surface {
|
||||||
|
Some(surf) => {
|
||||||
|
// Marquer la surface comme curseur (exclue de la
|
||||||
|
// composition normale et du raise/focus au commit).
|
||||||
|
if let Some(sd) = surf.data::<Arc<SurfaceData>>() {
|
||||||
|
sd.is_cursor.store(true, Ordering::Relaxed);
|
||||||
|
if let Some(sid) = *sd.id.lock().unwrap() {
|
||||||
|
state.cursor_surface_id = Some(sid);
|
||||||
|
// Si jamais elle était déjà visible dans le
|
||||||
|
// registry (ex. si le client commit avant le
|
||||||
|
// set_cursor), masquer côté composition.
|
||||||
|
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 explicit : on retombe sur "pas de
|
||||||
|
// sprite custom" (donc default sprite). Si on voulait
|
||||||
|
// le cacher complètement, on mettrait cursor_visible=
|
||||||
|
// false. Spec : surface=None → cursor invisible.
|
||||||
|
if let Some(sid) = state.cursor_surface_id.take() {
|
||||||
|
// L'ancienne surface curseur peut redevenir une
|
||||||
|
// surface normale si le client refait set_cursor
|
||||||
|
// avec un buffer ailleurs ; pour 7.3 on la laisse
|
||||||
|
// marquée is_cursor (le client ne va pas la
|
||||||
|
// recycler en pratique).
|
||||||
|
let _ = sid;
|
||||||
|
}
|
||||||
|
state.cursor_visible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
wl_pointer::Request::Release => {
|
wl_pointer::Request::Release => {
|
||||||
// Retire la resource de notre liste pour ne plus lui envoyer d'events
|
// Retire la resource de notre liste pour ne plus lui envoyer d'events
|
||||||
|
|
|
||||||
BIN
docs/phase7-3-cursor-over-window-200x150.png
Normal file
BIN
docs/phase7-3-cursor-over-window-200x150.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
docs/phase7-3-cursor-over-window-250x200.png
Normal file
BIN
docs/phase7-3-cursor-over-window-250x200.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
docs/phase7-3-cursor-pos-50x750.png
Normal file
BIN
docs/phase7-3-cursor-pos-50x750.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
docs/phase7-3-cursor-pos-900x600.png
Normal file
BIN
docs/phase7-3-cursor-pos-900x600.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
docs/phase7-3-cursor-pos-center.png
Normal file
BIN
docs/phase7-3-cursor-pos-center.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
267
docs/phase7-3-cursor.md
Normal file
267
docs/phase7-3-cursor.md
Normal file
|
|
@ -0,0 +1,267 @@
|
||||||
|
# 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.*
|
||||||
Loading…
Add table
Add a link
Reference in a new issue