Captures preuves dans docs/phase6-3-*.png : 4 frames qui prouvent
visuellement que raise change l'ordre Z et que compose_into propage le
résultat à l'écran QEMU :
- default-z.png : 3 surfaces overlap, blue top (créé en dernier)
- red-top.png : sendkey 1 → raise(red) → red couvre vert et bleu
- green-top.png : sendkey 2 → raise(green) → green couvre tout dans sa zone
- blue-top.png : sendkey 3 → raise(blue) → retour visuel à initial
Modifications :
compositor-core (commit dbf3bff → maintenant) :
- + iter_z_order_front_to_back() : utile pour hit testing
- + hit_test(x, y) -> Option<SurfaceId> : trouve la surface visible la
plus haute qui contient le point
- + 4 tests unitaires : 27 total / 27 pass natif (0.00s)
redox-wl-display :
- + dep redox-wl-compositor-core
- + impl Framebuffer for RedoxOutput (délègue à pixels_mut + width/height)
bin redox-wl-test-compose-static (190 lignes) :
- ouvre RedoxOutput + take_crtc
- crée InputBackend partagé
- 3 surfaces ARGB unies (rouge/vert/bleu) avec overlap centré
- boucle event : '1'/'2'/'3' raise resp. red/green/blue
- clic souris → hit_test puis raise (motion non testé sans usb-tablet)
- ré-render seulement si raise → économie CPU
- present_with_takeover() à chaque iter pour tenir le CRTC
Validation QEMU automatisée : sendkey 1/2/3 + screendump entre chaque.
Les 4 PNG montrent l'ordre Z évoluer correctement.
Image Redox restaurée à boot Orbital normal.
docs/phase6-compositor-core.md : compte-rendu 6.1-6.3 complet,
architecture, dépendances, API, limitations, plan 6.4.
Phase 6.3 close. Reste 6.4 (frontend Wayland : wl_compositor + wl_shm
+ xdg-shell mappés vers compositor-core, damage tracking, frame
callbacks). Estimé 2-3 sessions.
Leyoda 2026 – GPLv3
869 lines
28 KiB
Rust
869 lines
28 KiB
Rust
//! 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))
|
||
}
|
||
|
||
/// Itérer les surfaces dans l'ordre Z, du premier plan vers le fond.
|
||
/// Utile pour le hit testing : on cherche la première surface
|
||
/// (la plus haute) qui contient le point.
|
||
pub fn iter_z_order_front_to_back(&self) -> impl Iterator<Item = &Surface> {
|
||
self.z_order
|
||
.iter()
|
||
.rev()
|
||
.filter_map(|id| self.surfaces.get(id))
|
||
}
|
||
|
||
/// Trouve la surface visible la plus haute qui contient le point `(x, y)`
|
||
/// dans son rect [state.x..state.x+buffer.width) × [state.y..state.y+buffer.height).
|
||
/// Retourne `None` si aucune surface ne couvre ce point.
|
||
/// Surfaces invisibles ou sans buffer sont ignorées.
|
||
pub fn hit_test(&self, x: i32, y: i32) -> Option<SurfaceId> {
|
||
for surface in self.iter_z_order_front_to_back() {
|
||
let s = surface.current();
|
||
if !s.visible {
|
||
continue;
|
||
}
|
||
let Some(buf) = &s.buffer else {
|
||
continue;
|
||
};
|
||
let x0 = s.x;
|
||
let y0 = s.y;
|
||
let x1 = x0.saturating_add(buf.width as i32);
|
||
let y1 = y0.saturating_add(buf.height as i32);
|
||
if x >= x0 && x < x1 && y >= y0 && y < y1 {
|
||
return Some(surface.id());
|
||
}
|
||
}
|
||
None
|
||
}
|
||
|
||
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,
|
||
}
|
||
}
|
||
|
||
/// Compose les surfaces visibles dans le `Framebuffer` cible.
|
||
///
|
||
/// Itère du fond vers l'avant (z-order), et copie chaque buffer ARGB
|
||
/// à la position `(state.x, state.y)` du framebuffer cible. Les pixels
|
||
/// hors limites du framebuffer sont clippés. Les surfaces sans buffer
|
||
/// ou marquées `visible = false` sont ignorées.
|
||
///
|
||
/// **Pas de blending alpha en 6.2** : overwrite pur. Si tu veux un
|
||
/// fond, peins-le directement dans le framebuffer avant cet appel.
|
||
/// L'alpha viendra en 6.4 si nécessaire (Wayland clients peuvent
|
||
/// produire des surfaces transparentes via `wl_compositor.create_region`).
|
||
///
|
||
/// **Pas de damage tracking** : recompose tout. Suffisant pour 2-3
|
||
/// surfaces ; à reconsidérer en 6.4 quand le frontend Wayland
|
||
/// fournira `wl_surface.damage_buffer`.
|
||
pub fn compose_into<F: Framebuffer + ?Sized>(&self, target: &mut F) {
|
||
let fb_w = target.width() as i32;
|
||
let fb_h = target.height() as i32;
|
||
if fb_w <= 0 || fb_h <= 0 {
|
||
return;
|
||
}
|
||
let fb_w_us = fb_w as usize;
|
||
|
||
for surface in self.iter_z_order_back_to_front() {
|
||
let state = surface.current();
|
||
if !state.visible {
|
||
continue;
|
||
}
|
||
let Some(buf) = &state.buffer else {
|
||
continue;
|
||
};
|
||
let s_w = buf.width as i32;
|
||
let s_h = buf.height as i32;
|
||
if s_w <= 0 || s_h <= 0 {
|
||
continue;
|
||
}
|
||
|
||
// Rectangle de la surface dans les coords du framebuffer.
|
||
let surf_x0 = state.x;
|
||
let surf_y0 = state.y;
|
||
let surf_x1 = surf_x0.saturating_add(s_w);
|
||
let surf_y1 = surf_y0.saturating_add(s_h);
|
||
|
||
// Intersection avec le framebuffer.
|
||
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 {
|
||
continue; // surface entièrement hors écran
|
||
}
|
||
|
||
// Offset dans la surface (clip négatif sur le coin haut-gauche).
|
||
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 = s_w as usize;
|
||
|
||
let dst_pixels = target.pixels_mut();
|
||
let src_pixels = buf.pixels.as_ref();
|
||
|
||
for row in 0..copy_h {
|
||
let src_row_start = (src_y0 + row) * s_w_us + src_x0;
|
||
let dst_y = (dst_y0 as usize) + row;
|
||
let dst_row_start = dst_y * fb_w_us + (dst_x0 as usize);
|
||
let src_slice = &src_pixels[src_row_start..src_row_start + copy_w];
|
||
let dst_slice = &mut dst_pixels[dst_row_start..dst_row_start + copy_w];
|
||
dst_slice.copy_from_slice(src_slice);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Cible de composition. Implémenté par les backends display
|
||
/// (`redox-wl-display::RedoxOutput` plus tard) et par les mocks de test.
|
||
///
|
||
/// `pixels_mut()` retourne un buffer linéaire ARGB8888 de taille exacte
|
||
/// `width * height` u32. Le format mémoire est row-major sans padding ;
|
||
/// si un backend a un stride différent il doit faire son propre
|
||
/// arrangement avant l'appel.
|
||
pub trait Framebuffer {
|
||
fn width(&self) -> u32;
|
||
fn height(&self) -> u32;
|
||
fn pixels_mut(&mut self) -> &mut [u32];
|
||
}
|
||
|
||
#[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]);
|
||
}
|
||
|
||
/// Framebuffer mock pour les tests de composition. Stocke les pixels
|
||
/// dans un Vec<u32> aligné row-major (pas de stride).
|
||
struct MockFb {
|
||
w: u32,
|
||
h: u32,
|
||
pixels: Vec<u32>,
|
||
}
|
||
impl MockFb {
|
||
fn new(w: u32, h: u32, fill: u32) -> Self {
|
||
let n = (w as usize) * (h as usize);
|
||
Self {
|
||
w,
|
||
h,
|
||
pixels: vec![fill; n],
|
||
}
|
||
}
|
||
fn pixel_at(&self, x: u32, y: u32) -> u32 {
|
||
self.pixels[(y as usize) * (self.w as usize) + (x as usize)]
|
||
}
|
||
}
|
||
impl Framebuffer for MockFb {
|
||
fn width(&self) -> u32 {
|
||
self.w
|
||
}
|
||
fn height(&self) -> u32 {
|
||
self.h
|
||
}
|
||
fn pixels_mut(&mut self) -> &mut [u32] {
|
||
&mut self.pixels
|
||
}
|
||
}
|
||
|
||
/// Helper : crée une surface visible avec un buffer plein de `color`,
|
||
/// commit et retourne son SurfaceId.
|
||
fn add_surface(
|
||
r: &mut SurfaceRegistry,
|
||
x: i32,
|
||
y: i32,
|
||
w: u32,
|
||
h: u32,
|
||
color: u32,
|
||
) -> SurfaceId {
|
||
let id = r.create();
|
||
r.modify_pending(id, |s| {
|
||
s.x = x;
|
||
s.y = y;
|
||
s.visible = true;
|
||
s.buffer = Some(SurfaceBuffer::new_filled(w, h, color));
|
||
});
|
||
r.commit(id);
|
||
id
|
||
}
|
||
|
||
const BG: u32 = 0xCAFEBABE;
|
||
|
||
#[test]
|
||
fn compose_empty_registry_keeps_fb_unchanged() {
|
||
let r = SurfaceRegistry::new();
|
||
let mut fb = MockFb::new(10, 10, BG);
|
||
r.compose_into(&mut fb);
|
||
assert!(fb.pixels.iter().all(|&p| p == BG));
|
||
}
|
||
|
||
#[test]
|
||
fn compose_one_fullscreen_surface_fills_fb() {
|
||
let mut r = SurfaceRegistry::new();
|
||
add_surface(&mut r, 0, 0, 10, 10, 0xFFFF0000);
|
||
let mut fb = MockFb::new(10, 10, BG);
|
||
r.compose_into(&mut fb);
|
||
assert!(fb.pixels.iter().all(|&p| p == 0xFFFF0000));
|
||
}
|
||
|
||
#[test]
|
||
fn compose_partial_surface_only_modifies_its_rect() {
|
||
let mut r = SurfaceRegistry::new();
|
||
// Surface 4x3 à (2, 1) — couvre les pixels [2..6) × [1..4)
|
||
add_surface(&mut r, 2, 1, 4, 3, 0xFF00FF00);
|
||
let mut fb = MockFb::new(10, 10, BG);
|
||
r.compose_into(&mut fb);
|
||
|
||
// Vérifie pixel par pixel
|
||
for y in 0..10u32 {
|
||
for x in 0..10u32 {
|
||
let expected = if (2..6).contains(&x) && (1..4).contains(&y) {
|
||
0xFF00FF00
|
||
} else {
|
||
BG
|
||
};
|
||
let got = fb.pixel_at(x, y);
|
||
assert_eq!(got, expected, "mismatch at ({x},{y})");
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn compose_clips_at_right_edge() {
|
||
let mut r = SurfaceRegistry::new();
|
||
// Surface 5x5 à (8, 8) dans un fb 10x10 : déborde à droite de 3 et en bas de 3
|
||
add_surface(&mut r, 8, 8, 5, 5, 0xFF0000FF);
|
||
let mut fb = MockFb::new(10, 10, BG);
|
||
r.compose_into(&mut fb);
|
||
|
||
// Doit avoir peint [8..10) × [8..10) seulement (4 pixels)
|
||
for y in 0..10u32 {
|
||
for x in 0..10u32 {
|
||
let expected = if (8..10).contains(&x) && (8..10).contains(&y) {
|
||
0xFF0000FF
|
||
} else {
|
||
BG
|
||
};
|
||
assert_eq!(fb.pixel_at(x, y), expected, "mismatch at ({x},{y})");
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn compose_clips_at_left_top_with_negative_position() {
|
||
let mut r = SurfaceRegistry::new();
|
||
// Surface 4x4 à (-2, -1) : seul le rect [0..2) × [0..3) doit être peint
|
||
add_surface(&mut r, -2, -1, 4, 4, 0xFFFFFF00);
|
||
let mut fb = MockFb::new(10, 10, BG);
|
||
r.compose_into(&mut fb);
|
||
|
||
for y in 0..10u32 {
|
||
for x in 0..10u32 {
|
||
let expected = if (0..2).contains(&x) && (0..3).contains(&y) {
|
||
0xFFFFFF00
|
||
} else {
|
||
BG
|
||
};
|
||
assert_eq!(fb.pixel_at(x, y), expected, "mismatch at ({x},{y})");
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn compose_skips_offscreen_surface() {
|
||
let mut r = SurfaceRegistry::new();
|
||
// Surface 3x3 entièrement à droite du fb 10x10 (à x=15)
|
||
add_surface(&mut r, 15, 5, 3, 3, 0xFFFF00FF);
|
||
let mut fb = MockFb::new(10, 10, BG);
|
||
r.compose_into(&mut fb);
|
||
assert!(fb.pixels.iter().all(|&p| p == BG));
|
||
}
|
||
|
||
#[test]
|
||
fn compose_skips_invisible_surface() {
|
||
let mut r = SurfaceRegistry::new();
|
||
let id = add_surface(&mut r, 0, 0, 10, 10, 0xFFFF0000);
|
||
// Override : invisible
|
||
r.modify_pending(id, |s| s.visible = false);
|
||
r.commit(id);
|
||
|
||
let mut fb = MockFb::new(10, 10, BG);
|
||
r.compose_into(&mut fb);
|
||
assert!(fb.pixels.iter().all(|&p| p == BG));
|
||
}
|
||
|
||
#[test]
|
||
fn compose_skips_surface_without_buffer() {
|
||
let mut r = SurfaceRegistry::new();
|
||
let id = r.create();
|
||
r.modify_pending(id, |s| {
|
||
s.visible = true;
|
||
s.x = 0;
|
||
s.y = 0;
|
||
// pas de buffer
|
||
});
|
||
r.commit(id);
|
||
let mut fb = MockFb::new(5, 5, BG);
|
||
r.compose_into(&mut fb);
|
||
assert!(fb.pixels.iter().all(|&p| p == BG));
|
||
}
|
||
|
||
#[test]
|
||
fn compose_respects_z_order_top_overwrites_bottom() {
|
||
let mut r = SurfaceRegistry::new();
|
||
// bg rouge plein écran
|
||
add_surface(&mut r, 0, 0, 5, 5, 0xFFFF0000);
|
||
// overlay bleu 3x3 à (1, 1) — créé après donc au-dessus
|
||
add_surface(&mut r, 1, 1, 3, 3, 0xFF0000FF);
|
||
|
||
let mut fb = MockFb::new(5, 5, BG);
|
||
r.compose_into(&mut fb);
|
||
|
||
for y in 0..5u32 {
|
||
for x in 0..5u32 {
|
||
let expected = if (1..4).contains(&x) && (1..4).contains(&y) {
|
||
0xFF0000FF // bleu au-dessus
|
||
} else {
|
||
0xFFFF0000 // rouge dessous
|
||
};
|
||
assert_eq!(fb.pixel_at(x, y), expected);
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn compose_after_raise_changes_visible_overlap() {
|
||
let mut r = SurfaceRegistry::new();
|
||
let red = add_surface(&mut r, 0, 0, 5, 5, 0xFFFF0000);
|
||
let blue = add_surface(&mut r, 1, 1, 3, 3, 0xFF0000FF);
|
||
|
||
// Premier rendu : bleu au-dessus
|
||
let mut fb1 = MockFb::new(5, 5, BG);
|
||
r.compose_into(&mut fb1);
|
||
assert_eq!(fb1.pixel_at(2, 2), 0xFF0000FF);
|
||
|
||
// raise red → red passe au-dessus
|
||
r.raise(red);
|
||
|
||
let mut fb2 = MockFb::new(5, 5, BG);
|
||
r.compose_into(&mut fb2);
|
||
// Maintenant le rouge couvre tout, y compris le bleu
|
||
assert_eq!(fb2.pixel_at(2, 2), 0xFFFF0000);
|
||
let _ = blue; // tag "fait expressément"
|
||
}
|
||
|
||
#[test]
|
||
fn compose_uses_current_state_not_pending() {
|
||
let mut r = SurfaceRegistry::new();
|
||
let id = add_surface(&mut r, 0, 0, 5, 5, 0xFFFF0000);
|
||
|
||
// Modifier pending sans commit
|
||
r.modify_pending(id, |s| {
|
||
s.buffer = Some(SurfaceBuffer::new_filled(5, 5, 0xFF00FF00));
|
||
});
|
||
|
||
let mut fb = MockFb::new(5, 5, BG);
|
||
r.compose_into(&mut fb);
|
||
// Doit toujours être rouge (current), pas vert (pending)
|
||
assert!(fb.pixels.iter().all(|&p| p == 0xFFFF0000));
|
||
|
||
// Après commit, vert
|
||
r.commit(id);
|
||
let mut fb2 = MockFb::new(5, 5, BG);
|
||
r.compose_into(&mut fb2);
|
||
assert!(fb2.pixels.iter().all(|&p| p == 0xFF00FF00));
|
||
}
|
||
|
||
#[test]
|
||
fn iter_z_order_front_to_back_is_reverse() {
|
||
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_front_to_back()
|
||
.map(|s| s.id())
|
||
.collect();
|
||
assert_eq!(order, vec![c, b, a]);
|
||
}
|
||
|
||
#[test]
|
||
fn hit_test_returns_topmost_surface_at_point() {
|
||
let mut r = SurfaceRegistry::new();
|
||
// bg : 100x100 à (0, 0)
|
||
let bg = add_surface(&mut r, 0, 0, 100, 100, 0xFFFF0000);
|
||
// overlay : 30x30 à (10, 10)
|
||
let overlay = add_surface(&mut r, 10, 10, 30, 30, 0xFF0000FF);
|
||
|
||
// Point (5, 5) : dans bg uniquement
|
||
assert_eq!(r.hit_test(5, 5), Some(bg));
|
||
// Point (20, 20) : dans les deux, overlay au-dessus → overlay
|
||
assert_eq!(r.hit_test(20, 20), Some(overlay));
|
||
// Point (50, 50) : dans bg seulement
|
||
assert_eq!(r.hit_test(50, 50), Some(bg));
|
||
// Point (200, 200) : hors écran
|
||
assert_eq!(r.hit_test(200, 200), None);
|
||
// Point (-1, -1) : hors écran (négatif)
|
||
assert_eq!(r.hit_test(-1, -1), None);
|
||
}
|
||
|
||
#[test]
|
||
fn hit_test_skips_invisible_and_no_buffer() {
|
||
let mut r = SurfaceRegistry::new();
|
||
let a = add_surface(&mut r, 0, 0, 100, 100, 0xFFFF0000);
|
||
let b = add_surface(&mut r, 0, 0, 100, 100, 0xFF00FF00);
|
||
// Cache b → hit_test au-dessus de la zone doit retourner a
|
||
r.modify_pending(b, |s| s.visible = false);
|
||
r.commit(b);
|
||
assert_eq!(r.hit_test(50, 50), Some(a));
|
||
|
||
// c sans buffer
|
||
let c = r.create();
|
||
r.modify_pending(c, |s| {
|
||
s.x = 0;
|
||
s.y = 0;
|
||
s.visible = true;
|
||
});
|
||
r.commit(c);
|
||
// toujours a (c n'a pas de buffer donc skip)
|
||
assert_eq!(r.hit_test(50, 50), Some(a));
|
||
}
|
||
|
||
#[test]
|
||
fn hit_test_after_raise_returns_raised_surface() {
|
||
let mut r = SurfaceRegistry::new();
|
||
let a = add_surface(&mut r, 0, 0, 100, 100, 0xFFFF0000);
|
||
let b = add_surface(&mut r, 0, 0, 100, 100, 0xFF00FF00);
|
||
// b est au-dessus initialement
|
||
assert_eq!(r.hit_test(50, 50), Some(b));
|
||
r.raise(a);
|
||
// a passe au-dessus
|
||
assert_eq!(r.hit_test(50, 50), Some(a));
|
||
}
|
||
|
||
#[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());
|
||
}
|
||
}
|
||
}
|