srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/udev/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/wayland/src/udev/mod.rs')
-rw-r--r--crates/wayland/src/udev/mod.rs294
1 files changed, 276 insertions, 18 deletions
diff --git a/crates/wayland/src/udev/mod.rs b/crates/wayland/src/udev/mod.rs
index 501e3fc..1490d13 100644
--- a/crates/wayland/src/udev/mod.rs
+++ b/crates/wayland/src/udev/mod.rs
@@ -33,13 +33,14 @@ use std::rc::Rc;
use std::time::{Duration, Instant};
use smithay::backend::input::{
- Axis, ButtonState as BackendButtonState, Event as InputEventTrait, GestureBeginEvent as BackendGestureBeginEvent,
+ AbsolutePositionEvent, Axis, ButtonState as BackendButtonState, Event as InputEventTrait, GestureBeginEvent as BackendGestureBeginEvent,
GestureEndEvent as BackendGestureEndEvent, GesturePinchUpdateEvent as BackendGesturePinchUpdateEvent, InputEvent,
PointerAxisEvent, PointerButtonEvent, PointerMotionEvent,
};
use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
use smithay::backend::renderer::damage::OutputDamageTracker;
use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement;
+use smithay::backend::renderer::element::solid::SolidColorBuffer;
use smithay::backend::renderer::element::Kind;
use smithay::backend::renderer::pixman::PixmanRenderer;
use smithay::backend::renderer::{Bind, ImportDma};
@@ -128,6 +129,11 @@ pub(crate) struct UdevHead {
/// A flip is in flight; the next frame for this head waits for the DRM
/// page-flip event (matched by `crtc`) before starting.
pub(crate) flip_pending: bool,
+ /// When the current `flip_pending` was set - lets `render_udev_frame`
+ /// notice a page-flip event that never arrived (or arrived but matched
+ /// no head - see `FLIP_TIMEOUT`'s own doc comment) instead of waiting
+ /// on it forever. Meaningless while `flip_pending` is `false`.
+ pub(crate) flip_pending_since: Instant,
/// Per-buffer-slot age passed to `damage_tracker.render_output`: how
/// many *damage-producing* renders ago that exact buffer was last
/// brought fully up to date. 0 means "never rendered, contents
@@ -149,6 +155,16 @@ pub(crate) struct UdevHead {
/// Origin of this head in the global coordinate space.
pub(crate) location: Point<i32, Logical>,
pub(crate) size: (i32, i32),
+ /// The DRM mode this head was actually brought up with - kept so a VT-
+ /// switch resume can reassert the CRTC with its real connector and
+ /// mode (see `register_session_notifier`'s own `ActivateSession` arm),
+ /// rather than the empty connector list and `None` mode that call used
+ /// to pass, which does not reassert a CRTC at all - it is DRM/KMS's
+ /// own shape for *disabling* one. Confirmed live: switching back to
+ /// srdwm's VT after switching away left the screen black, with no
+ /// further VT switch (either direction) able to recover it, matching a
+ /// CRTC left disabled rather than restored.
+ pub(crate) mode: DrmMode,
}
/// Everything the DRM/udev backend needs that the nested winit backend
@@ -173,14 +189,112 @@ pub(crate) struct UdevState {
/// backend needed the session handle after startup, so it was never
/// retained anywhere before this.
pub(crate) session: LibSeatSession,
+ /// Connector names administratively disabled via `srd dispatch set
+ /// output enabled <name> false` - still physically connected (DRM
+ /// still reports/probes them), just deliberately not driven. Checked
+ /// by `reprobe_outputs`'s own "added" loop so an unrelated hotplug
+ /// event doesn't resurrect one of these the next time anything else
+ /// plugs or unplugs - without this, the very next `Changed` uevent
+ /// (any connector, not just this one) would see a disabled-but-still-
+ /// present connector as newly "added" (present in a fresh probe,
+ /// absent from `heads`, exactly the condition that branch already
+ /// uses to detect a real hotplug) and bring it straight back up.
+ pub(crate) disabled_connectors: std::collections::HashSet<String>,
+ /// `WorkspaceId` this backend last built `custom_elements` for --
+ /// compared against `WindowManager::current_workspace()` at the top of
+ /// every `render_udev_frame` call so a switch can force every head's
+ /// `ages` back to `[0, 0]` (see that call site's own comment for why).
+ /// `None` before the very first frame, which already renders fully
+ /// regardless (every head starts with `ages: [0, 0]` - see
+ /// `UdevHead`'s own field).
+ pub(crate) last_rendered_workspace: Option<srdwm_core::WorkspaceId>,
}
impl UdevState {
- /// Bounding box of every head, used to clamp pointer motion.
- fn bounds(&self) -> (f64, f64) {
- let w = self.heads.iter().map(|h| h.location.x + h.size.0).max().unwrap_or(0);
- let h = self.heads.iter().map(|h| h.location.y + h.size.1).max().unwrap_or(0);
- (w as f64, h as f64)
+ /// Bounding box of every head, used to clamp pointer motion --
+ /// `(min_x, min_y, max_x, max_y)`, not just a `(width, height)`
+ /// implicitly anchored at `(0, 0)` (what this used to return, and what
+ /// every call site clamped into with a hardcoded `0.0` floor). That
+ /// was only ever correct while every head's `location.x`/`location.y`
+ /// stayed `>= 0`, true for `reprobe_outputs`' own left-to-right hotplug
+ /// layout but not guaranteed once `set_output_position` exists: an
+ /// "extend left"/"extend above" arrangement (a real one, requested and
+ /// applied live by an AGS peer session's monitor-layout panel) places
+ /// the newly-added head at a *negative* `x`/`y` relative to whichever
+ /// one stayed at the origin. With the old `(0, w)` clamp, the pointer
+ /// could never actually cross into that negative-origin region at
+ /// all - reported live as "clicked it now I can't go to other
+ /// monitor at all" once such an arrangement was applied. The AGS
+ /// side has since started normalising every arrangement it sends so
+ /// the leftmost/topmost edge lands at `0` again, which works around
+ /// this from outside, but srdwm's own pointer clamp assuming an origin
+ /// no other part of this backend actually enforces is the real bug --
+ /// fixed here instead of just left for every future caller to avoid.
+ fn bounds(&self) -> (f64, f64, f64, f64) {
+ bounds_of(self.heads.iter().map(|h| (h.location.x, h.location.y, h.size.0, h.size.1)))
+ }
+}
+
+/// The actual arithmetic behind [`UdevState::bounds`], over plain
+/// `(x, y, width, height)` tuples rather than real `UdevHead`s - pulled
+/// out so it's testable without a real DRM/`Card` handle, which every
+/// `UdevHead` in this module otherwise needs to even construct.
+fn bounds_of(heads: impl Iterator<Item = (i32, i32, i32, i32)>) -> (f64, f64, f64, f64) {
+ let mut min_x = 0;
+ let mut min_y = 0;
+ let mut max_x = 0;
+ let mut max_y = 0;
+ let mut any = false;
+ for (x, y, w, h) in heads {
+ if !any {
+ min_x = x;
+ min_y = y;
+ any = true;
+ } else {
+ min_x = min_x.min(x);
+ min_y = min_y.min(y);
+ }
+ max_x = max_x.max(x + w);
+ max_y = max_y.max(y + h);
+ }
+ (min_x as f64, min_y as f64, max_x as f64, max_y as f64)
+}
+
+#[cfg(test)]
+mod bounds_tests {
+ use super::bounds_of;
+
+ #[test]
+ fn single_head_at_origin_matches_the_old_zero_anchored_behaviour() {
+ assert_eq!(bounds_of([(0, 0, 1920, 1080)].into_iter()), (0.0, 0.0, 1920.0, 1080.0));
+ }
+
+ #[test]
+ fn two_heads_left_to_right_from_origin() {
+ assert_eq!(bounds_of([(0, 0, 1920, 1080), (1920, 0, 1920, 1080)].into_iter()), (0.0, 0.0, 3840.0, 1080.0));
+ }
+
+ #[test]
+ fn negative_origin_head_is_reflected_in_min_not_clamped_to_zero() {
+ // The actual regression this exists for: an "extend left"
+ // arrangement places the new head at a negative x, and the old
+ // `(width, height)`-only version of this function (implicitly
+ // anchored at 0) made that head's own region completely
+ // unreachable by pointer motion - reported live as "clicked it
+ // now I can't go to other monitor at all".
+ let (min_x, min_y, max_x, max_y) = bounds_of([(0, 0, 1920, 1080), (-1920, 0, 1920, 1080)].into_iter());
+ assert_eq!((min_x, min_y, max_x, max_y), (-1920.0, 0.0, 1920.0, 1080.0));
+ }
+
+ #[test]
+ fn negative_origin_above_is_reflected_in_min_y() {
+ let (min_x, min_y, max_x, max_y) = bounds_of([(0, 0, 1920, 1080), (0, -1080, 1920, 1080)].into_iter());
+ assert_eq!((min_x, min_y, max_x, max_y), (0.0, -1080.0, 1920.0, 1080.0));
+ }
+
+ #[test]
+ fn no_heads_at_all_is_a_degenerate_zero_sized_box_not_a_panic() {
+ assert_eq!(bounds_of(std::iter::empty()), (0.0, 0.0, 0.0, 0.0));
}
}
@@ -204,8 +318,33 @@ impl UdevHead {
/// buffer (software rendering writes into its own owned image, not the
/// scanout memory directly, to avoid tying that image's lifetime to an
/// mmap - see this module's docs) and flips to it.
- fn copy_and_flip(&mut self, card: &Card, back: usize) -> std::io::Result<()> {
- let (src_stride, height) = (self.buffers[back].image.stride(), self.buffers[back].image.height());
+ /// `damage` is the exact set of rects `render_output` just re-rendered
+ /// into `self.buffers[back].image` - an empty slice means "copy
+ /// everything" (the locked/lock-UI render paths don't bother computing
+ /// per-rect damage, so this is also the safe fallback for any caller
+ /// that can't cheaply produce real rects), otherwise only those rows'
+ /// column ranges are copied.
+ ///
+ /// Used to be an unconditional full-buffer copy regardless of how
+ /// little of the frame actually changed - `render_output`'s own
+ /// age-based damage tracking already leaves everything outside
+ /// `damage` untouched in `image` (correct: that buffer's untouched
+ /// pixels still match what was on screen `ages[back]` frames ago), so
+ /// `dumb` - this same buffer's DRM-mapped twin, previously brought up
+ /// to date by this exact function on that same past frame - is
+ /// already correct everywhere outside `damage` too. Copying the whole
+ /// buffer anyway meant a full `stride * height` memcpy on every single
+ /// presented frame, for content as small as a moved cursor or a
+ /// blinking terminal caret - confirmed as the largest per-frame CPU
+ /// cost on this software `PixmanRenderer` backend by a direct
+ /// comparison against niri's DRM-composited present path (which has no
+ /// equivalent copy step at all) and mutter's native backend (which
+ /// explicitly restricts its own swap to damaged regions,
+ /// `swap_buffers_with_damage`) - this is the same technique, adapted
+ /// to a raw byte copy instead of a GL/EGL damage extension.
+ fn copy_and_flip(&mut self, card: &Card, back: usize, damage: &[Rectangle<i32, Physical>]) -> std::io::Result<()> {
+ let (src_stride, height, width) =
+ (self.buffers[back].image.stride(), self.buffers[back].image.height(), self.buffers[back].image.width());
let byte_len = src_stride * height;
// SAFETY: `image` owns this memory and outlives the byte slice we
// construct from it here; we only read, and only for the duration
@@ -224,23 +363,142 @@ impl UdevHead {
let dst_stride = self.buffers[back].dumb.pitch() as usize;
{
let mut mapping = card.map_dumb_buffer(&mut self.buffers[back].dumb)?;
- let dst = mapping.as_mut();
- let row_len = src_stride.min(dst_stride);
- for row in 0..height {
- let s = row * src_stride;
- let d = row * dst_stride;
- if s + row_len > src.len() || d + row_len > dst.len() {
- break;
- }
- dst[d..d + row_len].copy_from_slice(&src[s..s + row_len]);
- }
+ copy_damaged_rows(src, mapping.as_mut(), src_stride, dst_stride, width, height, damage);
}
card.page_flip(self.crtc, self.buffers[back].fb, PageFlipFlags::EVENT, None)?;
self.flip_pending = true;
+ self.flip_pending_since = Instant::now();
Ok(())
}
}
+/// The row/column copy math behind [`DrmHead::copy_and_flip`], pulled out
+/// as a free function over plain slices so it's testable without a real
+/// `Card`/dumb buffer - everything else in that method needs live DRM
+/// state, this doesn't. `damage` empty means "copy every row in full"
+/// (`width`/`height` are pixels, `src_stride`/`dst_stride` bytes); a
+/// non-empty `damage` copies only each rect's row/column span, clamped to
+/// the narrower of the two strides and to `width`/`height` the same way
+/// the full-copy path always has.
+fn copy_damaged_rows(src: &[u8], dst: &mut [u8], src_stride: usize, dst_stride: usize, width: usize, height: usize, damage: &[Rectangle<i32, Physical>]) {
+ let full_row_len = src_stride.min(dst_stride);
+ let copy_row = |dst: &mut [u8], row: usize, col_start_bytes: usize, col_len: usize| {
+ let s = row * src_stride + col_start_bytes;
+ let d = row * dst_stride + col_start_bytes;
+ let len = col_len.min(full_row_len.saturating_sub(col_start_bytes));
+ if len == 0 || s + len > src.len() || d + len > dst.len() {
+ return;
+ }
+ dst[d..d + len].copy_from_slice(&src[s..s + len]);
+ };
+ if damage.is_empty() {
+ for row in 0..height {
+ copy_row(dst, row, 0, full_row_len);
+ }
+ return;
+ }
+ const BPP: usize = 4; // Argb8888/Xrgb8888, same assumption every other raw-buffer path in this codebase makes.
+ for rect in damage {
+ let y0 = rect.loc.y.max(0) as usize;
+ let y1 = (rect.loc.y.saturating_add(rect.size.h).max(0) as usize).min(height);
+ let x0 = rect.loc.x.max(0) as usize;
+ let x1 = (rect.loc.x.saturating_add(rect.size.w).max(0) as usize).min(width);
+ if x1 <= x0 {
+ continue;
+ }
+ let (col_start_bytes, col_len) = (x0 * BPP, (x1 - x0) * BPP);
+ for row in y0..y1 {
+ copy_row(dst, row, col_start_bytes, col_len);
+ }
+ }
+}
+
+#[cfg(test)]
+mod copy_damaged_rows_tests {
+ use super::copy_damaged_rows;
+ use smithay::utils::{Physical, Point, Rectangle, Size};
+
+ fn rect(x: i32, y: i32, w: i32, h: i32) -> Rectangle<i32, Physical> {
+ Rectangle::new(Point::from((x, y)), Size::from((w, h)))
+ }
+
+ /// A tiny 4x3 BGRA canvas, one distinct byte value per pixel's blue
+ /// channel (row * width + col) so a wrong offset or a skipped pixel
+ /// shows up as the wrong number, not just "still zero".
+ fn make_src(width: usize, height: usize) -> Vec<u8> {
+ let mut buf = vec![0u8; width * height * 4];
+ for (i, px) in buf.chunks_exact_mut(4).enumerate() {
+ px[0] = i as u8;
+ px[3] = 255;
+ }
+ buf
+ }
+
+ #[test]
+ fn empty_damage_copies_every_row_in_full() {
+ let (w, h) = (4, 3);
+ let src = make_src(w, h);
+ let mut dst = vec![0u8; w * h * 4];
+ copy_damaged_rows(&src, &mut dst, w * 4, w * 4, w, h, &[]);
+ assert_eq!(dst, src);
+ }
+
+ #[test]
+ fn a_damage_rect_updates_only_its_own_pixels() {
+ let (w, h) = (4, 3);
+ let src = make_src(w, h);
+ let mut dst = vec![0u8; w * h * 4];
+ // Only the single pixel at (1, 1).
+ copy_damaged_rows(&src, &mut dst, w * 4, w * 4, w, h, &[rect(1, 1, 1, 1)]);
+ let idx = (1 * w + 1) * 4;
+ assert_eq!(dst[idx], src[idx], "the damaged pixel must be copied");
+ assert_eq!(dst[0], 0, "a pixel outside the damage rect must stay untouched");
+ assert_eq!(dst[dst.len() - 4], 0, "the last row's pixel is also outside the rect and must stay untouched");
+ }
+
+ #[test]
+ fn a_full_width_row_rect_copies_that_row_only() {
+ let (w, h) = (4, 3);
+ let src = make_src(w, h);
+ let mut dst = vec![0u8; w * h * 4];
+ copy_damaged_rows(&src, &mut dst, w * 4, w * 4, w, h, &[rect(0, 1, w as i32, 1)]);
+ let row1 = w * 4..w * 4 * 2;
+ assert_eq!(dst[row1.clone()], src[row1], "row 1 must be fully copied");
+ assert_eq!(&dst[..w * 4], &vec![0u8; w * 4][..], "row 0 must stay untouched");
+ assert_eq!(&dst[w * 4 * 2..], &vec![0u8; w * 4][..], "row 2 must stay untouched");
+ }
+
+ #[test]
+ fn a_rect_extending_past_the_buffer_is_clamped_not_panicking() {
+ let (w, h) = (4, 3);
+ let src = make_src(w, h);
+ let mut dst = vec![0u8; w * h * 4];
+ // Starts inside the buffer but both extends past its right/bottom
+ // edge and would run off a naive unclamped copy.
+ copy_damaged_rows(&src, &mut dst, w * 4, w * 4, w, h, &[rect(2, 2, 100, 100)]);
+ let idx = (2 * w + 2) * 4;
+ assert_eq!(dst[idx], src[idx], "the in-bounds corner of an oversized rect must still be copied");
+ }
+
+ #[test]
+ fn a_wider_destination_stride_does_not_shear_rows() {
+ // Destination row padded 4 extra bytes past the source's own
+ // stride - the same "driver-padded dumb buffer pitch" case the
+ // full-copy path was already written to handle; damage-restricted
+ // copying must preserve that, not just the empty-damage fallback.
+ let (w, h) = (4, 3);
+ let src = make_src(w, h);
+ let dst_stride = w * 4 + 4;
+ let mut dst = vec![0u8; dst_stride * h];
+ copy_damaged_rows(&src, &mut dst, w * 4, dst_stride, w, h, &[rect(0, 0, w as i32, h as i32)]);
+ for row in 0..h {
+ let s = row * w * 4..row * w * 4 + w * 4;
+ let d = row * dst_stride..row * dst_stride + w * 4;
+ assert_eq!(dst[d], src[s], "row {row} must land at the destination's own stride, not the source's");
+ }
+ }
+}
+
mod capture;
mod drm;
mod outputs;