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:
Votre Nom 2026-05-09 11:37:25 +02:00
parent a9bb88d9f3
commit e3e554ac92
2 changed files with 451 additions and 0 deletions

View 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.

View 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());
}
}
}