//! In-process KDE output management (`kde_output_management_v2` + `kde_output_device_v2`). //! //! Topology — make the streamed output primary, disable the physical/bootstrap outputs, capture //! their modes for restore, re-enable them on teardown, position the output — used to shell out to //! `kscreen-doctor` (see [`super::kwin`]). But `kscreen-doctor` drives a *separate* stack: //! libkscreen picks a backend and, depending on the setup, waits on the kscreen KDED module over //! D-Bus. On a machine where THAT layer is wedged it blocks in its own connect and never returns — //! a field report (Nobara, KWin 6.6.4) showed every topology `kscreen-doctor` timing out at its 5 s //! budget, so the streamed output never became the desktop (`also_disabled=[]`) and bring-up took //! ~26 s. //! //! The compositor's OWN Wayland is provably responsive on that same session — the host just created //! a virtual output over it via `zkde_screencast` — so we drive `kde_output_management_v2` directly //! over Wayland here, sidestepping whatever wedges the standalone tool. Every wait is time-bounded, //! so a genuinely wedged compositor degrades to `handled = false` and the caller falls back to the //! `kscreen-doctor` path rather than hanging. //! //! KWin advertises one `kde_output_device_v2` global per output (the classic model; verified live: //! `kde_output_management_v2` v19, `kde_output_device_v2` v20 on KWin 6.6.4). We bind them all, read //! each output's name / enabled / priority / current-mode size, then build a //! `kde_output_configuration_v2` and `apply()` it, waiting for `applied` / `failed`. #![allow(clippy::all, dead_code, non_camel_case_types, non_snake_case, unused)] #![deny(clippy::undocumented_unsafe_blocks)] use std::collections::HashMap; use std::os::fd::{AsFd, AsRawFd}; use std::time::{Duration, Instant}; use wayland_client::backend::ObjectId; use wayland_client::protocol::wl_callback::{self, WlCallback}; use wayland_client::protocol::wl_registry::{self, WlRegistry}; use wayland_client::{event_created_child, Connection, Dispatch, Proxy, QueueHandle}; // Generate the client bindings for the two vendored KDE protocols inline (no build.rs). The // management protocol references the device interfaces, so they can't share one `__interfaces` // module (each `generate_interfaces!` emits its own helper items, which collide). Instead — the // interdependent-protocol pattern from the `wayland-protocols` crate — `device` is a self-contained // module and `management` pulls in `device`'s interface statics + generated proxy types before its // own codegen, so its cross-protocol object args (`kde_output_device_v2`, `…_mode_v2`) resolve. #[allow(clippy::all, dead_code, non_camel_case_types, non_snake_case, unused)] pub mod device { use wayland_client; use wayland_client::protocol::*; pub mod __interfaces { use wayland_client::protocol::__interfaces::*; wayland_scanner::generate_interfaces!("protocols/kde-output-device-v2.xml"); } use self::__interfaces::*; wayland_scanner::generate_client_code!("protocols/kde-output-device-v2.xml"); } #[allow(clippy::all, dead_code, non_camel_case_types, non_snake_case, unused)] pub mod management { use wayland_client; use wayland_client::protocol::*; pub mod __interfaces { use super::super::device::__interfaces::*; use wayland_client::protocol::__interfaces::*; wayland_scanner::generate_interfaces!("protocols/kde-output-management-v2.xml"); } use self::__interfaces::*; // The device protocol's generated modules/types, so the foreign object args resolve. use super::device::*; wayland_scanner::generate_client_code!("protocols/kde-output-management-v2.xml"); } use device::kde_output_device_mode_v2::{Event as ModeEvent, KdeOutputDeviceModeV2 as DeviceMode}; use device::kde_output_device_v2::{Event as DeviceEvent, KdeOutputDeviceV2 as OutputDevice}; use management::kde_output_configuration_v2::{ Event as ConfigEvent, KdeOutputConfigurationV2 as OutputConfig, }; use management::kde_output_management_v2::KdeOutputManagementV2 as OutputManagement; /// Highest interface versions we drive; we bind `min(advertised, MAX)`. Every request we issue is /// `since ≤ 2` (`create_configuration`/`enable`/`mode`/`position`/`apply` are v1, `set_primary_output` /// is v2) and every event we read is `since ≤ 18` (`priority`), so binding high and calling low is /// always in range on any KWin that advertises the globals. const MGMT_MAX: u32 = 22; const DEVICE_MAX: u32 = 24; /// The opcode of `kde_output_device_v2.mode` (0-based event index) — the event that creates a child /// `kde_output_device_mode_v2`. Kept in sync with the vendored `kde-output-device-v2.xml`. const DEVICE_MODE_EVENT_OPCODE: u16 = 2; /// Overall budget for one enumerate-then-apply operation. Generous next to a healthy roundtrip (a /// few ms); it exists only so a wedged compositor can't pin the session's stream thread. const OP_BUDGET: Duration = Duration::from_secs(3); /// Poll slice while waiting on the Wayland fd (matches the keepalive loop's cadence in `kwin.rs`). const POLL_MS: i32 = 100; /// Which topology to apply once our output is resolved. #[derive(Clone, Copy, PartialEq, Eq)] pub(crate) enum TopologyKind { /// Make ours the sole desktop: primary + disable every other enabled output. Exclusive, /// Make ours primary but leave the other outputs enabled. Primary, } /// Outcome of [`apply_topology`]. pub(crate) struct TopologyOutcome { /// UUID of our resolved virtual output — a stable per-output id that survives a mode-switch /// supersede (unlike the shared name) — for later [`set_position`] / restore addressing. pub our_uuid: Option, /// The outputs we disabled, each `(name, "WxH@Hz")`, so teardown can restore them at their exact /// mode. Empty for `Primary`, or when nothing else was enabled. pub disabled: Vec<(String, String)>, /// `true` if the in-process path bound management, resolved our output, and applied (or tried to) /// a configuration. `false` ⇒ the compositor didn't answer in budget or our output never /// appeared, so the caller should fall back to `kscreen-doctor`. pub handled: bool, } /// One output as read from `kde_output_device_v2`. #[derive(Default, Clone)] struct DeviceState { /// The global `name` number (higher = more recently advertised) — used to pick the newest of two /// same-named outputs during a supersede. global: u32, name: Option, uuid: Option, enabled: bool, /// KWin's output priority; 1 is the primary. `None` until the `priority` event (device ≥ v18). priority: Option, /// The `current_mode` object id; its size is looked up in [`State::mode_dims`]. current_mode: Option, /// Every mode this output advertised, in announce order — `(mode object id, proxy)` — so restore /// can pick the one matching a captured `WxH@Hz`. modes: Vec<(ObjectId, DeviceMode)>, /// Set once this output's `done` burst has been seen (its state is coherent to read). seen_done: bool, proxy: Option, } /// Everything the enumerate/apply queue accumulates on one connection. #[derive(Default)] struct State { management: Option, mgmt_name_version: Option<(u32, u32)>, devices: HashMap, /// mode object id → `(width, height, refresh_mHz)`. mode_dims: HashMap, /// Highest `wl_callback` serial whose `done` has arrived — the barrier the pump waits on. sync_done: u32, /// Configuration apply verdict: `Some(true)` = applied, `Some(false)` = failed. applied: Option, failure_reason: Option, } impl Dispatch for State { fn event( state: &mut Self, registry: &WlRegistry, event: wl_registry::Event, _: &(), _: &Connection, qh: &QueueHandle, ) { match event { wl_registry::Event::Global { name, interface, version, } => { if interface == OutputManagement::interface().name { let v = version.min(MGMT_MAX); state.management = Some(registry.bind::(name, v, qh, ())); state.mgmt_name_version = Some((name, v)); } else if interface == OutputDevice::interface().name { let v = version.min(DEVICE_MAX); // The device's `name` global carries into the device's UserData so the event // handler can record it (newest-wins tie-break during a supersede). let dev = registry.bind::(name, v, qh, name); let id = dev.id(); state.devices.entry(id).or_default().proxy = Some(dev); } } wl_registry::Event::GlobalRemove { .. } => {} _ => {} } } } // Management has no events. impl Dispatch for State { fn event( _: &mut Self, _: &OutputManagement, _: management::kde_output_management_v2::Event, _: &(), _: &Connection, _: &QueueHandle, ) { } } /// The device's UserData is its global `name` number. impl Dispatch for State { fn event( state: &mut Self, device: &OutputDevice, event: DeviceEvent, global: &u32, _: &Connection, _: &QueueHandle, ) { let entry = state.devices.entry(device.id()).or_default(); entry.global = *global; if entry.proxy.is_none() { entry.proxy = Some(device.clone()); } match event { DeviceEvent::Name { name } => entry.name = Some(name), DeviceEvent::Uuid { uuid } => entry.uuid = Some(uuid), DeviceEvent::Enabled { enabled } => entry.enabled = enabled != 0, DeviceEvent::Priority { priority } => entry.priority = Some(priority), DeviceEvent::CurrentMode { mode } => entry.current_mode = Some(mode.id()), DeviceEvent::Mode { mode } => entry.modes.push((mode.id(), mode)), DeviceEvent::Done => entry.seen_done = true, _ => {} } } // The `mode` event hands us a server-created `kde_output_device_mode_v2`. The opcode is a bare // literal (the macro's fragment matcher rejects a `const` in some wayland-client versions); it is // pinned to `DEVICE_MODE_EVENT_OPCODE` by `mode_event_opcode_is_two` below. event_created_child!(State, OutputDevice, [ 2 => (DeviceMode, ()), ]); } impl Dispatch for State { fn event( state: &mut Self, mode: &DeviceMode, event: ModeEvent, _: &(), _: &Connection, _: &QueueHandle, ) { let entry = state.mode_dims.entry(mode.id()).or_insert((0, 0, 0)); match event { ModeEvent::Size { width, height } => { entry.0 = width.max(0) as u32; entry.1 = height.max(0) as u32; } ModeEvent::Refresh { refresh } => entry.2 = refresh.max(0) as u32, _ => {} } } } impl Dispatch for State { fn event( state: &mut Self, _: &OutputConfig, event: ConfigEvent, _: &(), _: &Connection, _: &QueueHandle, ) { match event { ConfigEvent::Applied => state.applied = Some(true), ConfigEvent::Failed => state.applied = Some(false), ConfigEvent::FailureReason { reason } => state.failure_reason = Some(reason), _ => {} } } } impl Dispatch for State { fn event( state: &mut Self, _: &WlCallback, event: wl_callback::Event, serial: &u32, _: &Connection, _: &QueueHandle, ) { if let wl_callback::Event::Done { .. } = event { state.sync_done = state.sync_done.max(*serial); } } } /// A connected, bound output-management session on its own Wayland connection. struct Session { conn: Connection, queue: wayland_client::EventQueue, state: State, next_sync: u32, } impl Session { /// Connect to the KWin Wayland socket, bind `kde_output_management_v2` + every /// `kde_output_device_v2`, and read each output's state — all bounded by `OP_BUDGET`. `None` if /// we can't connect, the management global isn't advertised, or the compositor doesn't answer in /// budget (the wedge case — the caller then falls back to `kscreen-doctor`). fn open() -> Option { let conn = Connection::connect_to_env().ok()?; let queue = conn.new_event_queue(); let qh = queue.handle(); let _registry = conn.display().get_registry(&qh, ()); let mut s = Session { conn, queue, state: State::default(), next_sync: 0, }; let deadline = Instant::now() + OP_BUDGET; // Phase 1: process the registry globals (binds management + every device in the handler). if !s.sync_barrier(deadline) { return None; } if s.state.management.is_none() { tracing::debug!( "KWin does not advertise kde_output_management_v2 to this client — kscreen-doctor \ fallback" ); return None; } // Phase 2: flush the device binds issued in phase 1 and drain each output's state burst // (name / enabled / priority / current_mode / mode sizes / done). if !s.sync_barrier(deadline) { return None; } Some(s) } /// Send a `wl_display.sync` and pump the queue until its `done` arrives or `deadline` passes. /// Returns `true` on the barrier, `false` on timeout. fn sync_barrier(&mut self, deadline: Instant) -> bool { self.next_sync += 1; let serial = self.next_sync; let qh = self.queue.handle(); let _cb = self.conn.display().sync(&qh, serial); self.pump_until(deadline, |st| st.sync_done >= serial) } /// Bounded manual event loop: flush, dispatch what's queued, then poll the connection fd for up /// to `POLL_MS` and read. Mirrors the keepalive loop in `kwin.rs::run` (blocking_dispatch can't /// be interrupted, so we poll the fd instead). Returns `true` once `done(&state)` holds. fn pump_until(&mut self, deadline: Instant, done: impl Fn(&State) -> bool) -> bool { loop { if done(&self.state) { return true; } if self.queue.dispatch_pending(&mut self.state).is_err() { return false; } if done(&self.state) { return true; } if Instant::now() >= deadline { return false; } if self.conn.flush().is_err() { return false; } let Some(guard) = self.conn.prepare_read() else { continue; // events already queued — loop dispatches them }; let mut pfd = libc::pollfd { fd: self.conn.as_fd().as_raw_fd(), events: libc::POLLIN, revents: 0, }; let remaining = deadline.saturating_duration_since(Instant::now()); let timeout = (remaining.as_millis() as i32).clamp(0, POLL_MS); // SAFETY: `&mut pfd` points at one live, fully-initialized `libc::pollfd` on the stack and // the count `1` matches that single element, so `poll` reads `fd`/`events` and writes // `revents` strictly within `pfd`. `pfd.fd` is the Wayland connection's fd, valid because // `self.conn` (and the `prepare_read` guard) outlive the call. `poll` blocks up to // `timeout` ms and writes only `revents`; `pfd` is a fresh local that aliases nothing. let r = unsafe { libc::poll(&mut pfd, 1, timeout) }; if r > 0 && (pfd.revents & libc::POLLIN) != 0 { let _ = guard.read(); } // else: timeout/signal — drop the guard, re-check the deadline } } /// A fresh `kde_output_configuration_v2` on this connection. fn new_config(&self) -> OutputConfig { let qh = self.queue.handle(); self.state .management .as_ref() .unwrap() .create_configuration(&qh, ()) } /// `apply()` the config and pump until `applied`/`failed` or the deadline. Returns the verdict /// (`true` applied, `false` failed/timeout). fn apply(&mut self, config: &OutputConfig, deadline: Instant) -> bool { self.state.applied = None; config.apply(); let ok = self.pump_until(deadline, |st| st.applied.is_some()); if !ok { return false; } matches!(self.state.applied, Some(true)) } /// The current-mode size of a device as `(w, h, refresh_mHz)`, if known. fn current_dims(&self, dev: &DeviceState) -> Option<(u32, u32, u32)> { let id = dev.current_mode.as_ref()?; self.state.mode_dims.get(id).copied() } } /// `(width, height, "WxH@Hz")` capture of a device's current mode, Hz rounded — the same shape the /// `kscreen-doctor` restore path used, so teardown can put a panel back at its real refresh. fn mode_spec(dims: (u32, u32, u32)) -> String { let hz = ((dims.2 as f64) / 1000.0).round() as u32; format!("{}x{}@{}", dims.0, dims.1, hz) } /// Prefix EVERY managed KWin output shares (mirrors `kwin::MANAGED_PREFIX`) — the streamed outputs /// are `Virtual-punktfunk` / `Virtual-punktfunk-`, so a same-family sibling session is never /// treated as a physical to disable, and its primary is never stolen (first-slot-wins). const MANAGED_PREFIX: &str = "Virtual-punktfunk"; /// Make the streamed output (name starts with `our_prefix`, current size `our_w`×`our_h`) the /// primary — and, for `Exclusive`, disable every other enabled output — over `kde_output_management_v2`. /// See the module docs for why this is done in-process instead of via `kscreen-doctor`. pub(crate) fn apply_topology( our_prefix: &str, our_w: u32, our_h: u32, kind: TopologyKind, ) -> TopologyOutcome { let miss = || TopologyOutcome { our_uuid: None, disabled: Vec::new(), handled: false, }; let Some(mut sess) = Session::open() else { return miss(); }; let deadline = Instant::now() + OP_BUDGET; // Resolve OUR output: managed-prefix name AND current size == the birth size (only the // just-created output sits there during a supersede); newest global wins the tie. let ours = sess .state .devices .values() .filter(|d| { d.name.as_deref().is_some_and(|n| n.starts_with(our_prefix)) && sess.current_dims(d).map(|(w, h, _)| (w, h)) == Some((our_w, our_h)) }) .max_by_key(|d| d.global) .cloned(); let Some(ours) = ours else { tracing::warn!( our_prefix, our_w, our_h, "KWin output management: our virtual output hasn't appeared yet — kscreen-doctor fallback" ); return miss(); }; let our_uuid = ours.uuid.clone(); let our_id = ours.proxy.as_ref().map(|p| p.id()); // First-slot-wins (§6.1): don't steal primary if another managed sibling already holds it // (priority 1) — a 2nd exclusive session joins as a secondary of the shared desktop. let sibling_is_primary = sess.state.devices.values().any(|d| { d.enabled && d.priority == Some(1) && d.proxy.as_ref().map(|p| p.id()) != our_id && d.name .as_deref() .is_some_and(|n| n.starts_with(MANAGED_PREFIX)) }); // The physical/bootstrap outputs to disable for `Exclusive`: enabled, not any managed sibling, // not ours. Captured WITH their current mode so teardown restores the exact refresh. let mut to_disable: Vec<(OutputDevice, String, String)> = Vec::new(); if kind == TopologyKind::Exclusive { for d in sess.state.devices.values() { let is_ours = d.proxy.as_ref().map(|p| p.id()) == our_id; let managed = d .name .as_deref() .is_some_and(|n| n.starts_with(MANAGED_PREFIX)); if d.enabled && !is_ours && !managed { if let (Some(name), Some(proxy)) = (d.name.clone(), d.proxy.clone()) { let spec = sess.current_dims(d).map(mode_spec).unwrap_or_default(); to_disable.push((proxy, name, spec)); } } } } // Build one configuration: ensure ours is enabled, take primary (unless a sibling holds it), // disable the others. `apply()` is atomic — KWin re-homes the shell onto the remaining desktop. let config = sess.new_config(); if let Some(proxy) = ours.proxy.as_ref() { config.enable(proxy, 1); if !sibling_is_primary { config.set_primary_output(proxy); } } for (proxy, _, _) in &to_disable { config.enable(proxy, 0); } let applied = sess.apply(&config, deadline); config.destroy(); // Always report the outputs we ASKED to disable so teardown restores them: re-enabling an output // that never actually got disabled is a harmless no-op, whereas dropping them here would strand a // physical dark if KWin processed the disable but the `applied` ack didn't land in budget. let disabled: Vec<(String, String)> = to_disable .into_iter() .map(|(_, name, spec)| (name, spec)) .collect(); if applied { tracing::info!( also_disabled = ?disabled, primary_taken = !sibling_is_primary, "KWin output management: streamed output set as the desktop (in-process)" ); } else { tracing::warn!( reason = ?sess.state.failure_reason, also_disabled = ?disabled, "KWin output management: apply() not confirmed in budget — proceeding (restore will re-enable)" ); } // We resolved our output and drove the config over Wayland; don't ALSO run kscreen-doctor — that // would double-apply (and on a wedged box it would just add 26 s of timeouts). `handled` is true // even on an unconfirmed apply; a genuinely absent management global / unresolved output took the // `handled = false` early returns above and falls back to kscreen-doctor. TopologyOutcome { our_uuid, disabled, handled: true, } } /// Re-enable outputs by name at their captured `WxH@Hz` modes (teardown), in-process. Returns /// `true` if the config applied; `false` (compositor unresponsive / management absent) tells the /// caller to fall back to `kscreen-doctor`. pub(crate) fn reenable_outputs(outputs: &[(String, String)]) -> bool { if outputs.is_empty() { return true; } let Some(mut sess) = Session::open() else { return false; }; let deadline = Instant::now() + OP_BUDGET; let config = sess.new_config(); for (name, spec) in outputs { // Find the device by name (physical names are stable across a session). let Some(dev) = sess .state .devices .values() .find(|d| d.name.as_deref() == Some(name.as_str())) .cloned() else { continue; }; let Some(proxy) = dev.proxy.as_ref() else { continue; }; // Enable first — a bare enable always succeeds, so a physical is never left dark. config.enable(proxy, 1); // Then re-assert the captured mode so a 120 Hz panel doesn't return at KWin's ~60 Hz default. if let Some(mode) = find_mode(&sess, &dev, spec) { config.mode(proxy, &mode); } } let ok = sess.apply(&config, deadline); config.destroy(); if ok { tracing::info!(reenabled = ?outputs, "KWin output management: restored outputs (in-process)"); } ok } /// Position the output identified by `uuid` at `(x, y)` in the desktop layout, in-process. Returns /// `true` if applied; `false` tells the caller to fall back to `kscreen-doctor`. pub(crate) fn set_position(uuid: &str, x: i32, y: i32) -> bool { let Some(mut sess) = Session::open() else { return false; }; let deadline = Instant::now() + OP_BUDGET; let Some(dev) = sess .state .devices .values() .find(|d| d.uuid.as_deref() == Some(uuid)) .cloned() else { return false; }; let Some(proxy) = dev.proxy.as_ref() else { return false; }; let config = sess.new_config(); config.position(proxy, x, y); let ok = sess.apply(&config, deadline); config.destroy(); if ok { tracing::info!( uuid, x, y, "KWin output management: placed output (in-process)" ); } ok } /// Find a device's advertised mode proxy matching a captured `"WxH@Hz"` spec (Hz rounded), for /// restore. `None` if the spec is empty or no mode matches (the caller then enables without a mode /// and lets KWin pick its preferred one). fn find_mode(sess: &Session, dev: &DeviceState, spec: &str) -> Option { if spec.is_empty() { return None; } let (wh, hz) = spec.split_once('@')?; let (w, h) = wh.split_once('x')?; let (w, h, hz): (u32, u32, u32) = (w.parse().ok()?, h.parse().ok()?, hz.parse().ok()?); dev.modes.iter().find_map(|(id, proxy)| { let (mw, mh, mmhz) = sess.state.mode_dims.get(id).copied()?; let mhz = ((mmhz as f64) / 1000.0).round() as u32; ((mw, mh, mhz) == (w, h, hz)).then(|| proxy.clone()) }) } #[cfg(test)] mod tests { use super::*; /// The `WxH@Hz` capture rounds mHz to whole Hz — the shape teardown parses back. #[test] fn mode_spec_rounds_millihertz() { assert_eq!(mode_spec((2560, 1440, 59940)), "2560x1440@60"); assert_eq!(mode_spec((1920, 1080, 60000)), "1920x1080@60"); assert_eq!(mode_spec((3840, 2160, 119880)), "3840x2160@120"); } /// The vendored device XML must keep `mode` at the opcode the `event_created_child!` macro /// hardcodes — a reorder there would bind the child to the wrong event and desync mode sizes. #[test] fn mode_event_opcode_is_two() { assert_eq!(DEVICE_MODE_EVENT_OPCODE, 2); } }