diff options
Diffstat (limited to 'crates/wayland')
| -rw-r--r-- | crates/wayland/src/input/pointer.rs | 31 | ||||
| -rw-r--r-- | crates/wayland/src/state/geometry.rs | 43 | ||||
| -rw-r--r-- | crates/wayland/src/state/mod.rs | 14 | ||||
| -rw-r--r-- | crates/wayland/src/udev/platform.rs | 1 | ||||
| -rw-r--r-- | crates/wayland/src/udev/render.rs | 38 | ||||
| -rw-r--r-- | crates/wayland/src/winit/connect.rs | 1 | ||||
| -rw-r--r-- | crates/wayland/src/winit/render.rs | 18 |
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}"), |