🎉 Phase 13.2.a — wl_output v3 complet (gating v1/v2/v3 validé en natif)
Dette protocolaire transversale comblée : wl_output n'était pas déclaré comme global, plusieurs toolkits client (sctk, GTK, Qt) le considèrent implicitement requis. Maintenant exposé à v3 avec gating conforme : - geometry, mode : envoyés à toutes les versions (>= 1) - scale : seulement si bind >= 3 (gating conservateur) - done : seulement si bind >= 2 Mode flags = CURRENT | PREFERRED, refresh 60 000 mHz, scale_factor=1, transform Normal, subpixel Unknown, make=Redox, model=redox-wl-output:0. Position (0,0), single output, taille pixel issue de screen_w/screen_h (setté par set_screen_size au boot compositor). Nouveau crate de validation : redox-wl-test-wl-output. Deux niveaux : 1. Client autonome pour test runtime dans QEMU (bind aux 3 versions, dump events, PASS/FAIL via exit code) 2. Test natif (cargo test, tourne sur l'hôte sans QEMU) qui simule serveur+client en-process via UnixStream::pair() et valide les 11 assertions du gating. Vérifié PASS le 2026-05-16 sur CachyOS. Le test natif sert de garde-fou anti-régression pour CI ultérieur. Plus besoin de relancer QEMU pour valider ce path. run-qemu.sh : copie maintenant aussi le test client si compilé (skip silencieux sinon). Leyoda 2026 – GPLv3
This commit is contained in:
parent
095d7e5550
commit
7413745dbe
5 changed files with 651 additions and 2 deletions
14
crates/redox-wl-test-wl-output/Cargo.toml
Normal file
14
crates/redox-wl-test-wl-output/Cargo.toml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "redox-wl-test-wl-output"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
wayland-client = { path = "../../../wayland-rs/wayland-client", default-features = false }
|
||||
wayland-backend = { path = "../../../wayland-rs/wayland-backend", default-features = false }
|
||||
libc = "0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
# Test natif (CachyOS, pas Redox) : server + client en-process
|
||||
# pour valider le gating de wl_output v1/v2/v3.
|
||||
wayland-server = { path = "../../../wayland-rs/wayland-server", default-features = false }
|
||||
291
crates/redox-wl-test-wl-output/src/main.rs
Normal file
291
crates/redox-wl-test-wl-output/src/main.rs
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
//! Phase 13.2.a — client de test : version gating de wl_output.
|
||||
//!
|
||||
//! Se connecte au compositor, trouve le global wl_output, et le bind
|
||||
//! 3 fois successivement aux versions 1, 2, 3. Pour chaque bind, dump
|
||||
//! les events reçus et vérifie le gating attendu :
|
||||
//!
|
||||
//! v1 → Geometry + Mode uniquement (pas de Scale, pas de Done)
|
||||
//! v2 → + Done (pas encore de Scale)
|
||||
//! v3 → + Scale (gating conservateur côté serveur)
|
||||
//!
|
||||
//! Sortie sur stdout ET /scheme/debug pour récupération depuis serial
|
||||
//! sous QEMU. Code de sortie 0 = PASS, 1 = FAIL si gating incorrect.
|
||||
//!
|
||||
//! NB : on s'aligne sur la procédure 13.1.b (connexion explicite au
|
||||
//! socket, pas via WAYLAND_DISPLAY ; logs tee'd vers debug). Aucune
|
||||
//! adaptation de fond — c'est juste un client wayland-client raw.
|
||||
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::path::Path;
|
||||
use std::process::ExitCode;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use wayland_client::{
|
||||
backend::Backend,
|
||||
protocol::{wl_output, wl_registry},
|
||||
Connection, Dispatch, QueueHandle,
|
||||
};
|
||||
|
||||
const SOCKET_PATH: &str = "/tmp/redox-wl-comp.sock";
|
||||
|
||||
// --- Logging tee vers stdout + /scheme/debug --------------------------------
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// --- Attente du socket -----------------------------------------------------
|
||||
|
||||
fn wait_for_socket(path: &Path, attempts: u32, sleep_ms: u64) -> bool {
|
||||
for _ in 0..attempts {
|
||||
if path.exists() {
|
||||
return true;
|
||||
}
|
||||
thread::sleep(Duration::from_millis(sleep_ms));
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// --- État partagé ----------------------------------------------------------
|
||||
|
||||
/// Accumulateur d'events pour un binding wl_output donné.
|
||||
#[derive(Default, Debug)]
|
||||
struct OutputObservation {
|
||||
seen_geometry: bool,
|
||||
seen_mode: bool,
|
||||
seen_scale: bool,
|
||||
seen_done: bool,
|
||||
mode_flags: u32,
|
||||
mode_width: i32,
|
||||
mode_height: i32,
|
||||
mode_refresh: i32,
|
||||
scale_factor: i32,
|
||||
}
|
||||
|
||||
/// État global du client de test : registry advertised globals + 3 observations.
|
||||
#[derive(Default)]
|
||||
struct State {
|
||||
output_global_name: Option<u32>,
|
||||
output_global_version: Option<u32>,
|
||||
// 3 observations indexées 0=v1, 1=v2, 2=v3.
|
||||
obs: [OutputObservation; 3],
|
||||
// Phase courante en cours de capture (None entre les binds).
|
||||
current_version_index: Option<usize>,
|
||||
}
|
||||
|
||||
impl Dispatch<wl_registry::WlRegistry, ()> for State {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
_r: &wl_registry::WlRegistry,
|
||||
event: wl_registry::Event,
|
||||
_data: &(),
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
) {
|
||||
if let wl_registry::Event::Global {
|
||||
name,
|
||||
interface,
|
||||
version,
|
||||
} = event
|
||||
{
|
||||
if interface == "wl_output" {
|
||||
state.output_global_name = Some(name);
|
||||
state.output_global_version = Some(version);
|
||||
dlog(&format!(
|
||||
"[test-output] registry: wl_output advertised name={name} version={version}"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<wl_output::WlOutput, ()> for State {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
_r: &wl_output::WlOutput,
|
||||
event: wl_output::Event,
|
||||
_data: &(),
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
) {
|
||||
let Some(idx) = state.current_version_index else {
|
||||
// Ne devrait pas arriver — events après un done observable
|
||||
// sur un binding qu'on a fini d'examiner. On ignore.
|
||||
return;
|
||||
};
|
||||
let obs = &mut state.obs[idx];
|
||||
match event {
|
||||
wl_output::Event::Geometry { .. } => obs.seen_geometry = true,
|
||||
wl_output::Event::Mode {
|
||||
flags,
|
||||
width,
|
||||
height,
|
||||
refresh,
|
||||
} => {
|
||||
obs.seen_mode = true;
|
||||
obs.mode_flags = flags.into();
|
||||
obs.mode_width = width;
|
||||
obs.mode_height = height;
|
||||
obs.mode_refresh = refresh;
|
||||
}
|
||||
wl_output::Event::Scale { factor } => {
|
||||
obs.seen_scale = true;
|
||||
obs.scale_factor = factor;
|
||||
}
|
||||
wl_output::Event::Done => obs.seen_done = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- main ------------------------------------------------------------------
|
||||
|
||||
fn main() -> ExitCode {
|
||||
dlog("[test-output] start (Phase 13.2.a — version gating wl_output)");
|
||||
|
||||
let path = Path::new(SOCKET_PATH);
|
||||
if !wait_for_socket(path, 50, 100) {
|
||||
dlog(&format!(
|
||||
"[test-output] FAIL: socket {SOCKET_PATH} introuvable après 5s"
|
||||
));
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
let stream = match UnixStream::connect(SOCKET_PATH) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
dlog(&format!("[test-output] FAIL: connect: {e}"));
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
stream.set_nonblocking(false).ok();
|
||||
|
||||
let backend = match Backend::connect(stream) {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
dlog(&format!("[test-output] FAIL: backend: {e}"));
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
let conn = Connection::from_backend(backend);
|
||||
|
||||
let mut event_queue = conn.new_event_queue::<State>();
|
||||
let qh = event_queue.handle();
|
||||
let display = conn.display();
|
||||
let _registry = display.get_registry(&qh, ());
|
||||
|
||||
let mut state = State::default();
|
||||
|
||||
// Premier roundtrip : capture les globals
|
||||
if let Err(e) = event_queue.roundtrip(&mut state) {
|
||||
dlog(&format!("[test-output] FAIL: roundtrip registry: {e}"));
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
let global_name = match state.output_global_name {
|
||||
Some(n) => n,
|
||||
None => {
|
||||
dlog("[test-output] FAIL: aucun global wl_output annoncé par le compositor");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
let advertised_version = state.output_global_version.unwrap_or(0);
|
||||
dlog(&format!(
|
||||
"[test-output] global wl_output découvert (name={global_name}, version annoncée={advertised_version})"
|
||||
));
|
||||
|
||||
if advertised_version < 3 {
|
||||
dlog(&format!(
|
||||
"[test-output] WARN: version annoncée < 3, le test v3 va échouer (compositor pas à jour ?)"
|
||||
));
|
||||
}
|
||||
|
||||
// Bind à v1, v2, v3 successivement avec un roundtrip entre chaque.
|
||||
let registry = display.get_registry(&qh, ());
|
||||
for (idx, version) in [(0usize, 1u32), (1, 2), (2, 3)].iter() {
|
||||
state.current_version_index = Some(*idx);
|
||||
dlog(&format!("[test-output] bind wl_output @ v{version} …"));
|
||||
let _output = registry.bind::<wl_output::WlOutput, _, _>(global_name, *version, &qh, ());
|
||||
// Plusieurs roundtrips pour s'assurer que tous les events init
|
||||
// ont atterri (geometry + mode + éventuel scale + éventuel done).
|
||||
for _ in 0..3 {
|
||||
if let Err(e) = event_queue.roundtrip(&mut state) {
|
||||
dlog(&format!(
|
||||
"[test-output] FAIL: roundtrip bind v{version}: {e}"
|
||||
));
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
}
|
||||
let obs = &state.obs[*idx];
|
||||
dlog(&format!(
|
||||
"[test-output] v{version} reçu : geometry={} mode={} scale={} done={} | mode={}x{}@{}mHz flags={} scale_factor={}",
|
||||
obs.seen_geometry, obs.seen_mode, obs.seen_scale, obs.seen_done,
|
||||
obs.mode_width, obs.mode_height, obs.mode_refresh, obs.mode_flags, obs.scale_factor
|
||||
));
|
||||
state.current_version_index = None;
|
||||
}
|
||||
|
||||
// --- Verdict ---
|
||||
let mut errors: Vec<String> = Vec::new();
|
||||
|
||||
let v1 = &state.obs[0];
|
||||
if !v1.seen_geometry { errors.push("v1: geometry manquant".into()); }
|
||||
if !v1.seen_mode { errors.push("v1: mode manquant".into()); }
|
||||
if v1.seen_scale { errors.push("v1: scale envoyé alors qu'attendu v3+".into()); }
|
||||
if v1.seen_done { errors.push("v1: done envoyé alors qu'attendu v2+".into()); }
|
||||
|
||||
let v2 = &state.obs[1];
|
||||
if !v2.seen_geometry { errors.push("v2: geometry manquant".into()); }
|
||||
if !v2.seen_mode { errors.push("v2: mode manquant".into()); }
|
||||
if v2.seen_scale { errors.push("v2: scale envoyé alors qu'attendu v3+ (gating conservateur)".into()); }
|
||||
if !v2.seen_done { errors.push("v2: done attendu mais manquant".into()); }
|
||||
|
||||
let v3 = &state.obs[2];
|
||||
if !v3.seen_geometry { errors.push("v3: geometry manquant".into()); }
|
||||
if !v3.seen_mode { errors.push("v3: mode manquant".into()); }
|
||||
if !v3.seen_scale { errors.push("v3: scale attendu mais manquant".into()); }
|
||||
if !v3.seen_done { errors.push("v3: done attendu mais manquant".into()); }
|
||||
|
||||
// Sanity check sur les valeurs : mode flags doit contenir CURRENT (0x1)
|
||||
// et PREFERRED (0x2), donc valeur 3.
|
||||
if v3.mode_flags & 0x1 == 0 {
|
||||
errors.push(format!("v3: mode flags ne contient pas CURRENT (got 0x{:x})", v3.mode_flags));
|
||||
}
|
||||
if v3.mode_flags & 0x2 == 0 {
|
||||
errors.push(format!("v3: mode flags ne contient pas PREFERRED (got 0x{:x})", v3.mode_flags));
|
||||
}
|
||||
if v3.scale_factor != 1 {
|
||||
errors.push(format!("v3: scale_factor attendu 1, got {}", v3.scale_factor));
|
||||
}
|
||||
|
||||
if errors.is_empty() {
|
||||
dlog("[test-output] PASS — gating v1/v2/v3 conforme aux attentes");
|
||||
ExitCode::SUCCESS
|
||||
} else {
|
||||
for e in &errors {
|
||||
dlog(&format!("[test-output] FAIL: {e}"));
|
||||
}
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
247
crates/redox-wl-test-wl-output/tests/gating_native.rs
Normal file
247
crates/redox-wl-test-wl-output/tests/gating_native.rs
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
//! Test natif (CachyOS, hors QEMU) : version gating de wl_output.
|
||||
//!
|
||||
//! On simule en-process l'init du global wl_output v3 avec la MÊME logique
|
||||
//! de gating que `redox-wl-wayland-frontend::lib.rs` :
|
||||
//! - geometry, mode : toujours envoyés
|
||||
//! - scale : seulement si version >= 3
|
||||
//! - done : seulement si version >= 2
|
||||
//!
|
||||
//! Un seul process : serveur sur un thread + client sur le thread principal,
|
||||
//! reliés par `UnixStream::pair()`. Pas besoin de Redox, pas besoin de QEMU.
|
||||
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use wayland_server::backend::{ClientData, ClientId, DisconnectReason};
|
||||
use wayland_server::{
|
||||
protocol::wl_output as srv_wl_output, Client as SrvClient, DataInit, Display, DisplayHandle,
|
||||
GlobalDispatch, Resource,
|
||||
};
|
||||
|
||||
use wayland_client::{
|
||||
backend::Backend,
|
||||
protocol::{wl_output as cli_wl_output, wl_registry},
|
||||
Connection, Dispatch as CliDispatch, QueueHandle,
|
||||
};
|
||||
|
||||
const OUTPUT_VERSION: u32 = 3;
|
||||
const SCREEN_W: i32 = 1280;
|
||||
const SCREEN_H: i32 = 800;
|
||||
|
||||
// ============= server side =============
|
||||
|
||||
struct ServerState;
|
||||
|
||||
struct StubClientData;
|
||||
impl ClientData for StubClientData {
|
||||
fn initialized(&self, _: ClientId) {}
|
||||
fn disconnected(&self, _: ClientId, _: DisconnectReason) {}
|
||||
}
|
||||
|
||||
impl GlobalDispatch<srv_wl_output::WlOutput, ()> for ServerState {
|
||||
fn bind(
|
||||
_state: &mut Self,
|
||||
_handle: &DisplayHandle,
|
||||
_client: &SrvClient,
|
||||
resource: wayland_server::New<srv_wl_output::WlOutput>,
|
||||
_data: &(),
|
||||
data_init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
let output = data_init.init(resource, ());
|
||||
let version = output.version();
|
||||
|
||||
// Logique identique à redox-wl-wayland-frontend/src/lib.rs
|
||||
output.geometry(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
srv_wl_output::Subpixel::Unknown,
|
||||
"Redox".to_string(),
|
||||
"redox-wl-output:0".to_string(),
|
||||
srv_wl_output::Transform::Normal,
|
||||
);
|
||||
|
||||
let mode_flags = srv_wl_output::Mode::Current | srv_wl_output::Mode::Preferred;
|
||||
output.mode(mode_flags, SCREEN_W, SCREEN_H, 60_000);
|
||||
|
||||
if version >= 3 {
|
||||
output.scale(1);
|
||||
}
|
||||
if version >= 2 {
|
||||
output.done();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl wayland_server::Dispatch<srv_wl_output::WlOutput, ()> for ServerState {
|
||||
fn request(
|
||||
_state: &mut Self,
|
||||
_client: &SrvClient,
|
||||
_r: &srv_wl_output::WlOutput,
|
||||
_request: srv_wl_output::Request,
|
||||
_data: &(),
|
||||
_dh: &DisplayHandle,
|
||||
_data_init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
// ============= client side =============
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy)]
|
||||
struct OutputObs {
|
||||
seen_geometry: bool,
|
||||
seen_mode: bool,
|
||||
seen_scale: bool,
|
||||
seen_done: bool,
|
||||
mode_flags: u32,
|
||||
scale_factor: i32,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ClientState {
|
||||
global_name: Option<u32>,
|
||||
obs: [OutputObs; 3],
|
||||
cur: Option<usize>,
|
||||
}
|
||||
|
||||
impl CliDispatch<wl_registry::WlRegistry, ()> for ClientState {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
_: &wl_registry::WlRegistry,
|
||||
event: wl_registry::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
if let wl_registry::Event::Global {
|
||||
name, interface, ..
|
||||
} = event
|
||||
{
|
||||
if interface == "wl_output" {
|
||||
state.global_name = Some(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CliDispatch<cli_wl_output::WlOutput, ()> for ClientState {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
_: &cli_wl_output::WlOutput,
|
||||
event: cli_wl_output::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
let Some(idx) = state.cur else { return };
|
||||
let obs = &mut state.obs[idx];
|
||||
match event {
|
||||
cli_wl_output::Event::Geometry { .. } => obs.seen_geometry = true,
|
||||
cli_wl_output::Event::Mode { flags, .. } => {
|
||||
obs.seen_mode = true;
|
||||
obs.mode_flags = flags.into();
|
||||
}
|
||||
cli_wl_output::Event::Scale { factor } => {
|
||||
obs.seen_scale = true;
|
||||
obs.scale_factor = factor;
|
||||
}
|
||||
cli_wl_output::Event::Done => obs.seen_done = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============= test =============
|
||||
|
||||
#[test]
|
||||
fn wl_output_v123_gating() {
|
||||
// 1. Setup serveur dans un thread
|
||||
let (s_stream, c_stream) = UnixStream::pair().expect("UnixStream::pair");
|
||||
|
||||
let server_thread = thread::spawn(move || {
|
||||
let mut display: Display<ServerState> = Display::new().unwrap();
|
||||
let dh = display.handle();
|
||||
dh.create_global::<ServerState, srv_wl_output::WlOutput, _>(OUTPUT_VERSION, ());
|
||||
|
||||
s_stream.set_nonblocking(true).unwrap();
|
||||
let _client = display
|
||||
.handle()
|
||||
.insert_client(s_stream, Arc::new(StubClientData))
|
||||
.unwrap();
|
||||
|
||||
let mut state = ServerState;
|
||||
// Dispatch loop : on tourne ~5 secondes pour absorber tous les
|
||||
// round-trips du test client. Le test signale la fin en fermant
|
||||
// le socket côté client, ce qui sort dispatch_clients sur error.
|
||||
let start = std::time::Instant::now();
|
||||
while start.elapsed() < Duration::from_secs(5) {
|
||||
let _ = display.dispatch_clients(&mut state);
|
||||
let _ = display.flush_clients();
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Client connecte via l'autre bout du socket
|
||||
c_stream.set_nonblocking(false).unwrap();
|
||||
let backend = Backend::connect(c_stream).unwrap();
|
||||
let conn = Connection::from_backend(backend);
|
||||
let mut event_queue = conn.new_event_queue::<ClientState>();
|
||||
let qh = event_queue.handle();
|
||||
let display = conn.display();
|
||||
let _reg1 = display.get_registry(&qh, ());
|
||||
|
||||
let mut state = ClientState::default();
|
||||
event_queue.roundtrip(&mut state).expect("roundtrip registry");
|
||||
|
||||
let global_name = state.global_name.expect("wl_output non advertised");
|
||||
|
||||
// 3. Bind aux 3 versions
|
||||
let registry = display.get_registry(&qh, ());
|
||||
for (idx, version) in [(0usize, 1u32), (1, 2), (2, 3)].iter() {
|
||||
state.cur = Some(*idx);
|
||||
let _o = registry.bind::<cli_wl_output::WlOutput, _, _>(global_name, *version, &qh, ());
|
||||
for _ in 0..3 {
|
||||
event_queue.roundtrip(&mut state).expect("roundtrip bind");
|
||||
}
|
||||
state.cur = None;
|
||||
}
|
||||
|
||||
// 4. Drop client to make server exit
|
||||
drop(conn);
|
||||
server_thread.join().unwrap();
|
||||
|
||||
// 5. Asserts
|
||||
let v1 = &state.obs[0];
|
||||
assert!(v1.seen_geometry, "v1: geometry attendu");
|
||||
assert!(v1.seen_mode, "v1: mode attendu");
|
||||
assert!(!v1.seen_scale, "v1: scale ne devrait PAS être envoyé");
|
||||
assert!(!v1.seen_done, "v1: done ne devrait PAS être envoyé");
|
||||
|
||||
let v2 = &state.obs[1];
|
||||
assert!(v2.seen_geometry, "v2: geometry attendu");
|
||||
assert!(v2.seen_mode, "v2: mode attendu");
|
||||
assert!(
|
||||
!v2.seen_scale,
|
||||
"v2: scale ne devrait PAS être envoyé (gating conservateur v3+)"
|
||||
);
|
||||
assert!(v2.seen_done, "v2: done attendu");
|
||||
|
||||
let v3 = &state.obs[2];
|
||||
assert!(v3.seen_geometry, "v3: geometry attendu");
|
||||
assert!(v3.seen_mode, "v3: mode attendu");
|
||||
assert!(v3.seen_scale, "v3: scale attendu");
|
||||
assert!(v3.seen_done, "v3: done attendu");
|
||||
assert_eq!(v3.scale_factor, 1, "v3: scale_factor doit être 1");
|
||||
assert!(
|
||||
v3.mode_flags & 0x1 != 0,
|
||||
"v3: mode flags doit contenir CURRENT (0x1)"
|
||||
);
|
||||
assert!(
|
||||
v3.mode_flags & 0x2 != 0,
|
||||
"v3: mode flags doit contenir PREFERRED (0x2)"
|
||||
);
|
||||
}
|
||||
|
|
@ -36,8 +36,8 @@ use wayland_protocols::xdg::shell::server::{
|
|||
use wayland_server::{
|
||||
backend::{ClientData, ClientId, DisconnectReason},
|
||||
protocol::{
|
||||
wl_buffer, wl_callback, wl_compositor, wl_keyboard, wl_pointer, wl_region, wl_seat, wl_shm,
|
||||
wl_shm_pool, wl_surface,
|
||||
wl_buffer, wl_callback, wl_compositor, wl_keyboard, wl_output, wl_pointer, wl_region,
|
||||
wl_seat, wl_shm, wl_shm_pool, wl_surface,
|
||||
},
|
||||
Client, DataInit, Display as WlDisplay, DisplayHandle, GlobalDispatch, Resource,
|
||||
};
|
||||
|
|
@ -48,6 +48,9 @@ const COMPOSITOR_VERSION: u32 = 5;
|
|||
const SHM_VERSION: u32 = 1;
|
||||
const XDG_WM_BASE_VERSION: u32 = 5;
|
||||
const SEAT_VERSION: u32 = 7;
|
||||
// Phase 13.2.a : wl_output v3 (couvre geometry + mode + scale + done +
|
||||
// release request, sans v4 name/description qu'on ne fournit pas encore).
|
||||
const OUTPUT_VERSION: u32 = 3;
|
||||
|
||||
/// Taille suggérée par défaut pour les nouvelles fenêtres xdg_toplevel.
|
||||
/// Le client peut respecter ou non ; on utilise sa propre taille de buffer
|
||||
|
|
@ -434,6 +437,11 @@ impl WaylandFrontend {
|
|||
dh.create_global::<Self, wl_shm::WlShm, _>(SHM_VERSION, ());
|
||||
dh.create_global::<Self, xdg_wm_base::XdgWmBase, _>(XDG_WM_BASE_VERSION, ());
|
||||
dh.create_global::<Self, wl_seat::WlSeat, _>(SEAT_VERSION, ());
|
||||
// Phase 13.2.a : wl_output déclaré comme global. Single output
|
||||
// mappé au framebuffer du compositor. Les events init (geometry/
|
||||
// mode/scale/done) sont envoyés dans le bind, cf GlobalDispatch
|
||||
// plus bas.
|
||||
dh.create_global::<Self, wl_output::WlOutput, _>(OUTPUT_VERSION, ());
|
||||
|
||||
let listener = wayland_server::ListeningSocket::bind_absolute(socket_path.to_path_buf())?;
|
||||
|
||||
|
|
@ -2157,6 +2165,88 @@ impl wayland_server::Dispatch<xdg_toplevel::XdgToplevel, Arc<XdgToplevelData>> f
|
|||
}
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// wl_output (phase 13.2.a)
|
||||
// =====================================================================
|
||||
//
|
||||
// Single output mappé sur le framebuffer du compositor. Les events
|
||||
// d'init (geometry / mode / scale / done) sont envoyés dans `bind` à
|
||||
// chaque nouvelle resource bound par un client, gated par la version
|
||||
// négociée :
|
||||
// - geometry, mode : v1+, toujours envoyés
|
||||
// - scale : v3+ (gating conservateur ; spec autorise v2+ mais on évite
|
||||
// l'ambiguïté sur les clients v2 qui pourraient mal gérer)
|
||||
// - done : v2+
|
||||
// La taille pixel vient de `screen_w / screen_h`, setté par
|
||||
// `set_screen_size()` au démarrage du compositor. Refresh hardcodé à
|
||||
// 60 Hz (60000 mHz) — notre boucle main tourne à ~30 fps mais les apps
|
||||
// modernes ignorent largement ce champ, et 60 Hz est un standard sûr.
|
||||
|
||||
impl GlobalDispatch<wl_output::WlOutput, ()> for WaylandFrontend {
|
||||
fn bind(
|
||||
state: &mut Self,
|
||||
_handle: &DisplayHandle,
|
||||
_client: &Client,
|
||||
resource: wayland_server::New<wl_output::WlOutput>,
|
||||
_data: &(),
|
||||
data_init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
let output = data_init.init(resource, ());
|
||||
let version = output.version();
|
||||
|
||||
// geometry : position dans l'espace logique multi-écran (0,0
|
||||
// ici), taille physique inconnue (0 mm, valeur "ne sait pas" par
|
||||
// convention Wayland), subpixel/make/model/transform placeholders.
|
||||
output.geometry(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
wl_output::Subpixel::Unknown,
|
||||
"Redox".to_string(),
|
||||
"redox-wl-output:0".to_string(),
|
||||
wl_output::Transform::Normal,
|
||||
);
|
||||
|
||||
// mode : taille pixel courante = framebuffer. Flags Current +
|
||||
// Preferred (un seul mode, c'est de facto le préféré).
|
||||
let mode_flags = wl_output::Mode::Current | wl_output::Mode::Preferred;
|
||||
output.mode(mode_flags, state.screen_w, state.screen_h, 60_000);
|
||||
|
||||
// scale v3+ : facteur 1 (pas de HiDPI pour notre framebuffer).
|
||||
if version >= 3 {
|
||||
output.scale(1);
|
||||
}
|
||||
|
||||
// done v2+ : signale la fin du batch init pour que le client
|
||||
// ne dispatche son OutputInfo qu'une fois tous les events arrivés.
|
||||
if version >= 2 {
|
||||
output.done();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl wayland_server::Dispatch<wl_output::WlOutput, ()> for WaylandFrontend {
|
||||
fn request(
|
||||
_state: &mut Self,
|
||||
_client: &Client,
|
||||
_r: &wl_output::WlOutput,
|
||||
request: wl_output::Request,
|
||||
_data: &(),
|
||||
_dh: &DisplayHandle,
|
||||
_data_init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
match request {
|
||||
wl_output::Request::Release => {
|
||||
// v3+ : le client libère son binding wl_output. La resource
|
||||
// est nettoyée automatiquement par wayland-server quand on
|
||||
// ne traite pas d'autre request — on no-op simplement.
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests unitaires xdg-shell (sprint 0 point 4).
|
||||
//
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@ MOUNT="${MOUNT:-/tmp/redox-mnt}"
|
|||
|
||||
COMPOSITOR_BIN="$ROOT/crates/redox-wl-compositor/target/x86_64-unknown-redox/release/redox-wl-compositor"
|
||||
CLIENT_BIN="$ROOT/crates/redox-wl-real-client-simple-window/target/x86_64-unknown-redox/release/redox-wl-real-client-simple-window"
|
||||
# Phase 13.2.a : client de test version-gating wl_output (optionnel, skip
|
||||
# silencieusement si pas compilé).
|
||||
TEST_OUTPUT_BIN="$ROOT/crates/redox-wl-test-wl-output/target/x86_64-unknown-redox/release/redox-wl-test-wl-output"
|
||||
|
||||
NO_QEMU=0
|
||||
for arg in "$@"; do
|
||||
|
|
@ -126,6 +129,10 @@ echo " monté (pid redoxfs=$REDOXFS_PID)"
|
|||
echo "==> copier les binaires dans /usr/bin/"
|
||||
cp -v "$COMPOSITOR_BIN" "$MOUNT/usr/bin/"
|
||||
cp -v "$CLIENT_BIN" "$MOUNT/usr/bin/"
|
||||
# Phase 13.2.a : test client wl_output (optionnel)
|
||||
if [[ -e "$TEST_OUTPUT_BIN" ]]; then
|
||||
cp -v "$TEST_OUTPUT_BIN" "$MOUNT/usr/bin/"
|
||||
fi
|
||||
|
||||
# --- 4. umount avant make qemu (sinon QEMU et FUSE se battent sur le même fichier) ---
|
||||
echo "==> démonter $MOUNT"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue