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:
parent
e3e554ac92
commit
dbf3bffa2b
1 changed files with 326 additions and 0 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue