From e3e554ac92ff66d20c34e821e8f63ac9e91b9427 Mon Sep 17 00:00:00 2001 From: Votre Nom Date: Sat, 9 May 2026 11:37:25 +0200 Subject: [PATCH] =?UTF-8?q?Phase=206.1=20=E2=80=94=20compositor-core=20ske?= =?UTF-8?q?leton=20+=2012=20tests=20unitaires?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Crate redox-wl-compositor-core (lib pure Rust, sans deps externes) : - SurfaceId : newtype u64 opaque - SurfaceBuffer : Arc> ARGB8888 + width/height - SurfaceState : x, y, buffer, visible - Surface : id + current + pending + commit() - SurfaceRegistry : HashMap + z_order Vec - create() / destroy() / raise() - get() / get_mut() / commit() / modify_pending() - iter_z_order_back_to_front() pour la composition Sémantique Wayland (pending → current via commit) prévue dans l'API mais implémentation triviale (clone). Pas de damage tracking, pas de double-buffer atomique : reportés à 6.4 quand wl_shm/xdg-shell arriveront. 12 tests unitaires : - création/destruction/idempotence - z-order par défaut + raise sur top/non-top/unknown - pending vs current state séparés - commit propage pending → current - destroyed surface skipped during iteration - workflow compositor typique end-to-end (3 fenêtres + raise) Tous passent en cargo test natif (0.77s release). La crate compile aussi pour x86_64-unknown-redox via redoxer (pure Rust, aucune dep system). Phase 6.1 close. Suite : 6.2 (compose_into RedoxOutput). Leyoda 2026 – GPLv3 --- crates/redox-wl-compositor-core/Cargo.toml | 9 + crates/redox-wl-compositor-core/src/lib.rs | 442 +++++++++++++++++++++ 2 files changed, 451 insertions(+) create mode 100644 crates/redox-wl-compositor-core/Cargo.toml create mode 100644 crates/redox-wl-compositor-core/src/lib.rs diff --git a/crates/redox-wl-compositor-core/Cargo.toml b/crates/redox-wl-compositor-core/Cargo.toml new file mode 100644 index 0000000..78b2005 --- /dev/null +++ b/crates/redox-wl-compositor-core/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "redox-wl-compositor-core" +version = "0.1.0" +edition = "2021" +description = "Compositor core: surfaces, registry, z-order. OS-agnostic." + +[dependencies] +# Pure Rust, no external deps for the core types. +# Backends (display/input) and frontends (wayland) live in separate crates. diff --git a/crates/redox-wl-compositor-core/src/lib.rs b/crates/redox-wl-compositor-core/src/lib.rs new file mode 100644 index 0000000..0a52688 --- /dev/null +++ b/crates/redox-wl-compositor-core/src/lib.rs @@ -0,0 +1,442 @@ +//! Phase 6.1 — Core compositor structures. +//! +//! Pure logique : surfaces, registry, z-order. Aucune dépendance Redox, +//! Wayland ou backend display/input. Le composition pipeline (parcourir les +//! surfaces dans l'ordre Z et copier les buffers dans un framebuffer) est +//! reporté à 6.2 ; le frontend Wayland (mappe `wl_compositor` / +//! `wl_shm` / `wl_surface` vers ce core) à 6.4. +//! +//! ## Modèle d'état +//! +//! Chaque `Surface` a deux états : `current` (ce qui est affiché) et +//! `pending` (modifications en cours). `commit()` copie pending → current. +//! C'est la sémantique Wayland : un client peut faire `set_position + +//! set_buffer + ...` puis `commit()` pour appliquer atomiquement. +//! +//! Pour 6.1 l'implémentation est triviale (clone), pas de double-buffer +//! atomique. Suffisant tant qu'on n'a qu'un thread compositor. +//! +//! ## Z-order +//! +//! Maintenu par le `SurfaceRegistry` sous forme de `Vec` du +//! plus bas (index 0) au plus haut (dernier). `raise()` retire et pousse +//! en fin. Pas de modal / always-on-top à ce stade — la politique sera +//! ajoutée au-dessus quand on aura un cas d'usage. + +#![forbid(unsafe_code)] + +use std::collections::HashMap; +use std::sync::Arc; + +/// Identifiant opaque d'une surface. +/// +/// Newtype pour éviter de mélanger avec d'autres `usize`. Réutilisable +/// après destruction (le registry ne garantit pas l'unicité absolue ; +/// c'est au caller de ne pas réutiliser un id qu'il a `destroy()`é). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct SurfaceId(u64); + +impl SurfaceId { + pub fn raw(self) -> u64 { + self.0 + } +} + +/// Buffer ARGB partagé pour une surface. +/// +/// `Arc<...>` pour permettre à plusieurs sites (registry, composition, +/// futur frame callback) de partager sans copier. Le contenu est +/// supposé déjà au format ARGB8888 (a en haut, b en bas dans un u32 LE). +/// +/// Pour 6.1 on utilise un `Vec` simple. En 6.4, le frontend Wayland +/// remplacera ça par un wrapper sur `wl_shm_pool` mmap'd, sans changer +/// l'API : seul le constructeur diffère. +#[derive(Debug, Clone)] +pub struct SurfaceBuffer { + pub pixels: Arc>, + pub width: u32, + pub height: u32, +} + +impl SurfaceBuffer { + pub fn new_filled(width: u32, height: u32, color: u32) -> Self { + let n = (width as usize) * (height as usize); + Self { + pixels: Arc::new(vec![color; n]), + width, + height, + } + } + + pub fn from_pixels(width: u32, height: u32, pixels: Vec) -> Self { + debug_assert_eq!(pixels.len(), (width as usize) * (height as usize)); + Self { + pixels: Arc::new(pixels), + width, + height, + } + } +} + +/// État applicable à une surface : position dans l'écran + buffer + visibilité. +/// +/// L'état est dupliqué entre `pending` et `current` au sein de la struct +/// `Surface`. `commit()` propage pending → current. +#[derive(Debug, Clone, Default)] +pub struct SurfaceState { + /// Position absolue du coin haut-gauche dans le framebuffer cible. + pub x: i32, + pub y: i32, + /// Buffer ARGB. `None` = surface vide (rendue noire ou skip). + pub buffer: Option, + /// Si `false`, la surface est ignorée pendant la composition. + pub visible: bool, +} + +#[derive(Debug)] +pub struct Surface { + id: SurfaceId, + /// État actuellement affiché. + current: SurfaceState, + /// État en cours de modification ; appliqué par `commit()`. + pending: SurfaceState, +} + +impl Surface { + fn new(id: SurfaceId) -> Self { + Self { + id, + current: SurfaceState::default(), + pending: SurfaceState::default(), + } + } + + pub fn id(&self) -> SurfaceId { + self.id + } + + pub fn current(&self) -> &SurfaceState { + &self.current + } + + pub fn pending(&self) -> &SurfaceState { + &self.pending + } + + pub fn pending_mut(&mut self) -> &mut SurfaceState { + &mut self.pending + } + + /// Applique `pending` → `current`. Sémantique Wayland : + /// les modifications sont visibles seulement après commit. + pub fn commit(&mut self) { + self.current = self.pending.clone(); + } +} + +/// Registre central de toutes les surfaces + leur ordre Z. +/// +/// L'ordre Z est maintenu par `z_order: Vec` allant du plus +/// bas (index 0) au plus haut (dernier index = au premier plan). +#[derive(Debug, Default)] +pub struct SurfaceRegistry { + surfaces: HashMap, + z_order: Vec, + next_id: u64, +} + +impl SurfaceRegistry { + pub fn new() -> Self { + Self::default() + } + + /// Crée une nouvelle surface, l'insère au-dessus (top du Z-order). + pub fn create(&mut self) -> SurfaceId { + let id = SurfaceId(self.next_id); + self.next_id += 1; + self.surfaces.insert(id, Surface::new(id)); + self.z_order.push(id); + id + } + + /// Détruit la surface ; le SurfaceId ne doit plus être utilisé. + pub fn destroy(&mut self, id: SurfaceId) -> bool { + let removed = self.surfaces.remove(&id).is_some(); + self.z_order.retain(|&s| s != id); + removed + } + + /// Amène la surface au premier plan (top du Z-order). No-op si absent. + pub fn raise(&mut self, id: SurfaceId) { + if !self.surfaces.contains_key(&id) { + return; + } + self.z_order.retain(|&s| s != id); + self.z_order.push(id); + } + + pub fn get(&self, id: SurfaceId) -> Option<&Surface> { + self.surfaces.get(&id) + } + + pub fn get_mut(&mut self, id: SurfaceId) -> Option<&mut Surface> { + self.surfaces.get_mut(&id) + } + + /// Itérer les surfaces dans l'ordre Z, du fond vers l'avant. + /// C'est l'ordre de composition : on peint d'abord le fond, puis + /// chaque surface au-dessus. + pub fn iter_z_order_back_to_front(&self) -> impl Iterator { + self.z_order + .iter() + .filter_map(|id| self.surfaces.get(id)) + } + + pub fn len(&self) -> usize { + self.surfaces.len() + } + + pub fn is_empty(&self) -> bool { + self.surfaces.is_empty() + } + + /// Convenience : applique pending → current sur une surface donnée. + /// Retourne `false` si la surface n'existe pas. + pub fn commit(&mut self, id: SurfaceId) -> bool { + match self.surfaces.get_mut(&id) { + Some(s) => { + s.commit(); + true + } + None => false, + } + } + + /// Convenience : modifie le pending state via une closure. + /// Pratique pour `set_position`, `set_buffer` etc. dans les tests. + pub fn modify_pending( + &mut self, + id: SurfaceId, + f: F, + ) -> bool { + match self.surfaces.get_mut(&id) { + Some(s) => { + f(&mut s.pending); + true + } + None => false, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn create_returns_unique_ids() { + let mut r = SurfaceRegistry::new(); + let a = r.create(); + let b = r.create(); + let c = r.create(); + assert_ne!(a, b); + assert_ne!(b, c); + assert_ne!(a, c); + assert_eq!(r.len(), 3); + } + + #[test] + fn destroy_removes_surface_and_zorder_entry() { + let mut r = SurfaceRegistry::new(); + let a = r.create(); + let b = r.create(); + assert_eq!(r.len(), 2); + assert!(r.destroy(a)); + assert_eq!(r.len(), 1); + assert!(r.get(a).is_none()); + assert!(r.get(b).is_some()); + // Re-destroy returns false (no-op) + assert!(!r.destroy(a)); + } + + #[test] + fn z_order_initial_is_creation_order_back_to_front() { + let mut r = SurfaceRegistry::new(); + let a = r.create(); + let b = r.create(); + let c = r.create(); + let order: Vec = r + .iter_z_order_back_to_front() + .map(|s| s.id()) + .collect(); + assert_eq!(order, vec![a, b, c]); // a au fond, c devant + } + + #[test] + fn raise_brings_surface_to_top() { + let mut r = SurfaceRegistry::new(); + let a = r.create(); + let b = r.create(); + let c = r.create(); + r.raise(a); // a passe au-dessus + let order: Vec = r + .iter_z_order_back_to_front() + .map(|s| s.id()) + .collect(); + assert_eq!(order, vec![b, c, a]); + } + + #[test] + fn raise_is_idempotent_on_top_surface() { + let mut r = SurfaceRegistry::new(); + let a = r.create(); + let b = r.create(); + r.raise(b); + r.raise(b); // déjà au top + let order: Vec = r + .iter_z_order_back_to_front() + .map(|s| s.id()) + .collect(); + assert_eq!(order, vec![a, b]); + } + + #[test] + fn raise_unknown_id_is_noop() { + let mut r = SurfaceRegistry::new(); + let a = r.create(); + let bogus = SurfaceId(999); + r.raise(bogus); // ne doit pas paniquer ni modifier z_order + let order: Vec = r + .iter_z_order_back_to_front() + .map(|s| s.id()) + .collect(); + assert_eq!(order, vec![a]); + } + + #[test] + fn pending_state_is_separate_from_current() { + let mut r = SurfaceRegistry::new(); + let a = r.create(); + r.modify_pending(a, |s| { + s.x = 100; + s.y = 200; + s.visible = true; + }); + let s = r.get(a).unwrap(); + assert_eq!(s.current().x, 0); // pas encore commité + assert_eq!(s.current().y, 0); + assert!(!s.current().visible); + assert_eq!(s.pending().x, 100); + assert_eq!(s.pending().y, 200); + assert!(s.pending().visible); + } + + #[test] + fn commit_propagates_pending_to_current() { + let mut r = SurfaceRegistry::new(); + let a = r.create(); + r.modify_pending(a, |s| { + s.x = 42; + s.y = 73; + s.visible = true; + s.buffer = Some(SurfaceBuffer::new_filled(10, 10, 0xFFFF0000)); + }); + assert!(r.commit(a)); + let s = r.get(a).unwrap(); + assert_eq!(s.current().x, 42); + assert_eq!(s.current().y, 73); + assert!(s.current().visible); + assert!(s.current().buffer.is_some()); + // Le pending state reste — un commit() suivant ré-appliquera la même + // chose si on ne modifie pas pending entre-temps. C'est le comportement + // Wayland : pending n'est pas vidé après commit. + assert_eq!(s.pending().x, 42); + } + + #[test] + fn commit_unknown_id_returns_false() { + let mut r = SurfaceRegistry::new(); + assert!(!r.commit(SurfaceId(0))); + } + + #[test] + fn surface_buffer_filled_has_correct_size() { + let buf = SurfaceBuffer::new_filled(4, 3, 0xDEADBEEF); + assert_eq!(buf.width, 4); + assert_eq!(buf.height, 3); + assert_eq!(buf.pixels.len(), 12); + assert!(buf.pixels.iter().all(|&p| p == 0xDEADBEEF)); + } + + #[test] + fn destroyed_surface_is_skipped_during_iteration() { + let mut r = SurfaceRegistry::new(); + let a = r.create(); + let b = r.create(); + let c = r.create(); + r.destroy(b); + let ids: Vec = r + .iter_z_order_back_to_front() + .map(|s| s.id()) + .collect(); + assert_eq!(ids, vec![a, c]); + } + + #[test] + fn typical_compositor_workflow() { + // Scénario : 3 fenêtres, on les modifie + commit, on raise une au top + let mut r = SurfaceRegistry::new(); + let bg = r.create(); + let fg1 = r.create(); + let fg2 = r.create(); + + // Configure background + r.modify_pending(bg, |s| { + s.x = 0; + s.y = 0; + s.visible = true; + s.buffer = Some(SurfaceBuffer::new_filled(800, 600, 0xFF202020)); + }); + r.commit(bg); + + // Window 1 (rouge) à (100, 100) 200x150 + r.modify_pending(fg1, |s| { + s.x = 100; + s.y = 100; + s.visible = true; + s.buffer = Some(SurfaceBuffer::new_filled(200, 150, 0xFFFF0000)); + }); + r.commit(fg1); + + // Window 2 (verte) à (250, 200) 200x150 — chevauche partiellement fg1 + r.modify_pending(fg2, |s| { + s.x = 250; + s.y = 200; + s.visible = true; + s.buffer = Some(SurfaceBuffer::new_filled(200, 150, 0xFF00FF00)); + }); + r.commit(fg2); + + // L'ordre attendu pour la composition : bg → fg1 → fg2 (vert au-dessus) + let ids: Vec = r + .iter_z_order_back_to_front() + .map(|s| s.id()) + .collect(); + assert_eq!(ids, vec![bg, fg1, fg2]); + + // Click sur fg1 → raise → fg1 passe au-dessus + r.raise(fg1); + let ids: Vec = r + .iter_z_order_back_to_front() + .map(|s| s.id()) + .collect(); + assert_eq!(ids, vec![bg, fg2, fg1]); + + // Vérifie que le current state est bien défini après les commits + for &id in &[bg, fg1, fg2] { + let s = r.get(id).unwrap(); + assert!(s.current().visible); + assert!(s.current().buffer.is_some()); + } + } +}