🎉 Phase 6.3 — display + input + compositor-core intégrés runtime
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
This commit is contained in:
parent
dbf3bffa2b
commit
509aae7769
10 changed files with 608 additions and 0 deletions
|
|
@ -192,6 +192,40 @@ impl SurfaceRegistry {
|
||||||
.filter_map(|id| self.surfaces.get(id))
|
.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 {
|
pub fn len(&self) -> usize {
|
||||||
self.surfaces.len()
|
self.surfaces.len()
|
||||||
}
|
}
|
||||||
|
|
@ -708,6 +742,73 @@ mod tests {
|
||||||
assert!(fb2.pixels.iter().all(|&p| p == 0xFF00FF00));
|
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]
|
#[test]
|
||||||
fn typical_compositor_workflow() {
|
fn typical_compositor_workflow() {
|
||||||
// Scénario : 3 fenêtres, on les modifie + commit, on raise une au top
|
// Scénario : 3 fenêtres, on les modifie + commit, on raise une au top
|
||||||
|
|
|
||||||
|
|
@ -9,3 +9,4 @@ drm = "0.15"
|
||||||
graphics-ipc = { git = "https://gitlab.redox-os.org/redox-os/base.git" }
|
graphics-ipc = { git = "https://gitlab.redox-os.org/redox-os/base.git" }
|
||||||
inputd = { git = "https://gitlab.redox-os.org/redox-os/base.git" }
|
inputd = { git = "https://gitlab.redox-os.org/redox-os/base.git" }
|
||||||
libredox = "0.1"
|
libredox = "0.1"
|
||||||
|
redox-wl-compositor-core = { path = "../redox-wl-compositor-core" }
|
||||||
|
|
|
||||||
|
|
@ -236,6 +236,23 @@ fn libredox_call_fpath(fd: usize, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
libredox::call::fpath(fd, buf).map_err(|e| io::Error::from_raw_os_error(e.errno()))
|
libredox::call::fpath(fd, buf).map_err(|e| io::Error::from_raw_os_error(e.errno()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Implémentation du trait `Framebuffer` de `compositor-core`. Permet à
|
||||||
|
/// `SurfaceRegistry::compose_into(&mut output)` de copier ses surfaces
|
||||||
|
/// directement dans le buffer ARGB du display Redox.
|
||||||
|
impl redox_wl_compositor_core::Framebuffer for RedoxOutput {
|
||||||
|
fn width(&self) -> u32 {
|
||||||
|
RedoxOutput::width(self)
|
||||||
|
}
|
||||||
|
fn height(&self) -> u32 {
|
||||||
|
RedoxOutput::height(self)
|
||||||
|
}
|
||||||
|
fn pixels_mut(&mut self) -> &mut [u32] {
|
||||||
|
// Délègue à la méthode existante. Panic si CRTC pas pris,
|
||||||
|
// ce qui est cohérent avec l'usage normal (compose après take_crtc).
|
||||||
|
RedoxOutput::pixels_mut(self).expect("Framebuffer::pixels_mut: CRTC not taken")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Drop for RedoxOutput {
|
impl Drop for RedoxOutput {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
if let Some(fb) = self.fb.take() {
|
if let Some(fb) = self.fb.take() {
|
||||||
|
|
|
||||||
9
crates/redox-wl-test-compose-static/Cargo.toml
Normal file
9
crates/redox-wl-test-compose-static/Cargo.toml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
[package]
|
||||||
|
name = "redox-wl-test-compose-static"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
redox-wl-display = { path = "../redox-wl-display" }
|
||||||
|
redox-wl-input = { path = "../redox-wl-input" }
|
||||||
|
redox-wl-compositor-core = { path = "../redox-wl-compositor-core" }
|
||||||
251
crates/redox-wl-test-compose-static/src/main.rs
Normal file
251
crates/redox-wl-test-compose-static/src/main.rs
Normal file
|
|
@ -0,0 +1,251 @@
|
||||||
|
//! Phase 6.3 — Test d'intégration display + input + compositor-core.
|
||||||
|
//!
|
||||||
|
//! 3 surfaces ARGB synthétiques (rouge, vert, bleu) qui se chevauchent
|
||||||
|
//! sur un fond gris foncé. Touche '1', '2', '3' raise resp. la rouge,
|
||||||
|
//! verte, bleue. Clic souris → hit-test et raise la surface ciblée.
|
||||||
|
//! Recompose + present à chaque event.
|
||||||
|
//!
|
||||||
|
//! Ce binaire est la première fois où **tous les morceaux phase 4-6.1-6.2**
|
||||||
|
//! travaillent ensemble :
|
||||||
|
//! - `RedoxOutput` (display backend, 4)
|
||||||
|
//! - `InputBackend` (input, 5)
|
||||||
|
//! - `SurfaceRegistry` + `compose_into` (composition, 6.1-6.2)
|
||||||
|
|
||||||
|
use std::env;
|
||||||
|
use std::fs::OpenOptions;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::process::{Command, ExitCode};
|
||||||
|
use std::sync::{Mutex, OnceLock};
|
||||||
|
use std::thread;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use redox_wl_compositor_core::{Framebuffer as _, SurfaceBuffer, SurfaceId, SurfaceRegistry};
|
||||||
|
use redox_wl_display::RedoxOutput;
|
||||||
|
use redox_wl_input::{InputBackend, InputEvent};
|
||||||
|
|
||||||
|
struct DebugSink(Mutex<Option<std::fs::File>>);
|
||||||
|
impl DebugSink {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self(Mutex::new(
|
||||||
|
OpenOptions::new().write(true).open("/scheme/debug").ok(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
fn writeln(&self, s: &str) {
|
||||||
|
println!("{s}");
|
||||||
|
if let Ok(mut g) = self.0.lock() {
|
||||||
|
if let Some(f) = g.as_mut() {
|
||||||
|
let _ = writeln!(f, "{s}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn dlog(s: &str) {
|
||||||
|
static SINK: OnceLock<DebugSink> = OnceLock::new();
|
||||||
|
SINK.get_or_init(DebugSink::new).writeln(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
const BG_COLOR: u32 = 0xFF202028; // gris foncé bleuté
|
||||||
|
|
||||||
|
fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
dlog("[compose] Phase 6.3 — composition statique 3 surfaces");
|
||||||
|
|
||||||
|
// Display setup ---------------------------------------------------------
|
||||||
|
let mut output = RedoxOutput::open().map_err(|e| format!("RedoxOutput::open: {e}"))?;
|
||||||
|
let our_vt = output.vt();
|
||||||
|
let fb_w = output.width();
|
||||||
|
let fb_h = output.height();
|
||||||
|
dlog(&format!("[compose] display {fb_w}x{fb_h}, VT={our_vt}"));
|
||||||
|
|
||||||
|
let _ = Command::new("inputd")
|
||||||
|
.arg("-A")
|
||||||
|
.arg(our_vt.to_string())
|
||||||
|
.status();
|
||||||
|
thread::sleep(Duration::from_millis(300));
|
||||||
|
|
||||||
|
output
|
||||||
|
.take_crtc()
|
||||||
|
.map_err(|e| format!("take_crtc: {e}"))?;
|
||||||
|
dlog("[compose] CRTC pris");
|
||||||
|
|
||||||
|
// Input setup -----------------------------------------------------------
|
||||||
|
let input = InputBackend::new(output.consumer());
|
||||||
|
dlog("[compose] InputBackend prêt");
|
||||||
|
|
||||||
|
// Compositor scene ------------------------------------------------------
|
||||||
|
let mut registry = SurfaceRegistry::new();
|
||||||
|
|
||||||
|
// Centrer les 3 surfaces avec overlap. Tailles relatives au display.
|
||||||
|
let surf_w = (fb_w / 3).max(200);
|
||||||
|
let surf_h = (fb_h / 3).max(150);
|
||||||
|
let cx = (fb_w / 2) as i32;
|
||||||
|
let cy = (fb_h / 2) as i32;
|
||||||
|
let off = (surf_w / 4) as i32;
|
||||||
|
|
||||||
|
let red = registry.create();
|
||||||
|
registry.modify_pending(red, |s| {
|
||||||
|
s.x = cx - surf_w as i32 / 2 - off;
|
||||||
|
s.y = cy - surf_h as i32 / 2 - off / 2;
|
||||||
|
s.visible = true;
|
||||||
|
s.buffer = Some(SurfaceBuffer::new_filled(surf_w, surf_h, 0xFF_E0_30_30));
|
||||||
|
});
|
||||||
|
registry.commit(red);
|
||||||
|
|
||||||
|
let green = registry.create();
|
||||||
|
registry.modify_pending(green, |s| {
|
||||||
|
s.x = cx - surf_w as i32 / 2;
|
||||||
|
s.y = cy - surf_h as i32 / 2 + off / 2;
|
||||||
|
s.visible = true;
|
||||||
|
s.buffer = Some(SurfaceBuffer::new_filled(surf_w, surf_h, 0xFF_30_C0_30));
|
||||||
|
});
|
||||||
|
registry.commit(green);
|
||||||
|
|
||||||
|
let blue = registry.create();
|
||||||
|
registry.modify_pending(blue, |s| {
|
||||||
|
s.x = cx - surf_w as i32 / 2 + off;
|
||||||
|
s.y = cy - surf_h as i32 / 2 - off / 2;
|
||||||
|
s.visible = true;
|
||||||
|
s.buffer = Some(SurfaceBuffer::new_filled(surf_w, surf_h, 0xFF_30_60_E0));
|
||||||
|
});
|
||||||
|
registry.commit(blue);
|
||||||
|
|
||||||
|
dlog(&format!(
|
||||||
|
"[compose] 3 surfaces créées : red={red:?} green={green:?} blue={blue:?}"
|
||||||
|
));
|
||||||
|
|
||||||
|
let surfaces: [(SurfaceId, &str); 3] = [(red, "red"), (green, "green"), (blue, "blue")];
|
||||||
|
|
||||||
|
// Helper de rendu : clear + compose + present.
|
||||||
|
let render = |output: &mut RedoxOutput, registry: &SurfaceRegistry| -> io::Result<()> {
|
||||||
|
// Clear le framebuffer au fond gris foncé. compose_into est overwrite,
|
||||||
|
// donc il faut peindre le fond avant si on veut une zone non couverte.
|
||||||
|
for p in <RedoxOutput as Framebuffer>::pixels_mut(output).iter_mut() {
|
||||||
|
*p = BG_COLOR;
|
||||||
|
}
|
||||||
|
registry.compose_into(output);
|
||||||
|
output
|
||||||
|
.present_with_takeover()
|
||||||
|
.map_err(|e| io::Error::other(format!("present: {e}")))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Premier rendu
|
||||||
|
render(&mut output, ®istry)?;
|
||||||
|
dlog("[compose] première frame présentée");
|
||||||
|
|
||||||
|
// Mouse position track (pour hit_test)
|
||||||
|
let mut mouse_x: i32 = (fb_w / 2) as i32;
|
||||||
|
let mut mouse_y: i32 = (fb_h / 2) as i32;
|
||||||
|
|
||||||
|
// Boucle principale 30 secondes
|
||||||
|
let start = Instant::now();
|
||||||
|
let total = Duration::from_secs(30);
|
||||||
|
let mut event_count = 0usize;
|
||||||
|
let mut quit = false;
|
||||||
|
|
||||||
|
while start.elapsed() < total && !quit {
|
||||||
|
let events = input.poll().map_err(|e| format!("poll: {e}"))?;
|
||||||
|
let mut need_redraw = false;
|
||||||
|
|
||||||
|
for ev in events {
|
||||||
|
event_count += 1;
|
||||||
|
match ev {
|
||||||
|
InputEvent::Key {
|
||||||
|
character,
|
||||||
|
scancode,
|
||||||
|
pressed,
|
||||||
|
} => {
|
||||||
|
if !pressed {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Touches '1' / '2' / '3' raise la surface correspondante.
|
||||||
|
let target = match character {
|
||||||
|
'1' => Some((red, "red")),
|
||||||
|
'2' => Some((green, "green")),
|
||||||
|
'3' => Some((blue, "blue")),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
if let Some((id, name)) = target {
|
||||||
|
registry.raise(id);
|
||||||
|
dlog(&format!("[compose] raise {name} (key '{character}')"));
|
||||||
|
need_redraw = true;
|
||||||
|
} else if scancode == 0x01 {
|
||||||
|
// Esc → quit
|
||||||
|
dlog("[compose] Esc → quit");
|
||||||
|
quit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InputEvent::PointerMotion { x, y } => {
|
||||||
|
mouse_x = x;
|
||||||
|
mouse_y = y;
|
||||||
|
}
|
||||||
|
InputEvent::PointerMotionRelative { dx, dy } => {
|
||||||
|
mouse_x = (mouse_x + dx).clamp(0, fb_w as i32 - 1);
|
||||||
|
mouse_y = (mouse_y + dy).clamp(0, fb_h as i32 - 1);
|
||||||
|
}
|
||||||
|
InputEvent::PointerButton { left, .. } => {
|
||||||
|
if left {
|
||||||
|
// Hit-test à la position courante
|
||||||
|
if let Some(id) = registry.hit_test(mouse_x, mouse_y) {
|
||||||
|
registry.raise(id);
|
||||||
|
let name = surfaces
|
||||||
|
.iter()
|
||||||
|
.find(|(s, _)| *s == id)
|
||||||
|
.map(|(_, n)| *n)
|
||||||
|
.unwrap_or("unknown");
|
||||||
|
dlog(&format!(
|
||||||
|
"[compose] click at ({mouse_x},{mouse_y}) → raise {name}"
|
||||||
|
));
|
||||||
|
need_redraw = true;
|
||||||
|
} else {
|
||||||
|
dlog(&format!(
|
||||||
|
"[compose] click at ({mouse_x},{mouse_y}) → miss (no surface)"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InputEvent::Quit => {
|
||||||
|
quit = true;
|
||||||
|
}
|
||||||
|
InputEvent::Handoff => {
|
||||||
|
dlog("[compose] handoff received");
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if need_redraw {
|
||||||
|
if let Err(e) = render(&mut output, ®istry) {
|
||||||
|
dlog(&format!("[compose] render error: {e}"));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Re-present sans recomposer pour tenir le CRTC face à fbcond.
|
||||||
|
let _ = output.present_with_takeover();
|
||||||
|
}
|
||||||
|
|
||||||
|
thread::sleep(Duration::from_millis(50));
|
||||||
|
}
|
||||||
|
|
||||||
|
dlog(&format!(
|
||||||
|
"[compose] fin — {event_count} events en {:.1}s",
|
||||||
|
start.elapsed().as_secs_f32()
|
||||||
|
));
|
||||||
|
let _ = env::var("VT");
|
||||||
|
drop(output);
|
||||||
|
thread::sleep(Duration::from_millis(500));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> ExitCode {
|
||||||
|
match run() {
|
||||||
|
Ok(()) => {
|
||||||
|
dlog("[compose] PASS");
|
||||||
|
ExitCode::SUCCESS
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
dlog(&format!("[compose] FAIL: {e}"));
|
||||||
|
ExitCode::FAILURE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use redox_wl_compositor_core::Framebuffer;
|
||||||
|
use std::io;
|
||||||
BIN
docs/phase6-3-blue-top.png
Normal file
BIN
docs/phase6-3-blue-top.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
BIN
docs/phase6-3-default-z.png
Normal file
BIN
docs/phase6-3-default-z.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
BIN
docs/phase6-3-green-top.png
Normal file
BIN
docs/phase6-3-green-top.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
BIN
docs/phase6-3-red-top.png
Normal file
BIN
docs/phase6-3-red-top.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
229
docs/phase6-compositor-core.md
Normal file
229
docs/phase6-compositor-core.md
Normal file
|
|
@ -0,0 +1,229 @@
|
||||||
|
# Phase 6 — Compositor core : résultats
|
||||||
|
|
||||||
|
> Document produit le 2026-05-09 dans le cadre du plan directeur
|
||||||
|
> `REDOX_COSMIC_XWAYLAND_RS_PLAN.md`.
|
||||||
|
>
|
||||||
|
> **Périmètre 6.1-6.3** : structures core, pipeline de composition,
|
||||||
|
> intégration runtime display + input + composition.
|
||||||
|
>
|
||||||
|
> **Hors scope (6.4)** : frontend Wayland (wl_compositor / wl_shm /
|
||||||
|
> xdg-shell), damage tracking, frame callbacks.
|
||||||
|
|
||||||
|
## Verdict 6.1-6.3
|
||||||
|
|
||||||
|
**✅ Cœur compositor fonctionnel et intégré.**
|
||||||
|
|
||||||
|
- 27 tests unitaires sur `compositor-core` (12 structures + 11 composition + 4 hit_test)
|
||||||
|
- `RedoxOutput` impl `Framebuffer` → composition directe dans le framebuffer Redox
|
||||||
|
- Bin d'intégration `redox-wl-test-compose-static` qui combine display + input + compositor
|
||||||
|
|
||||||
|
Captures preuves :
|
||||||
|
|
||||||
|
| Frame | Z-order (bottom → top) | PNG |
|
||||||
|
|---|---|---|
|
||||||
|
| Initial (3 surfaces créées) | red, green, blue | `phase6-3-default-z.png` |
|
||||||
|
| Après `sendkey 1` (raise red) | green, blue, red | `phase6-3-red-top.png` |
|
||||||
|
| Après `sendkey 2` (raise green) | blue, red, green | `phase6-3-green-top.png` |
|
||||||
|
| Après `sendkey 3` (raise blue) | red, green, blue | `phase6-3-blue-top.png` |
|
||||||
|
|
||||||
|
Chaque pression de touche déclenche `registry.raise(id)` puis
|
||||||
|
`registry.compose_into(&mut output)` puis `output.present_with_takeover()`,
|
||||||
|
le tout dans le même binaire qui consomme aussi les events via
|
||||||
|
`InputBackend`.
|
||||||
|
|
||||||
|
## Architecture finale 6.3
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────┐
|
||||||
|
│ redox-wl-test-compose-static (binaire) │
|
||||||
|
│ │
|
||||||
|
│ RedoxOutput ──────┐ │
|
||||||
|
│ (display) │ │
|
||||||
|
│ │ │ Arc<ConsumerHandle> │
|
||||||
|
│ │ impl Framebuffer│ │
|
||||||
|
│ │ ▼ │
|
||||||
|
│ │ InputBackend ──> InputEvent │
|
||||||
|
│ │ │ │
|
||||||
|
│ ▼ ▼ │
|
||||||
|
│ SurfaceRegistry ◄── (raise, hit_test, modify) │
|
||||||
|
│ │ │
|
||||||
|
│ │ compose_into(&mut output) │
|
||||||
|
│ ▼ │
|
||||||
|
│ pixels écrits dans le framebuffer Redox │
|
||||||
|
│ │ │
|
||||||
|
│ └──> present_with_takeover │
|
||||||
|
└──────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Dépendances** (graph propre, pas de cycle) :
|
||||||
|
|
||||||
|
```
|
||||||
|
redox-wl-compositor-core ← lib pure Rust, sans dep externe
|
||||||
|
▲
|
||||||
|
├──── redox-wl-display (impl Framebuffer for RedoxOutput)
|
||||||
|
│
|
||||||
|
└──── redox-wl-test-compose-static (bin)
|
||||||
|
▲
|
||||||
|
└──── redox-wl-input (lib)
|
||||||
|
```
|
||||||
|
|
||||||
|
## API publique 6.1-6.3
|
||||||
|
|
||||||
|
### Types core
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct SurfaceId(u64);
|
||||||
|
|
||||||
|
pub struct SurfaceBuffer {
|
||||||
|
pub pixels: Arc<Vec<u32>>, // ARGB8888
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SurfaceState {
|
||||||
|
pub x: i32,
|
||||||
|
pub y: i32,
|
||||||
|
pub buffer: Option<SurfaceBuffer>,
|
||||||
|
pub visible: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Surface { /* id, current, pending */ }
|
||||||
|
impl Surface {
|
||||||
|
pub fn id(&self) -> SurfaceId;
|
||||||
|
pub fn current(&self) -> &SurfaceState;
|
||||||
|
pub fn pending(&self) -> &SurfaceState;
|
||||||
|
pub fn pending_mut(&mut self) -> &mut SurfaceState;
|
||||||
|
pub fn commit(&mut self);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `SurfaceRegistry`
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct SurfaceRegistry { /* ... */ }
|
||||||
|
impl SurfaceRegistry {
|
||||||
|
pub fn create(&mut self) -> SurfaceId;
|
||||||
|
pub fn destroy(&mut self, id: SurfaceId) -> bool;
|
||||||
|
pub fn raise(&mut self, id: SurfaceId);
|
||||||
|
pub fn get(&self, id: SurfaceId) -> Option<&Surface>;
|
||||||
|
pub fn get_mut(&mut self, id: SurfaceId) -> Option<&mut Surface>;
|
||||||
|
pub fn commit(&mut self, id: SurfaceId) -> bool;
|
||||||
|
pub fn modify_pending<F>(&mut self, id: SurfaceId, f: F) -> bool;
|
||||||
|
|
||||||
|
pub fn iter_z_order_back_to_front(&self) -> impl Iterator<Item = &Surface>;
|
||||||
|
pub fn iter_z_order_front_to_back(&self) -> impl Iterator<Item = &Surface>;
|
||||||
|
pub fn hit_test(&self, x: i32, y: i32) -> Option<SurfaceId>;
|
||||||
|
pub fn compose_into<F: Framebuffer + ?Sized>(&self, target: &mut F);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Trait `Framebuffer`
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub trait Framebuffer {
|
||||||
|
fn width(&self) -> u32;
|
||||||
|
fn height(&self) -> u32;
|
||||||
|
fn pixels_mut(&mut self) -> &mut [u32];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Implémenté pour `RedoxOutput` dans `redox-wl-display`. Le bin d'intégration
|
||||||
|
peut faire `registry.compose_into(&mut output)` directement.
|
||||||
|
|
||||||
|
## Tests unitaires (27 total, 0.00s natif)
|
||||||
|
|
||||||
|
| Catégorie | # | Couverture |
|
||||||
|
|---|---|---|
|
||||||
|
| Surface registry | 6 | create/destroy/raise (idempotent, unknown, etc.) |
|
||||||
|
| Pending/current state | 2 | modification + commit + isolation |
|
||||||
|
| Composition | 9 | empty, fullscreen, partiel, clipping (4 bords), invisible, sans buffer, z-order, current vs pending |
|
||||||
|
| Hit testing | 4 | topmost, skip invisible, after raise, hors écran |
|
||||||
|
| End-to-end | 2 | iter z-order, workflow compositor typique |
|
||||||
|
| **Buffer construction** | 1 | `new_filled` produit bonne taille + couleur |
|
||||||
|
|
||||||
|
Compile aussi pour `x86_64-unknown-redox` (pure Rust, aucune dep system).
|
||||||
|
|
||||||
|
## Méthode validation runtime
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build
|
||||||
|
cd crates/redox-wl-test-compose-static && redoxer build --release
|
||||||
|
|
||||||
|
# Push dans image
|
||||||
|
mount image via redoxfs
|
||||||
|
cp binary into /usr/bin/
|
||||||
|
modify init.d/20_orbital → nowait VT=2 redox-wl-test-compose-static
|
||||||
|
clear init.d/30_console
|
||||||
|
unmount
|
||||||
|
|
||||||
|
# Boot QEMU headless avec capture
|
||||||
|
qemu ... -display none -monitor unix:/tmp/qmp.sock,...
|
||||||
|
sleep 2 && sendkey ret # bootloader
|
||||||
|
sleep 13 # boot complete
|
||||||
|
screendump /tmp/frame-1.ppm # initial state
|
||||||
|
sendkey 1 && sleep 1 && screendump frame-2.ppm # red top
|
||||||
|
sendkey 2 && sleep 1 && screendump frame-3.ppm # green top
|
||||||
|
sendkey 3 && sleep 1 && screendump frame-4.ppm # blue top
|
||||||
|
```
|
||||||
|
|
||||||
|
Les 4 captures sont visibles dans `docs/phase6-3-*.png`.
|
||||||
|
|
||||||
|
## Limitations / hors scope
|
||||||
|
|
||||||
|
### Pas de damage tracking
|
||||||
|
Chaque `compose_into()` rerend les surfaces complètement. Pour 3
|
||||||
|
surfaces ARGB de ~400x300, c'est ~720 KiB recopiés à chaque event.
|
||||||
|
Acceptable (on est largement sous le ms côté CPU). À reconsidérer
|
||||||
|
en 6.4 quand `wl_surface.damage_buffer` arrivera côté frontend.
|
||||||
|
|
||||||
|
### Pas de blending alpha
|
||||||
|
`compose_into` overwrite les pixels. Une surface ARGB avec α<255 est
|
||||||
|
traitée comme opaque (l'alpha est ignoré au niveau de la copie).
|
||||||
|
Pour Wayland natif on aura besoin de blending pour les decorations
|
||||||
|
côté client transparentes. Reportable à 6.4 ou 6.5.
|
||||||
|
|
||||||
|
### Pas de cursor visible
|
||||||
|
Le bin d'intégration ne dessine pas de curseur. Le pointeur souris
|
||||||
|
QEMU n'apparaît donc pas à l'écran. Hit-test fonctionne (on a vu
|
||||||
|
les events `PointerButton`) mais sans feedback visuel. Add à phase
|
||||||
|
7 (curseur stable).
|
||||||
|
|
||||||
|
### Hit-test au clic non testé visuellement
|
||||||
|
Le test runtime utilise les touches '1'/'2'/'3' pour raise. Les
|
||||||
|
events `PointerMotion` n'arrivent pas dans la config QEMU minimale
|
||||||
|
(pas de `-device usb-tablet`). Le hit-test fonctionne sur les coords
|
||||||
|
qu'on garde côté binaire ; à valider avec un vrai input mouse en 7.
|
||||||
|
|
||||||
|
## Code source
|
||||||
|
|
||||||
|
```
|
||||||
|
crates/redox-wl-compositor-core/ # lib pure Rust (450 lignes + 27 tests)
|
||||||
|
├── Cargo.toml
|
||||||
|
└── src/lib.rs
|
||||||
|
|
||||||
|
crates/redox-wl-display/ # MODIFIÉ
|
||||||
|
├── Cargo.toml # + dep redox-wl-compositor-core
|
||||||
|
└── src/lib.rs # + impl Framebuffer for RedoxOutput
|
||||||
|
|
||||||
|
crates/redox-wl-test-compose-static/ # bin d'intégration (190 lignes)
|
||||||
|
├── Cargo.toml
|
||||||
|
└── src/main.rs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Suite phase 6.4
|
||||||
|
|
||||||
|
- Ajouter dep `wayland-server` au futur `redox-wl-wayland-frontend`
|
||||||
|
- Mapper `wl_compositor.create_surface` → `registry.create()`
|
||||||
|
- Mapper `wl_shm_pool.create_buffer` → wrapper sur mmap'd region
|
||||||
|
- Mapper `wl_surface.attach` → `state.buffer = Some(...)`
|
||||||
|
- Mapper `wl_surface.commit` → `registry.commit(id)`
|
||||||
|
- Mapper `wl_surface.damage_buffer` → suivi pour optimiser `compose_into`
|
||||||
|
- Mapper `wl_surface.frame` → callback après `present`
|
||||||
|
- Tester avec un client Wayland simple (par ex. `weston-info` ou
|
||||||
|
un client de test custom utilisant `wayland-client`)
|
||||||
|
|
||||||
|
Estimé 2-3 sessions de 2h.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Fin du document de phase 6.1-6.3.*
|
||||||
Loading…
Add table
Add a link
Reference in a new issue