🎉 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:
Votre Nom 2026-05-09 12:20:04 +02:00
parent dbf3bffa2b
commit 509aae7769
10 changed files with 608 additions and 0 deletions

View file

@ -192,6 +192,40 @@ impl SurfaceRegistry {
.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()
}
@ -708,6 +742,73 @@ mod tests {
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

View file

@ -9,3 +9,4 @@ drm = "0.15"
graphics-ipc = { git = "https://gitlab.redox-os.org/redox-os/base.git" }
inputd = { git = "https://gitlab.redox-os.org/redox-os/base.git" }
libredox = "0.1"
redox-wl-compositor-core = { path = "../redox-wl-compositor-core" }

View file

@ -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()))
}
/// 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 {
fn drop(&mut self) {
if let Some(fb) = self.fb.take() {

View 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" }

View 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, &registry)?;
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, &registry) {
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;