srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/udev/session.rs
blob: e39c578a60e722acea43ed94679c3bd1ce9b4d09 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
use super::*;

pub(crate) fn register_drm_fd(handle: &LoopHandle<'static, CompState>, card: &Rc<Card>) -> 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(())
}

pub(crate) fn register_libinput(handle: &LoopHandle<'static, CompState>, session: &LibSeatSession, seat_name: &str) -> PlatformResult<()> {
    let mut libinput_context = Libinput::new_with_udev::<LibinputSessionInterface<LibSeatSession>>(session.clone().into());
    libinput_context.udev_assign_seat(seat_name).map_err(|_| PlatformError::Other("udev: libinput udev_assign_seat failed".into()))?;
    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(())
}

pub(crate) fn register_session_notifier(handle: &LoopHandle<'static, CompState>, notifier: LibSeatSessionNotifier) -> 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;
                }
                SessionEvent::ActivateSession => {
                    log::info!("udev: session resumed (VT switch back)");
                    udev.active = true;
                    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}"),
                    }

                    // Some drivers reset mode-setting state across a VT
                    // switch; reassert every 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 &mut udev.heads {
                        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}");
                        }
                        // 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<LibinputInputBackend>) {
    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;
            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;
            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 });
        }
        _ => {}
    }
}