Window applies a new preferred scale only at the next resize: pointer, touch and caret positions are off until then #165

Open
opened 2026-10-31 07:26:25 +00:00 by jorijnvdgraaf · 1 comment

Summary

Window::xdg_surface_handle_preferred_scale stores the compositor's new fractional scale in Window::scale and does nothing else. The swapchain size and the viewport destination are recomputed only when something resizes the window, so after a scale change the window keeps rendering at the old scale while every conversion between surface-local and physical pixels already uses the new one. Pointer and touch positions and the text-input caret rectangle are off by new / old until the window is resized. Line numbers refer to bcdc885.

Details

The window keeps width x height physical pixels in its swapchain and shows them through the viewport at ceil(width / scale) x ceil(height / scale) surface-local units (implementations/Crafter.Graphics-Window.cpp:378 in the constructor, :733-737 in Resize). Input arrives in surface-local units and is multiplied by scale: pointer motion and enter at implementations/Crafter.Graphics-Device.cpp:497 and :510, touch down and motion at :690 and :729. ApplyTextInput divides the caret rectangle by it (:450-453). All of that holds only while scale is the ratio the current swapchain and viewport were made with.

  • Window.cpp:1784-1787: xdg_surface_handle_preferred_scale sets scale and leaves width, height and the viewport as they were.
  • :1770-1776: a later xdg_surface.configure restores the ratio only when its toplevel configure carries a size, which becomes Resize(ceil(logical * scale)). A (0, 0) configure keeps the current size. Neither protocol promises a configure on a scale change, since the logical size does not change.

Concrete case

KWin 6.8.0 on the development desktop, with outputs at 1.75 (DP-1, DP-2) and 1.0 (DP-7). WAYLAND_DEBUG=client on a test window shows:

  • preferred_scale(210) arrives right after get_fractional_scale, before the first configure, so the constructor's viewport already uses 1.75 (set_destination(183, 138) for 320 x 240). Start-up is fine on KWin.
  • The configures KWin sent the floating window after that were all xdg_toplevel.configure(0, 0, ...), including one carrying a state. So on KWin a configure restores the ratio only when something resizes the window (an interactive resize, maximizing).

By the code, a 1280 x 720 window moved from a 1.75 output (viewport 732 x 412) to the 1.0 one gets scale = 1.0 and keeps its 1280 x 720 buffer at 732 x 412. A pointer at the middle of the window, surface-local (366, 206), is reported at (366, 206) instead of (640, 360), so hit tests against the UI miss. The opposite move reports positions 1.75 times too far out and shows the old buffer upscaled.

Found by reading the code. Not reproduced by moving a window between outputs; the event order above is from the trace. A compositor that sends the first preferred scale only once the surface has entered an output (after its first buffer) would show this at start-up too.

