From dbf3bffa2bbf29c053d74d9797696317a72879ae Mon Sep 17 00:00:00 2001 From: Votre Nom Date: Sat, 9 May 2026 11:52:40 +0200 Subject: [PATCH] =?UTF-8?q?Phase=206.2=20=E2=80=94=20composition=20pipelin?= =?UTF-8?q?e=20+=2011=20tests=20unitaires?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. SurfaceRegistry::compose_into(&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 --- crates/redox-wl-compositor-core/src/lib.rs | 326 +++++++++++++++++++++ 1 file changed, 326 insertions(+) diff --git a/crates/redox-wl-compositor-core/src/lib.rs b/crates/redox-wl-compositor-core/src/lib.rs index 0a52688..5ae5e62 100644 --- a/crates/redox-wl-compositor-core/src/lib.rs +++ b/crates/redox-wl-compositor-core/src/lib.rs @@ -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(&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 aligné row-major (pas de stride). + struct MockFb { + w: u32, + h: u32, + pixels: Vec, + } + 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