Phase 6.1 — compositor-core skeleton + 12 tests unitaires
Crate redox-wl-compositor-core (lib pure Rust, sans deps externes) : - SurfaceId : newtype u64 opaque - SurfaceBuffer : Arc<Vec<u32>> ARGB8888 + width/height - SurfaceState : x, y, buffer, visible - Surface : id + current + pending + commit() - SurfaceRegistry : HashMap<SurfaceId, Surface> + 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
This commit is contained in:
parent
a9bb88d9f3
commit
e3e554ac92
2 changed files with 451 additions and 0 deletions
9
crates/redox-wl-compositor-core/Cargo.toml
Normal file
9
crates/redox-wl-compositor-core/Cargo.toml
Normal file
|
|
@ -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.
|
||||
442
crates/redox-wl-compositor-core/src/lib.rs
Normal file
442
crates/redox-wl-compositor-core/src/lib.rs
Normal file
|
|
@ -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<SurfaceId>` 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<u32>` 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<Vec<u32>>,
|
||||
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<u32>) -> 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<SurfaceBuffer>,
|
||||
/// 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<SurfaceId>` allant du plus
|
||||
/// bas (index 0) au plus haut (dernier index = au premier plan).
|
||||
#[derive(Debug, Default)]
|
||||
pub struct SurfaceRegistry {
|
||||
surfaces: HashMap<SurfaceId, Surface>,
|
||||
z_order: Vec<SurfaceId>,
|
||||
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<Item = &Surface> {
|
||||
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<F: FnOnce(&mut SurfaceState)>(
|
||||
&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<SurfaceId> = 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<SurfaceId> = 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<SurfaceId> = 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<SurfaceId> = 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<SurfaceId> = 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<SurfaceId> = 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<SurfaceId> = 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue