use super::*; pub(crate) fn register_drm_fd(handle: &LoopHandle<'static, CompState>, card: &Rc) -> PlatformResult<()> { let raw = card.as_fd().as_raw_fd(); // SAFETY: `FdWrapper` does not close `raw`; the owning `Card` lives in // `CompState::udev` for as long as this event source is registered. let wrapper = unsafe { FdWrapper::new(raw) }; let source = Generic::new(wrapper, Interest::READ, CalloopMode::Level); handle .insert_source(source, move |_, _, data: &mut CompState| { let Some(udev) = data.udev.as_ref() else { return Ok(PostAction::Continue) }; let card = udev.card.clone(); match card.receive_events() { Ok(events) => { // The event names the CRTC it came from, so with several // monitors only that head advances - flipping all of // them would desynchronise the others' buffers. let mut flipped = false; for event in events { let DrmEvent::PageFlip(flip) = event else { continue }; if let Some(udev) = data.udev.as_mut() { if let Some(head) = udev.heads.iter_mut().find(|h| h.crtc == flip.crtc) { head.front = 1 - head.front; head.flip_pending = false; flipped = true; } } } if flipped { data.render_udev_frame(); } } Err(e) => log::warn!("udev: receive_events failed: {e}"), } Ok(PostAction::Continue) }) .map_err(|e| PlatformError::Other(format!("failed to register DRM fd: {e}")))?; Ok(()) } /// Registers the `DrmDeviceNotifier` [`gpu::probe`](super::gpu::probe) /// returns on success as its own calloop event source - a real, /// independent `DrmDevice`/fd from the legacy one [`register_drm_fd`] /// above already watches, so this is a second, parallel registration, not /// a replacement. Only called from `udev/platform.rs`'s startup path when /// `SRDWM_GPU=1` and the probe actually succeeded; a no-op (never called /// at all) otherwise. /// /// `DrmEvent::VBlank` marks the matching `GpuOutput` (if any - `udev.gpu` /// might still be mid-construction, or the crtc might not be this /// backend's GPU-driven one) as having had its frame actually scanned /// out, via `frame_submitted()` - required by `DrmOutput::queue_frame`'s /// own doc comment (see `render_udev_frame`'s GPU branch), or the /// underlying swapchain eventually runs out of buffers. `DrmEvent::Error` /// is logged, not treated as fatal - matching every other error-handling /// choice in this GPU path, which always prefers "log and keep going" over /// tearing down the whole session for an experimental, opt-in feature. pub(crate) fn register_gpu_drm_notifier(handle: &LoopHandle<'static, CompState>, notifier: smithay::backend::drm::DrmDeviceNotifier) -> PlatformResult<()> { // Fully qualified, not the bare `DrmEvent` this module's own `use // super::*` already brings in - that name is `drm::control::Event` // (the legacy raw drm-rs event type `register_drm_fd` above reads via // `card.receive_events()`, with a `PageFlip` variant), a different // type from smithay's own higher-level `backend::drm::DrmEvent` (with // `Vblank`/`Error` variants) this notifier actually produces. handle .insert_source(notifier, move |event, _metadata, data: &mut CompState| match event { smithay::backend::drm::DrmEvent::VBlank(crtc) => { if let Some(udev) = data.udev.as_mut() { if let Some(gpu) = udev.gpu.as_ref() { if let Some(gpu_output) = gpu.output_for(crtc) { if let Err(e) = gpu_output.frame_submitted() { log::warn!("udev: SRDWM_GPU=1 frame_submitted failed for crtc {crtc:?}: {e:?}"); } } } } } smithay::backend::drm::DrmEvent::Error(e) => log::warn!("udev: SRDWM_GPU=1 DrmDevice error: {e:?}"), }) .map_err(|e| PlatformError::Other(format!("failed to register GPU DRM notifier: {e}")))?; Ok(()) } /// Registers the real libinput event source and hands back a second, /// reference-counted handle onto the exact same underlying context (`Libinput` /// wraps a `libinput_ref`/`libinput_unref`-counted C pointer - see its own /// `Clone` impl - so this is the same live context `LibinputInputBackend` /// dispatches events from, not a separate one) for `register_session_notifier` /// to call `suspend()`/`resume()` on across a VT switch. `LibinputInputBackend` /// itself only ever exposes an immutable `&Libinput` (`context()`), and is /// moved into calloop's event source registration below with no way to get /// a `&mut` back out afterward - cloning before that move is the only way /// to keep a callable handle at all. pub(crate) fn register_libinput(handle: &LoopHandle<'static, CompState>, session: &LibSeatSession, seat_name: &str) -> PlatformResult { let mut libinput_context = Libinput::new_with_udev::>(session.clone().into()); libinput_context.udev_assign_seat(seat_name).map_err(|_| PlatformError::Other("udev: libinput udev_assign_seat failed".into()))?; let resume_handle = libinput_context.clone(); let libinput_backend = LibinputInputBackend::new(libinput_context); handle .insert_source(libinput_backend, move |event, _, data: &mut CompState| { handle_libinput_event(data, event); }) .map_err(|e| PlatformError::Other(format!("failed to register libinput backend: {e}")))?; Ok(resume_handle) } /// `libinput` is a second handle onto the exact same context /// `register_libinput` gave its own event source - see that function's own /// doc comment for why a clone is the only way to get one at all. Kept /// alive by this closure for as long as this event source is registered /// (the entire life of the process), purely so `suspend()`/`resume()` can be /// called on it here. pub(crate) fn register_session_notifier(handle: &LoopHandle<'static, CompState>, notifier: LibSeatSessionNotifier, mut libinput: Libinput) -> PlatformResult<()> { handle .insert_source(notifier, move |event, &mut (), data: &mut CompState| { let Some(udev) = data.udev.as_mut() else { return }; match event { SessionEvent::PauseSession => { log::info!("udev: session paused (VT switch away)"); udev.active = false; // Not strictly required (the kernel revokes every input // device's fd across a VT switch regardless), but this // is the documented, correct way to tell libinput that's // about to happen rather than let it discover revoked // fds as surprise read errors - see `resume()`'s own // doc comment below for why the other half of this pair // is not optional at all. libinput.suspend(); // `SRDWM_GPU=1` only - see `gpu::probe`'s own doc // comment. `DrmOutputManager::pause` (-> `DrmDevice:: // pause`) drops this device's own DRM master lock and // marks it inactive, the same pairing `anvil` itself // uses around a VT switch - a separate device/fd from // the legacy `Card` above, so this is additive, not a // substitute for anything already happening here. if let Some(gpu) = udev.gpu.as_mut() { gpu.output_manager.pause(); } } SessionEvent::ActivateSession => { log::info!("udev: session resumed (VT switch back)"); udev.active = true; // Without this, libinput's own internal device list // stays exactly as it was before the switch away, still // holding the same file descriptors the kernel already // revoked the moment this session lost the VT -- // `receive_events`/`dispatch` on a revoked fd doesn't // error, it just silently never produces another event, // forever. Confirmed live: after one real VT switch, no // further keyboard or mouse input reached this // compositor for the rest of the session (30+ minutes, // multiple confirmed attempts) with literally nothing // logged anywhere to explain it - rendering, the DRM/ // DPMS state above, and even libseat's own session // activation all recovered correctly on their own, which // is what made this the one piece actually missing // rather than a repeat of the DPMS bug. `resume()` // (`libinput_resume`) is libinput's own documented API // for exactly this: it re-opens every device through // the (now reactivated) session and resumes producing // real events, the same call every other libinput-based // compositor's session-resume path makes and this one // never did. if libinput.resume().is_err() { log::warn!("udev: libinput resume failed after VT switch back - input devices may not recover; a full restart will be needed if so"); } let card = udev.card.clone(); // A flip issued right before the VT switch away may // never have completed while inactive (nothing was // scanning out), and its completion event can still be // sitting undelivered on the DRM fd. The kernel refuses // a new page flip on a CRTC with one already // unacknowledged (EBUSY) - drain and apply any such // events now, before reasserting crtcs, so a stale flip // from before the switch can't collide with the fresh // one `render_udev_frame` is about to issue below. match card.receive_events() { Ok(events) => { for event in events { let DrmEvent::PageFlip(flip) = event else { continue }; if let Some(head) = udev.heads.iter_mut().find(|h| h.crtc == flip.crtc) { head.front = 1 - head.front; head.flip_pending = false; } } } Err(e) => log::debug!("udev: no pending flip events to drain on resume: {e}"), } // `SRDWM_GPU=1` only. `DrmOutputManager::activate` (-> // `DrmDevice::activate`) re-acquires this device's own // DRM master lock - a separate device/fd from the // legacy `Card` below, so this can run regardless of // whether any legacy head also needs reasserting. // Deliberately does *not* also force a fresh render // here: `data.render_udev_frame()` at the end of this // arm already reaches the GPU head unconditionally // (`render.rs`'s GPU branch), and `DrmCompositor:: // render_frame` always issues a full state commit -- // atomic or legacy, whichever this specific device // negotiated (see `gpu::probe`'s own doc comment) -- // not just a buffer swap, so that one call already // reasserts mode-set and CRTC-active state together, // unlike the legacy heads below, which need `set_crtc` // called explicitly first because their own flip path // (`copy_and_flip`) only ever swaps the buffer. // `disable_connectors: false` - matches this // compositor's own control over which connectors are // actually driven (`disabled_connectors`), not // something this resume path should reset. if let Some(gpu) = udev.gpu.as_mut() { if let Err(e) = gpu.output_manager.activate(false) { log::warn!("udev: SRDWM_GPU=1 failed to reactivate DrmOutputManager on resume: {e}"); } } // Every crtc `SRDWM_GPU=1`'s `DrmOutputManager` is // driving (if any - now possibly several, since it // targets every head it could, not just one) -- // excluded from the legacy reassert loop below, since // that loop's `set_crtc` runs through the *legacy* // `Card`/fd, a completely different device handle than // the GPU path's own `DrmDeviceFd`. Two separate fds // issuing mode-set commands against the same physical // CRTC is exactly the kind of conflict that produced // this session's own worst VT-switch incidents when it // was really one fd racing itself (`EBUSY` loops - see // `UdevHead::flip_retry_after`'s own doc comment) - not // a risk worth re-introducing here for a head this // resume path already just reactivated correctly // through its own, real API. let gpu_crtcs: Vec = udev.gpu.as_ref().map(|g| g.outputs.iter().map(|(c, _)| *c).collect()).unwrap_or_default(); // Some drivers reset mode-setting state across a VT // switch; reassert every (non-GPU-driven) head before // rendering again. // // The real connector and mode, not an empty connector // list and no mode - that shape is DRM/KMS's own way // to *disable* a CRTC, not reassert it, and was // confirmed live to leave the screen black after // switching back with no further VT switch, either // direction, able to recover it. See `UdevHead::mode`'s // own doc comment. for head in udev.heads.iter_mut().filter(|h| !gpu_crtcs.contains(&h.crtc)) { let fb = head.buffers[head.front].fb; if let Err(e) = card.set_crtc(head.crtc, Some(fb), (0, 0), &[head.connector], Some(head.mode)) { log::warn!("udev: failed to reassert crtc on resume: {e}"); } // A real VT-switch-back can leave the panel itself // powered down (DPMS off/standby) even once the CRTC // above is genuinely re-driving it - mode-setting // and display power are two separate pieces of KMS // state, and only the former was ever touched here. // Confirmed live: page flips kept succeeding with no // error logged anywhere (this backend's own // `copy_and_flip` never saw a failure) for the rest // of a 30+ minute session after a real switch-away- // and-back, and neither the screen nor keyboard // input ever recovered on their own - every frame // was genuinely being composited and flipped to a // CRTC the panel simply wasn't lit to show. Setting // "DPMS" straight to on here, unconditionally, is // the same property `set_output_power` already // writes for `zwlr_output_power_v1` - this is just // that same write happening automatically on every // resume instead of only when a client explicitly // asks, since nothing else in this codebase ever // calls it after a VT switch. A no-op on hardware // that never needed it (the property is simply // already `on`, or genuinely absent on some virtual/ // headless outputs - `find` below just yields // nothing and this silently continues to the next // head either way). const DRM_MODE_DPMS_ON: u64 = 0; if let Ok(props) = card.get_properties(head.connector) { if let Some(dpms_prop) = props.as_props_and_values().0.iter().copied().find(|&h| card.get_property(h).is_ok_and(|info| info.name().to_str() == Ok("DPMS"))) { if let Err(e) = card.set_property(head.connector, dpms_prop, DRM_MODE_DPMS_ON) { log::warn!("udev: failed to reassert DPMS-on for output {} on resume: {e}", head.output.name()); } } } // Force a full repaint: contents are undefined after // the VT switch (another VT's session may have // scanned out something else entirely in between). head.flip_pending = false; head.ages = [0, 0]; head.flip_retry_after = None; } data.render_udev_frame(); } } }) .map_err(|e| PlatformError::Other(format!("failed to register session notifier: {e}")))?; Ok(()) } /// Watches udev for DRM device changes. The kernel emits a `change` uevent /// on the card when a connector is plugged or unplugged, which smithay /// surfaces as [`UdevEvent::Changed`] - that is the hotplug signal. /// /// `Added`/`Removed` refer to whole GPUs appearing or disappearing, which /// this backend does not support (it binds one primary GPU at startup), so /// they are logged and ignored rather than silently dropped. pub(crate) fn register_udev_monitor(handle: &LoopHandle<'static, CompState>, seat_name: &str) -> PlatformResult<()> { let backend = UdevBackend::new(seat_name).map_err(err)?; handle .insert_source(backend, move |event, _, data: &mut CompState| match event { UdevEvent::Changed { .. } => { data.reprobe_outputs(); data.render_udev_frame(); } UdevEvent::Added { path, .. } => { log::info!("udev: new GPU {} appeared; multi-GPU is not supported, ignoring", path.display()) } UdevEvent::Removed { .. } => log::info!("udev: a GPU was removed; multi-GPU is not supported, ignoring"), }) .map_err(|e| PlatformError::Other(format!("failed to register udev monitor: {e}")))?; Ok(()) } fn handle_libinput_event(state: &mut CompState, event: InputEvent) { match event { InputEvent::Keyboard { event } => handle_keyboard_key_event(state, &event), InputEvent::PointerMotion { event } => { let Some(udev) = state.udev.as_mut() else { return }; let delta = event.delta(); // Clamped to the union of every head, so the pointer travels // between monitors instead of stopping at the first one's edge // - `min_x`/`min_y`, not a hardcoded `0.0` floor, so a head // placed at a negative origin (a real "extend left"/"extend // above" arrangement) is actually reachable. See `bounds`'s own // doc comment for the live bug this fixes. let (min_x, min_y, max_x, max_y) = udev.bounds(); udev.pointer_pos.x = (udev.pointer_pos.x + delta.x).clamp(min_x, (max_x - 1.0).max(min_x)); udev.pointer_pos.y = (udev.pointer_pos.y + delta.y).clamp(min_y, (max_y - 1.0).max(min_y)); let pos = udev.pointer_pos; // Multi-cursor mode, Phase 1 (see `UdevState::secondary_ // cursors`'s own doc comment): records this specific physical // device's own position too, purely for rendering its own // cursor sprite - `pos`/`handle_pointer_position` below are // still the one interactive position, unchanged. udev.secondary_cursors.insert(event.device(), pos); handle_pointer_position(state, pos, event.time_msec()); } // Absolute-positioning devices (a touchscreen, a drawing tablet, // and - confirmed live via a `WAYLAND_DEBUG=1` trace from a peer // session - ydotool's virtual uinput device, used throughout this // whole debugging effort) had no handler here at all: this match // only ever covered `PointerMotion` (relative deltas), so every // `PointerMotionAbsolute` event fell through to the catch-all // below and was silently dropped. The winit (nested) backend // already handles this exact event via `event.position_transformed` // (see `winit/events.rs`'s matching arm); this is that same // pattern for the bare-metal backend, which never got it. Uses the // same union-of-every-head bounds `PointerMotion` above clamps // into, so a single absolute-positioning device still addresses // the whole multi-monitor span, not just the first head. InputEvent::PointerMotionAbsolute { event } => { let Some(udev) = state.udev.as_mut() else { return }; // `position_transformed` maps the device's own normalized // [0,1] position into a `(0, 0)`-anchored size - offset by // `min_x`/`min_y` afterward, same reasoning as `PointerMotion` // above, so this still addresses a negative-origin head. let (min_x, min_y, max_x, max_y) = udev.bounds(); let size = Size::from(((max_x - min_x) as i32, (max_y - min_y) as i32)); let pos = event.position_transformed(size); udev.pointer_pos.x = (pos.x + min_x).clamp(min_x, (max_x - 1.0).max(min_x)); udev.pointer_pos.y = (pos.y + min_y).clamp(min_y, (max_y - 1.0).max(min_y)); let pos = udev.pointer_pos; udev.secondary_cursors.insert(event.device(), pos); handle_pointer_position(state, pos, event.time_msec()); } InputEvent::PointerButton { event } => { let Some(pos) = state.udev.as_ref().map(|u| u.pointer_pos) else { return }; let button = event.button_code(); let pressed = event.state() == BackendButtonState::Pressed; handle_pointer_button(state, pos, button, pressed, event.time_msec()); } // Laptop lid. libinput reports this as a switch toggle; without // handling it, closing the lid does nothing at all - no lock, no // suspend - which is a genuine problem on a laptop rather than a // missing nicety. InputEvent::SwitchToggle { event } => { // Fully qualified: libinput's own `Switch` is also in scope here. use smithay::backend::input::{SwitchState, SwitchToggleEvent}; if matches!(event.switch(), Some(smithay::reexports::input::event::switch::Switch::Lid)) { let closed = event.state() == SwitchState::On; log::info!("lid {}", if closed { "closed" } else { "opened" }); state.pending.borrow_mut().push(CoreEvent::LidSwitch { closed }); } } InputEvent::PointerAxis { event } => { // Modifier+scroll switches workspace instead of reaching the // client - the `bind = SUPER, mouse_down/up, workspace, e+1/e-1` // gesture. Checked first so the client never sees these events; // forwarding them too would scroll the window under the cursor // as a side effect of changing workspace. if crate::input::handle_workspace_scroll(state, &event) { return; } // Otherwise: forwarded to the focused client via the pointer axis // frame, no WM-level handling. let Some(pointer) = state.seat.get_pointer() else { return }; let source = event.source(); let mut frame = AxisFrame::new(event.time_msec()).source(source); for axis in [Axis::Horizontal, Axis::Vertical] { match event.amount(axis) { Some(value) => frame = frame.value(axis, value), // `AxisSource::Finger` (a touchpad) *requires* a stop // event on the frame where the finger lifts and the // axis genuinely has no more motion - see `AxisFrame:: // source`'s own doc comment ("Using AxisSource::Finger // requires a stop event to be sent, when the user lifts // off the finger"). Never sending it left every // two-finger scroll gesture with no way to tell Firefox/ // GTK it had actually ended, which is exactly the kind // of thing that reads as "scrolling doesn't work" -- // not "no events arrive" (discrete wheel scrolling, // which needs no stop event, was never affected) but // kinetic/momentum scrolling and starting a fresh // gesture right after a previous one never settling. None if source == AxisSource::Finger => frame = frame.stop(axis), None => {} } // Discrete wheel steps, additional to the pixel `value` // above - optional (`value` is the only event a client // strictly needs), but some clients use it to distinguish // "one physical click" from a smooth/high-resolution // scroll, so provide it whenever the device actually // reports one (real scroll wheels; never touchpads, which // have no discrete steps to report - `amount_v120` is // `None` for those, same guarantee `amount` gives the // other way around). if let Some(v120) = event.amount_v120(axis) { frame = frame.v120(axis, v120 as i32); } } pointer.axis(state, frame); pointer.frame(state); } // 3+-finger swipe - claimed entirely for workspace switching, never // reaches a client. See `handle_gesture_swipe_end`'s doc comment. InputEvent::GestureSwipeBegin { event } => handle_gesture_swipe_begin(state, &event), InputEvent::GestureSwipeUpdate { event } => handle_gesture_swipe_update(state, &event), InputEvent::GestureSwipeEnd { event } => handle_gesture_swipe_end(state, &event), // Pinch/hold: no WM-level meaning, forwarded to the focused client // as-is (`wp_pointer_gestures`) - pinch-to-zoom in an image viewer // or PDF reader, the one real use either has. Same reasoning as the // `PointerAxis` forwarding above: nothing here should be silently // dropped just because this WM has no use for it itself. InputEvent::GesturePinchBegin { event } => { let Some(pointer) = state.seat.get_pointer() else { return }; let fingers = event.fingers(); pointer.gesture_pinch_begin(state, &GesturePinchBeginEvent { serial: SERIAL_COUNTER.next_serial(), time: event.time_msec(), fingers }); } InputEvent::GesturePinchUpdate { event } => { let Some(pointer) = state.seat.get_pointer() else { return }; let (delta, scale, rotation) = (event.delta(), event.scale(), event.rotation()); pointer.gesture_pinch_update(state, &GesturePinchUpdateEvent { time: event.time_msec(), delta, scale, rotation }); } InputEvent::GesturePinchEnd { event } => { let Some(pointer) = state.seat.get_pointer() else { return }; let cancelled = event.cancelled(); pointer.gesture_pinch_end(state, &GesturePinchEndEvent { serial: SERIAL_COUNTER.next_serial(), time: event.time_msec(), cancelled }); } InputEvent::GestureHoldBegin { event } => { let Some(pointer) = state.seat.get_pointer() else { return }; let fingers = event.fingers(); pointer.gesture_hold_begin(state, &GestureHoldBeginEvent { serial: SERIAL_COUNTER.next_serial(), time: event.time_msec(), fingers }); } InputEvent::GestureHoldEnd { event } => { let Some(pointer) = state.seat.get_pointer() else { return }; let cancelled = event.cancelled(); pointer.gesture_hold_end(state, &GestureHoldEndEvent { serial: SERIAL_COUNTER.next_serial(), time: event.time_msec(), cancelled }); } _ => {} } }