Phase 6.2 — composition pipeline + 11 tests unitaires

Trait Framebuffer dans compositor-core (lib pure Rust):
    pub trait Framebuffer {
        fn width(&self) -> u32;
        fn height(&self) -> u32;
        fn pixels_mut(&mut self) -> &mut [u32];
    }

Backends impl ce trait pour leur framebuffer (RedoxOutput le fera en 6.3).
Tests utilisent un mock MockFb sur Vec<u32>.

SurfaceRegistry::compose_into<F: Framebuffer + ?Sized>(&self, target: &mut F) :
- itère iter_z_order_back_to_front()
- skip surfaces invisible / sans buffer / entièrement offscreen
- clip aux bords du framebuffer (x/y négatifs, débordement droit/bas)
- copie row-major ARGB8888 (overwrite, pas de blending alpha)

Tests ajoutés (11) :
- compose_empty_registry_keeps_fb_unchanged
- compose_one_fullscreen_surface_fills_fb
- compose_partial_surface_only_modifies_its_rect
- compose_clips_at_right_edge
- compose_clips_at_left_top_with_negative_position
- compose_skips_offscreen_surface
- compose_skips_invisible_surface
- compose_skips_surface_without_buffer
- compose_respects_z_order_top_overwrites_bottom
- compose_after_raise_changes_visible_overlap
- compose_uses_current_state_not_pending

Total : 23/23 tests pass cargo test --release (0.00s, c'est dire la
légèreté de la lib). Compile aussi pour x86_64-unknown-redox.

Pas de damage tracking, pas de blending alpha — reportés à 6.4 quand
le frontend Wayland aura besoin de damage_buffer et de surfaces
transparentes.

Phase 6.2 close. Suite : 6.3 — bin redox-wl-test-compose-static qui
impl Framebuffer for RedoxOutput + 3 rectangles synthétiques + raise
au clic via InputBackend + screenshot validation.

Leyoda 2026 – GPLv3
This commit is contained in:
Votre Nom 2026-05-09 11:52:40 +02:00
parent e3e554ac92
commit dbf3bffa2b

View file

@ -227,6 +227,92 @@ impl SurfaceRegistry {
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)]
@ -382,6 +468,246 @@ mod tests {
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 typical_compositor_workflow() {
// Scénario : 3 fenêtres, on les modifie + commit, on raise une au top