🎉 Phase 5 — input backend Redox validé runtime

Crates ajoutés :
- redox-wl-input (lib) : InputBackend wrappe Arc<ConsumerHandle>
  + enum InputEvent { Key, TextInput, PointerMotion(Relative)?,
  PointerButton, PointerScroll, Quit, Unhandled, Handoff }
- redox-wl-test-input (bin) : ouvre display + take CRTC + peint bleu
  marine + polle events 30s en logguant chaque event

Modifs redox-wl-display :
- _consumer: ConsumerHandle → consumer: Arc<ConsumerHandle>
- + pub fn consumer() -> Arc<ConsumerHandle> pour partage avec input

Validation runtime sur Redox bootée via QEMU + monitor unix socket :
20 events injectés via `sendkey` et `mouse_button` HMP commands, tous
reçus et traduits correctement :
- a/b/c PRESS+RELEASE — keymap directe
- shift+a → 'A' uppercase — modificateur fonctionnel
- ctrl+c → ctrl PRESS + 'c' PRESS — composition fonctionnelle
- mouse_button 1/0 → PointerButton L=true/false
- Esc, Enter, Shift, Ctrl reçus avec scancode brut

Décision architecturale : un seul ConsumerHandle partagé via Arc entre
RedoxOutput (pour vie du VT) et InputBackend (lecteur unique d'events).
Sinon deux consumers = deux VTs distincts dont un seul reçoit les events.

Capture preuve : docs/phase5-blue-screen-with-input.png — bleu marine
plein écran 1280x800 confirmant que display + input fonctionnent
ensemble dans le même binaire.

docs/phase5-input-backend.md : compte-rendu complet.

Image restaurée à boot Orbital normal après session.

Leyoda 2026 – GPLv3
This commit is contained in:
Votre Nom 2026-05-09 11:22:54 +02:00
parent 753a30757b
commit a9bb88d9f3
7 changed files with 542 additions and 2 deletions

View file

@ -18,6 +18,7 @@
use std::io;
use std::os::fd::AsRawFd;
use std::slice;
use std::sync::Arc;
use drm::Device as _;
use drm::buffer::{Buffer as _, DrmFourcc};
@ -32,7 +33,10 @@ pub struct RedoxOutput {
/// Consumer handle GARDÉ EN VIE pour que le VT alloué par inputd persiste.
/// Si on drop ce handle, inputd retire le VT de sa table et toute opération
/// `inputd -A <vt>` ultérieure dira "non-existent VT".
_consumer: ConsumerHandle,
/// Exposé via `consumer()` pour partage avec un éventuel `InputBackend`
/// (qui polle les events sur le même fd, sans réouvrir un consumer
/// supplémentaire — sinon on aurait deux VTs et un seul recevrait les events).
consumer: Arc<ConsumerHandle>,
handle: V2GraphicsHandle,
width: u32,
height: u32,
@ -101,7 +105,7 @@ impl RedoxOutput {
.ok_or_else(|| io::Error::other("no crtc compatible with encoder"))?;
Ok(Self {
_consumer: consumer,
consumer: Arc::new(consumer),
handle,
width: w as u32,
height: h as u32,
@ -128,6 +132,13 @@ impl RedoxOutput {
pub fn vt(&self) -> usize {
self.vt
}
/// Renvoie un Arc partagé du `ConsumerHandle` pour un `InputBackend` qui
/// veut polle les events sur le même VT que le display.
/// Le caller ne doit PAS lire les events depuis ce handle (RedoxOutput
/// ne lit pas non plus) — c'est l'`InputBackend` qui sera lecteur unique.
pub fn consumer(&self) -> Arc<ConsumerHandle> {
Arc::clone(&self.consumer)
}
/// Alloue un CpuBackedBuffer ARGB8888 plein écran, l'ajoute comme
/// framebuffer DRM et appelle `set_crtc` pour qu'il soit affiché.

View file

@ -0,0 +1,9 @@
[package]
name = "redox-wl-input"
version = "0.1.0"
edition = "2021"
description = "Input backend wrapper around inputd ConsumerHandle for Redox"
[dependencies]
inputd = { git = "https://gitlab.redox-os.org/redox-os/base.git" }
orbclient = "0.3"

View file

@ -0,0 +1,134 @@
//! Backend input pour le compositor Redox.
//!
//! Encapsule `inputd::ConsumerHandle` et traduit les `orbclient::Event`
//! bruts en un enum `InputEvent` neutre que le compositor peut consommer
//! sans être couplé à orbclient ou à inputd directement.
//!
//! Conception : un seul `ConsumerHandle` est partagé entre `RedoxOutput`
//! (qui le tient en vie pour préserver le VT) et `InputBackend` (qui polle
//! les events). `RedoxOutput` ne lit pas les events ; `InputBackend` est
//! lecteur unique. Ils partagent via `Arc<ConsumerHandle>`.
use std::io;
use std::os::fd::{AsRawFd, BorrowedFd};
use std::sync::Arc;
use inputd::{ConsumerHandle, ConsumerHandleEvent};
use orbclient::{Event, EventOption};
/// Event compositor neutre, traduit depuis `orbclient::Event`.
///
/// Reste indépendant de Wayland : un compositor peut le mapper vers
/// `wl_keyboard.key`, `wl_pointer.motion`, etc. ; un autre frontend
/// peut le mapper vers tout autre protocole.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InputEvent {
/// Touche clavier pressée ou relâchée.
Key {
/// Caractère résolu via la keymap active. `'\0'` si touche non-imprimable.
character: char,
/// Scancode brut envoyé par le driver clavier.
scancode: u8,
/// `true` = press, `false` = release.
pressed: bool,
},
/// Caractère issu d'une méthode IME (compose, dead key, etc.).
TextInput { character: char },
/// Position absolue de la souris dans les coordonnées display.
PointerMotion { x: i32, y: i32 },
/// Mouvement relatif (utile pour les jeux ou modes "grab").
PointerMotionRelative { dx: i32, dy: i32 },
/// État des boutons de la souris (toujours envoyé en bloc côté Redox).
PointerButton {
left: bool,
middle: bool,
right: bool,
},
/// Événement de scroll (delta).
PointerScroll { dx: i32, dy: i32 },
/// L'utilisateur veut sortir (Quit / kill window).
Quit,
/// Event reçu mais non encore mappé. Conserve le code orbclient pour debug.
Unhandled { code: i64, a: i64, b: i64 },
/// inputd nous a retiré le contrôle du VT (handoff vers un autre VT).
/// Le compositor doit relâcher le display jusqu'au prochain Resume.
Handoff,
}
/// Capacité maximale du buffer interne de read_events. Aligné sur Orbital.
const READ_BUF: usize = 16;
pub struct InputBackend {
consumer: Arc<ConsumerHandle>,
}
impl InputBackend {
/// Construit un backend depuis le `ConsumerHandle` partagé avec le
/// `RedoxOutput`. Le caller ne doit pas appeler `read_events` lui-même
/// sur le consumer, sinon les events seront répartis entre les lecteurs.
pub fn new(consumer: Arc<ConsumerHandle>) -> Self {
Self { consumer }
}
/// Borrow le fd des events pour subscription dans une `EventQueue`.
pub fn event_fd(&self) -> BorrowedFd<'_> {
self.consumer.event_handle()
}
pub fn event_raw_fd(&self) -> i32 {
self.consumer.event_handle().as_raw_fd()
}
/// Récupère tous les events disponibles maintenant. Non-bloquant
/// (`ConsumerHandle::new_vt` ouvre avec `O_NONBLOCK`).
/// Retourne aussi `InputEvent::Handoff` si inputd nous notifie un switch VT.
pub fn poll(&self) -> io::Result<Vec<InputEvent>> {
let mut out = Vec::new();
let mut buf = [Event::new(); READ_BUF];
loop {
match self.consumer.read_events(&mut buf)? {
ConsumerHandleEvent::Events(&[]) => break,
ConsumerHandleEvent::Events(events) => {
for ev in events {
out.push(translate(ev));
}
}
ConsumerHandleEvent::Handoff => {
out.push(InputEvent::Handoff);
break; // après un handoff on arrête de poller cette fois-ci
}
}
}
Ok(out)
}
}
fn translate(ev: &Event) -> InputEvent {
match ev.to_option() {
EventOption::Key(k) => InputEvent::Key {
character: k.character,
scancode: k.scancode,
pressed: k.pressed,
},
EventOption::TextInput(t) => InputEvent::TextInput {
character: t.character,
},
EventOption::Mouse(m) => InputEvent::PointerMotion { x: m.x, y: m.y },
EventOption::MouseRelative(m) => InputEvent::PointerMotionRelative {
dx: m.dx,
dy: m.dy,
},
EventOption::Button(b) => InputEvent::PointerButton {
left: b.left,
middle: b.middle,
right: b.right,
},
EventOption::Scroll(s) => InputEvent::PointerScroll { dx: s.x, dy: s.y },
EventOption::Quit(_) => InputEvent::Quit,
_ => InputEvent::Unhandled {
code: ev.code,
a: ev.a,
b: ev.b,
},
}
}

View file

@ -0,0 +1,8 @@
[package]
name = "redox-wl-test-input"
version = "0.1.0"
edition = "2021"
[dependencies]
redox-wl-display = { path = "../redox-wl-display" }
redox-wl-input = { path = "../redox-wl-input" }

View file

@ -0,0 +1,176 @@
//! Phase 5 — Test input backend.
//!
//! Boote en remplaçant Orbital sur le VT 3 (cf init), prend le CRTC, peint
//! un fond uniforme bleu marine pour confirmer qu'on contrôle le display,
//! puis polle les events input pendant 30 secondes en logguant chaque
//! event reçu vers /scheme/debug (visible côté host via le serial QEMU).
//!
//! Côté host on peut envoyer des touches via le monitor QEMU :
//! printf "sendkey a\n" | ncat -U /tmp/qmp.sock
//! printf "sendkey ret\n" | ncat -U /tmp/qmp.sock
//! printf "mouse_move 100 100\n" | ncat -U /tmp/qmp.sock (HMP)
//! printf "mouse_button 1\n" | ncat -U /tmp/qmp.sock
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_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);
}
fn run() -> Result<(), Box<dyn std::error::Error>> {
dlog("[input] Phase 5 — test input backend");
// Ouvre le display (ce qui alloue notre VT côté inputd)
let mut output = RedoxOutput::open().map_err(|e| format!("RedoxOutput::open: {e}"))?;
let our_vt = output.vt();
dlog(&format!(
"[input] display {}x{}, VT={our_vt}",
output.width(),
output.height()
));
// Active notre VT (sinon set_crtc no-op cf phase 4)
let _ = Command::new("inputd")
.arg("-A")
.arg(our_vt.to_string())
.status();
thread::sleep(Duration::from_millis(300));
// Prend le CRTC, peint un fond bleu marine pour signaler "on est là"
output
.take_crtc()
.map_err(|e| format!("take_crtc: {e}"))?;
{
let pixels = output.pixels_mut()?;
for p in pixels.iter_mut() {
*p = 0xFF_10_30_60; // ARGB bleu marine sombre
}
}
output
.present_with_takeover()
.map_err(|e| format!("present init: {e}"))?;
dlog("[input] CRTC pris, fond bleu marine peint");
// Crée le backend input partageant le ConsumerHandle de RedoxOutput
let input = InputBackend::new(output.consumer());
dlog(&format!(
"[input] InputBackend prêt, fd events={}",
input.event_raw_fd()
));
// Boucle de polling pendant 30 secondes
let start = Instant::now();
let total_duration = Duration::from_secs(30);
let mut event_count: usize = 0;
let mut quit_requested = false;
while start.elapsed() < total_duration && !quit_requested {
let events = input.poll().map_err(|e| format!("poll: {e}"))?;
for ev in events {
event_count += 1;
match &ev {
InputEvent::Key {
character,
scancode,
pressed,
} => {
let c = if character.is_control() || *character == '\0' {
'·'
} else {
*character
};
dlog(&format!(
"[input] #{event_count:04} Key '{c}' scan={scancode:#x} {}",
if *pressed { "PRESS" } else { "RELEASE" }
));
}
InputEvent::TextInput { character } => {
dlog(&format!("[input] #{event_count:04} TextInput '{character}'"));
}
InputEvent::PointerMotion { x, y } => {
dlog(&format!("[input] #{event_count:04} PointerMotion abs=({x},{y})"));
}
InputEvent::PointerMotionRelative { dx, dy } => {
dlog(&format!(
"[input] #{event_count:04} PointerMotionRelative d=({dx},{dy})"
));
}
InputEvent::PointerButton {
left,
middle,
right,
} => {
dlog(&format!(
"[input] #{event_count:04} PointerButton L={left} M={middle} R={right}"
));
}
InputEvent::PointerScroll { dx, dy } => {
dlog(&format!("[input] #{event_count:04} PointerScroll d=({dx},{dy})"));
}
InputEvent::Quit => {
dlog(&format!("[input] #{event_count:04} Quit demandé"));
quit_requested = true;
}
InputEvent::Unhandled { code, a, b } => {
dlog(&format!(
"[input] #{event_count:04} Unhandled code={code} a={a} b={b}"
));
}
InputEvent::Handoff => {
dlog(&format!("[input] #{event_count:04} Handoff (VT switch)"));
}
}
}
// Re-flush pour tenir l'écran si fbcond/fbbootlogd tente de reprendre
let _ = output.present_with_takeover();
thread::sleep(Duration::from_millis(50));
}
dlog(&format!(
"[input] fin — {event_count} events reçus 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("[input] PASS");
ExitCode::SUCCESS
}
Err(e) => {
dlog(&format!("[input] FAIL: {e}"));
ExitCode::FAILURE
}
}
}