Suggested fix

  • On a preferred scale that differs from the current one, keep the window's logical size and remake the swapchain at ceil(logical * newScale) as Resize does (viewport destination at the logical size, onResize), then commit. Keep the logical size as state, since it is what the viewport destination says, instead of re-deriving it from width / scale, so the configure path and the scale path compute from the same number and rounding can't drift.
  • Before the swapchain exists (between the constructor's first configure and CreateSwapchain), only store the scale, as now.
  • A test that drives xdg_surface_handle_preferred_scale on a live window, as PointerEnterLeave drives the pointer callbacks, and checks that width follows and that a motion at the surface's middle lands at the buffer's middle.

Related: #162. Its fix (in progress) makes the fractional scale only alongside a viewport; without one scale stays 1 and this does not arise.

## Summary `Window::xdg_surface_handle_preferred_scale` stores the compositor's new fractional scale in `Window::scale` and does nothing else. The swapchain size and the viewport destination are recomputed only when something resizes the window, so after a scale change the window keeps rendering at the old scale while every conversion between surface-local and physical pixels already uses the new one. Pointer and touch positions and the text-input caret rectangle are off by `new / old` until the window is resized. Line numbers refer to `bcdc885`. ## Details The window keeps `width x height` physical pixels in its swapchain and shows them through the viewport at `ceil(width / scale) x ceil(height / scale)` surface-local units (`implementations/Crafter.Graphics-Window.cpp:378` in the constructor, `:733-737` in `Resize`). Input arrives in surface-local units and is multiplied by `scale`: pointer motion and enter at `implementations/Crafter.Graphics-Device.cpp:497` and `:510`, touch down and motion at `:690` and `:729`. `ApplyTextInput` divides the caret rectangle by it (`:450-453`). All of that holds only while `scale` is the ratio the current swapchain and viewport were made with. - `Window.cpp:1784-1787`: `xdg_surface_handle_preferred_scale` sets `scale` and leaves `width`, `height` and the viewport as they were. - `:1770-1776`: a later `xdg_surface.configure` restores the ratio only when its toplevel configure carries a size, which becomes `Resize(ceil(logical * scale))`. A `(0, 0)` configure keeps the current size. Neither protocol promises a configure on a scale change, since the logical size does not change. ## Concrete case KWin 6.8.0 on the development desktop, with outputs at 1.75 (DP-1, DP-2) and 1.0 (DP-7). `WAYLAND_DEBUG=client` on a test window shows: - `preferred_scale(210)` arrives right after `get_fractional_scale`, before the first configure, so the constructor's viewport already uses 1.75 (`set_destination(183, 138)` for 320 x 240). Start-up is fine on KWin. - The configures KWin sent the floating window after that were all `xdg_toplevel.configure(0, 0, ...)`, including one carrying a state. So on KWin a configure restores the ratio only when something resizes the window (an interactive resize, maximizing). By the code, a 1280 x 720 window moved from a 1.75 output (viewport 732 x 412) to the 1.0 one gets `scale = 1.0` and keeps its 1280 x 720 buffer at 732 x 412. A pointer at the middle of the window, surface-local (366, 206), is reported at (366, 206) instead of (640, 360), so hit tests against the UI miss. The opposite move reports positions 1.75 times too far out and shows the old buffer upscaled. Found by reading the code. Not reproduced by moving a window between outputs; the event order above is from the trace. A compositor that sends the first preferred scale only once the surface has entered an output (after its first buffer) would show this at start-up too. ## Suggested fix - On a preferred scale that differs from the current one, keep the window's logical size and remake the swapchain at `ceil(logical * newScale)` as `Resize` does (viewport destination at the logical size, `onResize`), then commit. Keep the logical size as state, since it is what the viewport destination says, instead of re-deriving it from `width / scale`, so the configure path and the scale path compute from the same number and rounding can't drift. - Before the swapchain exists (between the constructor's first configure and `CreateSwapchain`), only store the scale, as now. - A test that drives `xdg_surface_handle_preferred_scale` on a live window, as `PointerEnterLeave` drives the pointer callbacks, and checks that `width` follows and that a motion at the surface's middle lands at the buffer's middle. Related: #162. Its fix (in progress) makes the fractional scale only alongside a viewport; without one `scale` stays 1 and this does not arise.
Author
Owner

Another consequence of deriving the logical size from width / scale, found while reviewing 670df31: the conversion rounds up in both directions. The viewport destination is ceil(width / scale) (implementations/Crafter.Graphics-Window.cpp:395 in the constructor, :751-755 in Resize), and a sized configure becomes ceil(logical * scale) physical pixels (:1788-1794). fractional-scale-v1 says: "For toplevel surfaces, the size is rounded halfway away from zero."

At 1.5, a configure of 853 wide gives a buffer of ceil(1279.5) = 1280 (rounding gives the same) and then a destination of ceil(853.33) = 854: one logical pixel wider than configured, which a maximized window must not be, and the compositor resamples the 1280-pixel buffer over 1281 device pixels. At 1.25, 101 wide gives ceil(126.25) = 127, where the protocol's rounding gives 126.

The suggested fix above covers it: with the logical size kept as state, the destination is exactly the configured size, and the buffer is round(logical * scale), halfway away from zero. Line numbers refer to 670df31.

Another consequence of deriving the logical size from `width / scale`, found while reviewing 670df31: the conversion rounds up in both directions. The viewport destination is `ceil(width / scale)` (`implementations/Crafter.Graphics-Window.cpp:395` in the constructor, `:751-755` in `Resize`), and a sized configure becomes `ceil(logical * scale)` physical pixels (`:1788-1794`). fractional-scale-v1 says: "For toplevel surfaces, the size is rounded halfway away from zero." At 1.5, a configure of 853 wide gives a buffer of `ceil(1279.5) = 1280` (rounding gives the same) and then a destination of `ceil(853.33) = 854`: one logical pixel wider than configured, which a maximized window must not be, and the compositor resamples the 1280-pixel buffer over 1281 device pixels. At 1.25, 101 wide gives `ceil(126.25) = 127`, where the protocol's rounding gives 126. The suggested fix above covers it: with the logical size kept as state, the destination is exactly the configured size, and the buffer is `round(logical * scale)`, halfway away from zero. Line numbers refer to `670df31`.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Catcrafts/Crafter.Graphics#165
No description provided.