srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2025-08-09 00:57:00 +0200
committersrdusr <[email protected]>2025-08-09 00:57:00 +0200
commit61d01bb79c6882c950f45a1176ba1f30fbbd4824 (patch)
treeeb2a24c7f301f74f987f954b62d768c9cb789fef /crates/wayland/src
parent9e811d2f2f1037ba4ec08ab0e05b350e6cf69e66 (diff)
downloadsrdwm-61d01bb79c6882c950f45a1176ba1f30fbbd4824.tar.gz
srdwm-61d01bb79c6882c950f45a1176ba1f30fbbd4824.zip
Fix interactive-resize border/shadow lag without the OOB risk that sank the first attempt
effective_frame_of now returns the live drag target while a window is being interactively resized (same change as the reverted first attempt), but two things make it safe this time instead of reintroducing the out-of-bounds texture sample that reversion was for: - Every src crop rect built from a window's frame width in udev/render.rs and winit/render.rs (titlebar, top border strip, bottom border strip) is now clamped against DecorationSignature's own recorded width/ border_width - the bitmap's actual last-built size - before reaching MemoryRenderBufferRenderElement::from_buffer, which does not itself validate src against the real texture size. This is a structural floor independent of timing, not a repeat of the previous unsafe approach. - handle_pointer_position now calls redraw_decoration_buffer once per resize motion event (throttled to 60Hz via a new CompState::resize_redraw_at), closing the lag at its source instead of only catching up on the next real client commit. This also fixes the shadow bitmap's identical commit-vs-live-position gap for free, since redraw_decoration_buffer rebuilds all three bitmaps together. Updates the TODO.md entry for this bug with the full before/after.
Diffstat (limited to 'crates/wayland/src')
-rw-r--r--crates/wayland/src/input/pointer.rs31
-rw-r--r--crates/wayland/src/state/geometry.rs43
-rw-r--r--crates/wayland/src/state/mod.rs14
-rw-r--r--crates/wayland/src/udev/platform.rs1
-rw-r--r--crates/wayland/src/udev/render.rs38
-rw-r--r--crates/wayland/src/winit/connect.rs1
-rw-r--r--crates/wayland/src/winit/render.rs18
7 files changed, 128 insertions, 18 deletions
diff --git a/crates/wayland/src/input/pointer.rs b/crates/wayland/src/input/pointer.rs
index 7b3bbce..d4c5678 100644
--- a/crates/wayland/src/input/pointer.rs
+++ b/crates/wayland/src/input/pointer.rs
@@ -16,6 +16,13 @@ use super::keyboard::core_modifiers_from_xkb;
use super::layers::{background_layer_surface_under, layer_surface_under};
use super::{notify_idle_activity, DRAG_MODIFIER};
+/// Minimum gap between `redraw_decoration_buffer` calls fired from an
+/// active resize drag's own pointer-motion events - see `CompState::
+/// resize_redraw_at`'s doc comment. 60Hz: fast enough that the border
+/// visibly tracks the drag, far below the per-motion-event rate a real
+/// mouse or touchpad can produce.
+pub(crate) const RESIZE_REDRAW_INTERVAL: std::time::Duration = std::time::Duration::from_millis(1000 / 60);
+
/// `WindowManager::hit_test`, but substituting each window's currently
/// *animated* rect (if it has one active in `state.window_anims`) for its
/// final `geometry` - see `hit_test_with`'s own doc comment in
@@ -300,6 +307,11 @@ pub(crate) fn handle_pointer_position(state: &mut CompState, pos: Point<f64, Log
let mut wm = state.wm.borrow_mut();
let dragging_or_resizing = wm.is_dragging() || wm.is_resizing();
+ // Captured now, while `wm` is already borrowed, and acted on further
+ // down after `drop(wm)` - `redraw_decoration_buffer` needs `&mut
+ // state` as a whole, which can't happen while `state.wm`'s own
+ // `RefMut` is still alive.
+ let resizing_id = wm.resizing_window();
if wm.is_dragging() {
wm.update_drag(pos.x as i32, pos.y as i32);
} else if wm.is_resizing() {
@@ -338,6 +350,25 @@ pub(crate) fn handle_pointer_position(state: &mut CompState, pos: Point<f64, Log
state.sync_geometry(id);
}
}
+ // Keeps the border/titlebar bitmap tracking an active resize drag's own
+ // live geometry (see `state::geometry::effective_frame_of`'s doc
+ // comment) instead of only catching up once the drag ends - throttled
+ // against `RESIZE_REDRAW_INTERVAL` since motion events can arrive far
+ // faster than a redraw is worth paying for. Reset to `None` on every
+ // tick that isn't resizing this exact window, so a later resize's first
+ // motion event always redraws immediately rather than inheriting a
+ // stale timestamp from a previous drag (or from dragging, which shares
+ // `dragging_or_resizing` above but never touches `resizing_id`).
+ if let Some(id) = resizing_id {
+ let now = std::time::Instant::now();
+ let due = state.resize_redraw_at.is_none_or(|t| now.duration_since(t) >= RESIZE_REDRAW_INTERVAL);
+ if due {
+ state.redraw_decoration_buffer(id);
+ state.resize_redraw_at = Some(now);
+ }
+ } else {
+ state.resize_redraw_at = None;
+ }
}
/// Sets the pointer to a resize-direction shape while hovering (or
diff --git a/crates/wayland/src/state/geometry.rs b/crates/wayland/src/state/geometry.rs
index b537412..26c23fe 100644
--- a/crates/wayland/src/state/geometry.rs
+++ b/crates/wayland/src/state/geometry.rs
@@ -46,22 +46,41 @@ impl CompState {
// in a more important way - every caller of this function that
// reads a *bitmap*-backed element (the titlebar, the top/bottom
// border strip's own rounded-corner bitmap, both built by `redraw_
- // decoration_buffer`, itself only called on a real client *commit*,
- // not on every resize step) uses this rect's width/height to size
- // the `src` crop rectangle it samples that bitmap with. Making this
+ // decoration_buffer`) uses this rect's width/height to size the
+ // `src` crop rectangle it samples that bitmap with. Making this
// function return the *live* drag target while the underlying
// bitmap was still sized for whatever the *last commit* actually
- // was means that crop can end up larger than the real bitmap's own
- // stored dimensions - `MemoryRenderBufferRenderElement::from_
+ // was meant that crop could end up larger than the real bitmap's
+ // own stored dimensions - `MemoryRenderBufferRenderElement::from_
// buffer` does not validate `src` against the texture's real size,
// so an oversized crop reads as an out-of-bounds texture sample
- // (stretched/repeated/garbage pixels, not a clean error) for as
- // long as a fast resize keeps outrunning the client's own recommit
- // rate - a worse, more visibly broken failure mode than the
- // one-frame-stale lag it replaced. Fixing the lag properly needs
- // `redraw_decoration_buffer` itself rebuilding on every resize
- // step, not just on commit, which is real, separate scope - not
- // yet done.
+ // (stretched/repeated/garbage pixels, not a clean error).
+ //
+ // Now reinstated, safely: this returns the *live* drag target
+ // (`geom`, unmodified) while a resize of this specific window is
+ // active, same as the reverted attempt did - but two things are
+ // different this time, together closing the gap that made it
+ // unsafe rather than just re-taking the risk:
+ // 1. `input::pointer::handle_pointer_position` now calls
+ // `redraw_decoration_buffer` on every resize motion tick (see
+ // its own doc comment), not just on a real client commit, so
+ // the bitmap itself keeps catching up to this same live value
+ // almost every frame instead of staying pinned to the last
+ // commit for the resize's whole duration.
+ // 2. Independent of how well-synced that keeps the two, every
+ // render-loop call site that turns this rect's width/height
+ // into a `src` crop now clamps it against `decoration_
+ // signatures`' own recorded `(width, height)` - the bitmap's
+ // own *actual* last-built size, tracked there already for
+ // unrelated caching reasons - before handing it to `from_
+ // buffer`. That clamp is what actually prevents the out-of-
+ // bounds read now, structurally, regardless of any remaining
+ // timing gap between this function and the next rebuild; this
+ // branch existing is what keeps that gap small in practice
+ // rather than a full commit-cycle wide.
+ if wm.borrow().resizing_window() == Some(id) {
+ return geom;
+ }
let Some(w) = wm.borrow().window(id).cloned() else { return geom };
let Some(dwindow) = id_to_window.get(&id) else { return geom };
// `dwindow.geometry()` - `xdg_surface::set_window_geometry` - is,
diff --git a/crates/wayland/src/state/mod.rs b/crates/wayland/src/state/mod.rs
index a3e9b1a..0ef4346 100644
--- a/crates/wayland/src/state/mod.rs
+++ b/crates/wayland/src/state/mod.rs
@@ -417,6 +417,20 @@ pub(crate) struct CompState {
/// own blanket call, which never actually checked whether this
/// specific window was one of the windows that triggered the tick.
pub(crate) decoration_signatures: HashMap<WindowId, DecorationSignature>,
+ /// When `handle_pointer_position` last called `redraw_decoration_buffer`
+ /// for the window currently being interactively resized - throttles
+ /// that call to once per `RESIZE_REDRAW_INTERVAL` (see `input::pointer`),
+ /// since a pointer
+ /// can emit motion events far faster than a titlebar's text and border
+ /// bitmaps are worth re-rasterizing. Without this the decoration buffer
+ /// only catches up with `effective_frame_of`'s now-live resize geometry
+ /// (see that function's own doc comment) once the drag ends and the
+ /// blanket `sync()` redraw runs - correct, but visibly laggy borders
+ /// for the whole drag. `None` whenever no resize is in progress; reset
+ /// there rather than left stale, so a *new* resize's first motion event
+ /// always redraws immediately instead of inheriting a stale timestamp
+ /// from a previous drag.
+ pub(crate) resize_redraw_at: Option<std::time::Instant>,
/// Which titlebar button (if any) the pointer is currently over, on
/// which window, and *when that hover started* - set from `handle_
/// pointer_position`'s own `hit_test` result, read by `redraw_
diff --git a/crates/wayland/src/udev/platform.rs b/crates/wayland/src/udev/platform.rs
index 691c8de..a4a72a0 100644
--- a/crates/wayland/src/udev/platform.rs
+++ b/crates/wayland/src/udev/platform.rs
@@ -240,6 +240,7 @@ impl UdevPlatform {
border_top_decorations: HashMap::new(),
border_bottom_decorations: HashMap::new(),
decoration_signatures: HashMap::new(),
+ resize_redraw_at: None,
hovered_titlebar_button: None,
shadow_buffers: HashMap::new(),
rounded_corners_program: None,
diff --git a/crates/wayland/src/udev/render.rs b/crates/wayland/src/udev/render.rs
index 0250c8a..802428f 100644
--- a/crates/wayland/src/udev/render.rs
+++ b/crates/wayland/src/udev/render.rs
@@ -527,7 +527,22 @@ impl CompState {
// mask feature.
let (row0, rows, shift) = decoration::border_top_visible_rows(border_curve_is_safe, w.border_width, w.corner_radius);
let pos = ((strips[0].x - origin.x) as f64, (strips[0].y - origin.y + shift as i32) as f64);
- let src = Some(Rectangle::new(Point::from((0.0, row0 as f64)), Size::from((strips[0].width as f64, rows as f64))));
+ // Clamped to the buffer's own actual last-
+ // built width, not trusted at face value --
+ // see `effective_frame_of`'s own doc comment
+ // on why: during an active resize `frame`
+ // (and so `strips[0]`) tracks the *live*
+ // drag target, which can outrun however far
+ // `redraw_decoration_buffer` has actually
+ // gotten rebuilding this same buffer to
+ // match. An uncrapped `src` wider than the
+ // real texture is an out-of-bounds sample,
+ // not a clean error - this is what actually
+ // prevents that, not just the rebuild-on-
+ // every-tick call in `input::pointer` trying
+ // to keep the gap small.
+ let crop_w = self.decoration_signatures.get(&id).map(|s| s.width + 2 * s.border_width).unwrap_or(strips[0].width).min(strips[0].width);
+ let src = Some(Rectangle::new(Point::from((0.0, row0 as f64)), Size::from((crop_w as f64, rows as f64))));
match MemoryRenderBufferRenderElement::from_buffer(&mut udev.renderer, pos, buffer, None, src, None, Kind::Unspecified) {
Ok(elem) => custom_elements.push(crate::elements::OverlayElement::Memory(elem)),
Err(e) => log::warn!("udev: failed to import top border buffer: {e}"),
@@ -548,7 +563,19 @@ impl CompState {
// visible fragment can come from the matching
// sub-rect of the source image rather than the
// whole thing.
- let titlebar_rect = srdwm_core::Rect::new(frame.x, frame.y, frame.width, srdwm_core::TITLEBAR_HEIGHT);
+ // Width clamped to the buffer's own actual last-
+ // built width - see `effective_frame_of`'s own doc
+ // comment and the matching clamp on the top border
+ // strip just above for why: during an active resize
+ // `frame.width` tracks the *live* drag target, which
+ // can outrun however far `redraw_decoration_buffer`
+ // has actually gotten. Clamped here, before
+ // `visible_border_fragments` runs, so every fragment
+ // it produces is already within the real texture --
+ // an unclamped one could ask for a crop past it,
+ // an out-of-bounds sample rather than a clean error.
+ let titlebar_w = self.decoration_signatures.get(&id).map(|s| s.width).unwrap_or(frame.width).min(frame.width);
+ let titlebar_rect = srdwm_core::Rect::new(frame.x, frame.y, titlebar_w, srdwm_core::TITLEBAR_HEIGHT);
for fragment in crate::elements::visible_border_fragments(titlebar_rect, &occluders) {
let pos = ((fragment.x - origin.x) as f64, (fragment.y - origin.y) as f64);
let src = Rectangle::new(
@@ -597,7 +624,12 @@ impl CompState {
// real screenshot, not assumed.
let (row0, rows, shift) = decoration::border_bottom_visible_rows(border_curve_is_safe, w.border_width, w.corner_radius);
let pos = ((strips[1].x - origin.x) as f64, (strips[1].y - origin.y - shift as i32) as f64);
- let src = Some(Rectangle::new(Point::from((0.0, row0 as f64)), Size::from((strips[1].width as f64, rows as f64))));
+ // Same live-resize safety clamp as the top
+ // strip above: bound against the buffer's
+ // own actual last-built width, not the
+ // possibly-ahead-of-it live `strips[1].width`.
+ let crop_w = self.decoration_signatures.get(&id).map(|s| s.width + 2 * s.border_width).unwrap_or(strips[1].width).min(strips[1].width);
+ let src = Some(Rectangle::new(Point::from((0.0, row0 as f64)), Size::from((crop_w as f64, rows as f64))));
match MemoryRenderBufferRenderElement::from_buffer(&mut udev.renderer, pos, buffer, None, src, None, Kind::Unspecified) {
Ok(elem) => custom_elements.push(crate::elements::OverlayElement::Memory(elem)),
Err(e) => log::warn!("udev: failed to import bottom border buffer: {e}"),
diff --git a/crates/wayland/src/winit/connect.rs b/crates/wayland/src/winit/connect.rs
index c748ef4..30eaaf8 100644
--- a/crates/wayland/src/winit/connect.rs
+++ b/crates/wayland/src/winit/connect.rs
@@ -161,6 +161,7 @@ impl WaylandPlatform {
border_top_decorations: HashMap::new(),
border_bottom_decorations: HashMap::new(),
decoration_signatures: HashMap::new(),
+ resize_redraw_at: None,
hovered_titlebar_button: None,
shadow_buffers: HashMap::new(),
rounded_corners_program,
diff --git a/crates/wayland/src/winit/render.rs b/crates/wayland/src/winit/render.rs
index 6f5f600..345d40c 100644
--- a/crates/wayland/src/winit/render.rs
+++ b/crates/wayland/src/winit/render.rs
@@ -238,7 +238,13 @@ impl WaylandPlatform {
// only once *fully* covered) wasn't enough: a titlebar
// only partially covered, the common case for cascaded
// windows, still bled through the covered part.
- let titlebar_rect = srdwm_core::Rect::new(frame.x, frame.y, frame.width, srdwm_core::TITLEBAR_HEIGHT);
+ // Live-resize safety clamp - see the matching clamp in
+ // `udev/render.rs`'s own titlebar push for the full
+ // reasoning: `frame.width` can be the live drag target
+ // (via `effective_frame_of`), ahead of whatever size this
+ // buffer was actually last built at.
+ let titlebar_w = self.state.decoration_signatures.get(&id).map(|s| s.width).unwrap_or(frame.width).min(frame.width);
+ let titlebar_rect = srdwm_core::Rect::new(frame.x, frame.y, titlebar_w, srdwm_core::TITLEBAR_HEIGHT);
for fragment in crate::elements::visible_border_fragments(titlebar_rect, &occluders) {
let pos = (fragment.x as f64, fragment.y as f64);
let src = Rectangle::new(
@@ -285,7 +291,10 @@ impl WaylandPlatform {
// away instead of landing on real content.
let (row0, rows, shift) = decoration::border_top_visible_rows(border_curve_is_safe, w.border_width, w.corner_radius);
let pos = (strips[0].x as f64, (strips[0].y + shift as i32) as f64);
- let src = Some(Rectangle::new(Point::from((0.0, row0 as f64)), Size::from((strips[0].width as f64, rows as f64))));
+ // Same live-resize safety clamp as the titlebar push
+ // above and `udev/render.rs`'s matching top strip.
+ let crop_w = self.state.decoration_signatures.get(&id).map(|s| s.width + 2 * s.border_width).unwrap_or(strips[0].width).min(strips[0].width);
+ let src = Some(Rectangle::new(Point::from((0.0, row0 as f64)), Size::from((crop_w as f64, rows as f64))));
match MemoryRenderBufferRenderElement::from_buffer(renderer, pos, buffer, None, src, None, Kind::Unspecified) {
Ok(elem) => custom_elements.push(crate::rounded_corners::WinitElement::Base(crate::elements::OverlayElement::Memory(elem))),
Err(e) => log::warn!("failed to import top border buffer for window {id}: {e}"),
@@ -301,7 +310,10 @@ impl WaylandPlatform {
// own doc comment.
let (row0, rows, shift) = decoration::border_bottom_visible_rows(border_curve_is_safe, w.border_width, w.corner_radius);
let pos = (strips[1].x as f64, (strips[1].y - shift as i32) as f64);
- let src = Some(Rectangle::new(Point::from((0.0, row0 as f64)), Size::from((strips[1].width as f64, rows as f64))));
+ // Same live-resize safety clamp as the top strip
+ // above and `udev/render.rs`'s matching bottom strip.
+ let crop_w = self.state.decoration_signatures.get(&id).map(|s| s.width + 2 * s.border_width).unwrap_or(strips[1].width).min(strips[1].width);
+ let src = Some(Rectangle::new(Point::from((0.0, row0 as f64)), Size::from((crop_w as f64, rows as f64))));
match MemoryRenderBufferRenderElement::from_buffer(renderer, pos, buffer, None, src, None, Kind::Unspecified) {
Ok(elem) => custom_elements.push(crate::rounded_corners::WinitElement::Base(crate::elements::OverlayElement::Memory(elem))),
Err(e) => log::warn!("failed to import bottom border buffer for window {id}: {e}"),