git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commit6151bda5765a34053b6d0fa48fbc37018601f79f
parent7a48cfc3cc
authorLucas Galante <[email protected]>
date2026-09-06 18:06
perf(resize): configure and relayout the dragged window once per frame, not per pointer event

The seat op recomputed the dragged window's geometry on every pointer
event — cheap, and the arrange pass reads the op state — but it also
called the window's manage_finish (an xdg configure, immediately) and
scheduled a manage pass per event. A fast mouse handed the client several
sizes per frame, rendered and never shown, and the compositor ran an
arrange pass for each.

The op now queues one output frame (like finger-pan's pan_pending) and
WindowManager::step_op_frame, called from the frame handler after the
camera step, configures the op window for the pointer's latest position
and runs the manage pass synchronously — as the dirty-idle callback would
— so the same frame draws the result; with a pass in flight it sets the
dirty flag as before. Scheduling the frame explicitly matters with a
hardware cursor, where pointer motion alone damages nothing.

Measured headless at output scale 2 with a 4 ms burst of 40 motion events
on a left-edge drag: 20 configures with 20 distinct sizes before, 1 after,
identical geometry (anchored right edge at 453, width 896). A body drag
still lands the window exactly where the pointer delta says.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

 src/server/output.rs         |  3 +++
 src/server/seat.rs           |  6 ++++--
 src/server/window_manager.rs | 50 ++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 57 insertions(+), 2 deletions(-)

diff --git a/src/server/output.rs b/src/server/output.rs
index 3b90d41..1066430 100644
--- a/src/server/output.rs
+++ b/src/server/output.rs
@@ -1665,6 +1665,9 @@ unsafe extern "C" fn handle_frame(listener: *mut ffi::wl_listener, _data: *mut s
     // The camera steps here, on the vblank, so what this frame renders is
     // the position computed for it (see WindowManager::step_camera_frame).
     (*output.server).wm.step_camera_frame();
+    // Likewise the interactive move/resize: one configure + relayout per
+    // vblank, for the pointer's latest position.
+    (*output.server).wm.step_op_frame();
     let render_start = if frame_debug() {
         Some(std::time::Instant::now())
     } else {
diff --git a/src/server/seat.rs b/src/server/seat.rs
index abc84e2..ad7aeea 100644
--- a/src/server/seat.rs
+++ b/src/server/seat.rs
@@ -1449,9 +1449,11 @@ impl Seat {
                         (*win).set_dimensions(new_w, new_h);
                     }
                 }
-                (*win).manage_finish();
             }
-            (*self.server).wm.dirty_windowing();
+            // The configure and the relayout go out once per output frame,
+            // for wherever the pointer is by then (WindowManager::
+            // step_op_frame), not once per motion event.
+            (*self.server).wm.queue_op_frame();
         }
         self.update_edge_pan(x as f64, y as f64);
     }
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index a52a330..f7d7455 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -187,6 +187,14 @@ pub struct WindowManager {
     /// applied once, at the output frame, so the on-screen step lands on
     /// the vblank instead of whenever the last event happened to arrive.
     pub pan_pending: [f64; 2],
+    /// An interactive move/resize has pointer motion the client has not
+    /// been configured for yet. The seat op recomputes the dragged window's
+    /// geometry on every pointer event (cheap, and the arrange pass reads
+    /// the op state), but it used to also send the client a configure and
+    /// run a manage pass per event — a fast mouse handed the client several
+    /// sizes per frame, most rendered and never shown. Now it queues one
+    /// frame and `step_op_frame` does both once, on the vblank.
+    pub op_frame_pending: bool,
     pub animation_timer: *mut ffi::wl_event_source,
     /// Edge auto-pan velocity during an interactive move/resize, in SCREEN
     /// px/s (the tick divides by zoom). Written by `Seat::update_edge_pan`
@@ -376,6 +384,7 @@ impl WindowManager {
         self.pan_finger_v = [0.0, 0.0];
         self.camera_anim_active = false;
         self.pan_pending = [0.0, 0.0];
+        self.op_frame_pending = false;
         self.animation_timer = std::ptr::null_mut();
         self.edge_pan_vx = 0.0;
         self.edge_pan_vy = 0.0;
@@ -1515,6 +1524,47 @@ impl WindowManager {
         self.schedule_frame_all_outputs();
     }
 
+    /// Queue the interactive move/resize's configure and relayout for the
+    /// next output frame (see `op_frame_pending`).
+    pub unsafe fn queue_op_frame(&mut self) {
+        if !self.op_frame_pending {
+            self.op_frame_pending = true;
+            self.schedule_frame_all_outputs();
+        }
+    }
+
+    /// The seat-op step for the frame about to render: configure the
+    /// dragged window for the LATEST pointer position and run the manage
+    /// pass, once per vblank — what `Seat::op_update` did per event. The
+    /// pass runs synchronously, as the dirty-idle callback would run it, so
+    /// this frame draws the result; with a pass already in flight the dirty
+    /// flag queues it, as before.
+    pub unsafe fn step_op_frame(&mut self) {
+        if !self.op_frame_pending {
+            return;
+        }
+        self.op_frame_pending = false;
+        let seats_list = &mut (*self.server).input_manager.seats as *mut ffi::wl_list as *mut WlList;
+        let mut curr = (*seats_list).next;
+        while curr != seats_list {
+            let seat = crate::container_of!(curr, crate::seat::Seat, link);
+            if let Some(ref op) = (*seat).op {
+                let win = op.window_ptr;
+                if !win.is_null() && !(*win).closed {
+                    (*win).manage_finish();
+                }
+            }
+            curr = (*curr).next;
+        }
+        if matches!(self.state, WindowManagerState::Idle) {
+            self.scheduled.dirty = true;
+            self.scheduled.dirty_lazy = false;
+            self.manage_start();
+        } else {
+            self.dirty_windowing();
+        }
+    }
+
     /// The camera step for the frame about to render: apply queued finger
     /// motion, advance any live animation by the real elapsed time, and
     /// relayout if the camera moved. Called from the output frame handler