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

commit82bd67f9eec1a4236cc7bcceaea3ac9d04573114
parent5bac38ca02
authorLucas Galante <[email protected]>
date2026-09-06 17:32
fix(resize): one rounded virtual-to-screen conversion for every writer of a window's origin

Interactive resizes twitched: the anchored edge stepped a pixel back and
forth on every commit, and the whole window hopped a pixel on grab and on
release. Three writers of a window's screen origin truncated the
virtual-to-screen conversion — the seat op's Move and Resize arms and the
resize-commit anchoring — while the arrange pass rounds (cce-window-manager
6b83b0f). Whenever the fractional part was .5 or more, a commit placed the
tree at the truncated position and the next arrange moved it to the
rounded one; at overview zoom, where a virtual pixel is a third of a screen
pixel, that was nearly every step.

Window::virtual_to_screen is the single rounded conversion now, and the
three sites go through it (the two seat arms also drop their copies of the
first-enabled-output scan).

Measured headless at output scale 2, sampling after every 1px pointer step
of a left-edge drag: the anchored right edge stays at 453 from rest through
the drag and after release; before, it sat at 452 while dragging, 453 at
rest, and the origin flipped 154/155 at the same virtual position.

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

 src/server/seat.rs   | 36 ++----------------------------------
 src/server/window.rs | 24 ++++++++++++++++++------
 2 files changed, 20 insertions(+), 40 deletions(-)

diff --git a/src/server/seat.rs b/src/server/seat.rs
index c45b004..abc84e2 100644
--- a/src/server/seat.rs
+++ b/src/server/seat.rs
@@ -1376,23 +1376,7 @@ impl Seat {
                                 );
                             }
 
-                            let mut out_x = 0;
-                            let mut out_y = 0;
-                            let outputs_list = &mut (*(*self.server).wm.server).om.outputs as *mut ffi::wl_list as *mut WlList;
-                            let mut curr_out = (*outputs_list).next;
-                            while curr_out != outputs_list {
-                                let output = crate::container_of!(curr_out, crate::output::Output, link);
-                                if (*output).sent.state == crate::output::OutputStateValue::Enabled {
-                                    let wlr_box = (*output).sent.box_layout();
-                                    out_x = wlr_box.x;
-                                    out_y = wlr_box.y;
-                                    break;
-                                }
-                                curr_out = (*curr_out).next;
-                            }
-
-                            let final_x = out_x + ((vx - pan_x) * scale) as i32;
-                            let final_y = out_y + ((vy - pan_y) * scale) as i32;
+                            let (final_x, final_y) = (*win).virtual_to_screen(vx, vy);
                             (*win).rendering_requested.x = final_x;
                             (*win).rendering_requested.y = final_y;
                             (*win).box_geom.x = final_x;
@@ -1446,23 +1430,7 @@ impl Seat {
                         (*win).virtual_x = vx;
                         (*win).virtual_y = vy;
 
-                        let mut out_x = 0;
-                        let mut out_y = 0;
-                        let outputs_list = &mut (*(*self.server).wm.server).om.outputs as *mut ffi::wl_list as *mut WlList;
-                        let mut curr_out = (*outputs_list).next;
-                        while curr_out != outputs_list {
-                            let output = crate::container_of!(curr_out, crate::output::Output, link);
-                            if (*output).sent.state == crate::output::OutputStateValue::Enabled {
-                                let wlr_box = (*output).sent.box_layout();
-                                out_x = wlr_box.x;
-                                out_y = wlr_box.y;
-                                break;
-                            }
-                            curr_out = (*curr_out).next;
-                        }
-
-                        let final_x = out_x + ((vx - pan_x) * scale) as i32;
-                        let final_y = out_y + ((vy - pan_y) * scale) as i32;
+                        let (final_x, final_y) = (*win).virtual_to_screen(vx, vy);
 
                         (*win).rendering_requested.x = final_x;
                         (*win).rendering_requested.y = final_y;
diff --git a/src/server/window.rs b/src/server/window.rs
index 0cd98ab..4605c3f 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -1164,6 +1164,23 @@ impl Window {
         (0.0, 0.0, 1920.0, 1080.0)
     }
 
+    /// Virtual position to layout (screen) position, ROUNDED — the same
+    /// conversion the arrange pass makes (`PlacementCtx::virtual_to_screen`).
+    /// Every writer of a window's screen origin has to agree on the
+    /// rounding: the seat op and the resize-commit anchoring truncated while
+    /// the arrange pass rounds, so whenever the fractional part was .5 or
+    /// more the window stepped a pixel back and forth between a commit and
+    /// the next arrange — a twitch on every resize step at overview zoom,
+    /// and a one-pixel hop on grab and release.
+    pub unsafe fn virtual_to_screen(&self, vx: f64, vy: f64) -> (i32, i32) {
+        let wm = &(*self.server).wm;
+        let (out_x, out_y, _, _) = self.first_enabled_output_box();
+        (
+            out_x as i32 + ((vx - wm.desk_pan_x) * wm.desk_zoom).round() as i32,
+            out_y as i32 + ((vy - wm.desk_pan_y) * wm.desk_zoom).round() as i32,
+        )
+    }
+
     /// Best-known window size in VIRTUAL units at map time. `box_geom` is the
     /// render pass's size and is only filled in once a frame has been drawn
     /// (or by `try_restore` from the saved geometry), so a first-ever launch
@@ -1966,12 +1983,7 @@ impl Window {
             self.virtual_y = self.resize_start_vy + (self.resize_start_h as f64 - committed_h as f64);
         }
 
-        let zoom = (*self.server).wm.desk_zoom;
-        let pan_x = (*self.server).wm.desk_pan_x;
-        let pan_y = (*self.server).wm.desk_pan_y;
-        let (out_x, out_y, _, _) = self.first_enabled_output_box();
-        let final_x = out_x as i32 + ((self.virtual_x - pan_x) * zoom) as i32;
-        let final_y = out_y as i32 + ((self.virtual_y - pan_y) * zoom) as i32;
+        let (final_x, final_y) = self.virtual_to_screen(self.virtual_x, self.virtual_y);
         self.rendering_requested.x = final_x;
         self.rendering_requested.y = final_y;
         self.box_geom.x = final_x;