Bug#1146122: kwin-wayland: Windows become permanently invisible/unmovable at fractional scale
James Robertson <[email protected]>
| Newsgroups | gmane.linux.debian.devel.qt-kde |
|---|---|
| Message-ID | <178805665202.30664.12763134941282642717.reportbug__5986.70994383923$1788057329$gmane$org@millionaire> |
Package: kwin-wayland Version: 4:6.3.6-1 Severity: important Dear Maintainer, On Wayland sessions with a fractional display scale factor (125%, 150%, ...), KWin 6.3.6 can get into a state where every geometry change applied to a window - including plain same-size moves - is silently deferred behind an xdg_surface configure that the client can never satisfy, making the window permanently invisible and unmovable. Root cause (src/xdgshellwindow.cpp, XdgSurfaceWindow::moveResizeInternal): whether a geometry change can be applied immediately is decided by comparing the requested client size with the committed client size, truncated via QSizeF::toSize(). The two values are derived differently, and at a fractional scale they can disagree permanently. Example at 125%: logical width 1041 is 1301.25 device pixels; KWin's side becomes 1040.8 -> 1040 while the client's committed size rounds to 1041. 1040 != 1041, forever. Every placement of that window then takes the configure path - even a move to a different position at the same size. Clients that suspend rendering while fully off-screen (Chromium/Electron-based applications, Firefox) never acknowledge the configure, so the window is wedged: it cannot be moved on-screen because KWin is waiting for the client, and the client will not draw because it is off-screen. Impact: any application window can end up invisible with no user-visible recovery short of killing the application. Scrolling/tiling KWin scripts (e.g. Karousel) move windows off-screen as part of normal operation and trigger this within minutes of ordinary use; user reports of the symptom: https://github.com/peterfajdiga/karousel/issues/166 Reproduction (Plasma Wayland session, any output at 125% scale): 1. Open a Chromium-family browser (Wayland-native, e.g. chromium --ozone-platform=wayland) and note its window is e.g. 1041 px wide (any width whose product with the scale factor is non-integer). 2. Move the window fully outside all outputs via the KWin scripting console (assigning frameGeometry), let it idle a few seconds. 3. Attempt to move it back on-screen the same way, same size. The assignment is never applied; reading frameGeometry back shows the old position indefinitely. At 100% scale the same steps behave normally. Fixed upstream in Plasma 6.7.0 - incidentally, as part of scale-override preparation work, which is presumably why it was never identified as a bugfix or backported to 6.4-6.6: https://invent.kde.org/plasma/kwin/-/commit/16affa41f2877799edaf073ddb440ef1e7c38d9f (the relevant part: both sides of the comparison now go through SurfaceInterface::snappedSize(), rounding both to the surface's pixel grid before comparing). The attached patch is a minimal equivalent of that comparison change for 6.3.6, applying on top of the existing upstream_e8541cf2_wayland-Fix-resizing-with-fractional-increments.patch (which reworked the surrounding lines but kept the truncated comparison). DEP-3 headers included. Tested: built in a trixie chroot and deployed on a trixie system running Plasma Wayland at 125% scale where the deadlock was reproducible at will; with the patch it cannot be reproduced at all, and previously-affected setups (tiling script + Chromium/Electron apps) behave normally. I am currently running the patched package, so reportbug may show my version as 4:6.3.6-1+snapfix1; the bug applies to stock 4:6.3.6-1. Given the small, targeted nature of the fix and that the failure mode is windows becoming permanently invisible, this seems like a candidate for a trixie point release; happy to provide any further testing. -- System Information: Debian Release: 13.6 APT prefers stable-updates APT policy: (500, 'stable-updates'), (500, 'stable-security'), (500, 'stable') Architecture: amd64 (x86_64) Kernel: Linux 7.1.3+deb13-amd64 (SMP w/8 CPU threads; PREEMPT) Kernel taint flags: TAINT_OOT_MODULE, TAINT_UNSIGNED_MODULE Locale: LANG=en_AU.UTF-8, LC_CTYPE=en_AU.UTF-8 (charmap=UTF-8), LANGUAGE=en_AU:en Shell: /bin/sh linked to /usr/bin/dash Init: systemd (via /run/systemd/system) LSM: AppArmor: enabled Versions of packages kwin-wayland depends on: ii libc6 2.41-12+deb13u3 ii libcanberra0 0.30-18 ii libcap2-bin 1:2.75-10+deb13u1+b1 ii libepoxy0 1.5.10-2 ii libkdecorations3-6 4:6.3.4-1 ii libkf6configcore6 6.13.0-2 ii libkf6configgui6 6.13.0-2 ii libkf6configwidgets6 6.13.0-1 ii libkf6coreaddons6 6.13.0-1 ii libkf6crash6 6.13.0-1 ii libkf6dbusaddons6 6.13.0-1 ii libkf6globalaccel6 6.13.0-1 ii libkf6i18n6 6.13.0-1 ii libkf6idletime6 6.13.0-1 ii libkf6notifications6 6.13.0-1 ii libkf6service-bin 6.13.0-1 ii libkf6service6 6.13.0-1 ii libkf6svg6 6.13.0-1 ii libkf6windowsystem6 6.13.0-2 ii libkglobalacceld0 6.3.6-1 hi libkwin6 4:6.3.6-1+snapfix1 ii libqaccessibilityclient-qt6-0 0.6.0-3 ii libqt6core6t64 [qt6-base-private-abi] 6.8.2+dfsg-9+deb13u2 ii libqt6dbus6 6.8.2+dfsg-9+deb13u2 ii libqt6gui6 6.8.2+dfsg-9+deb13u2 ii libqt6qml6 6.8.2+dfsg-7 ii libqt6quick6 6.8.2+dfsg-7 ii libqt6widgets6 6.8.2+dfsg-9+deb13u2 ii libstdc++6 14.2.0-19 ii libsystemd0 257.13-1~deb13u1 ii libxcb-composite0 1.17.0-2+b1 ii libxcb-randr0 1.17.0-2+b1 ii libxcb-xfixes0 1.17.0-2+b1 ii libxcb1 1.17.0-2+b1 ii xwayland 2:24.1.6-1 Versions of packages kwin-wayland recommends: ii xwaylandvideobridge 0.4.0-2+b1 kwin-wayland suggests no packages. -- no debconf information
kwinsnappedsize.patch
(text/plain, 3.6 KB)
Description: Compare client sizes snapped to the surface scale in moveResizeInternal
At fractional display scale factors (e.g. 125%), many logical window sizes are
not representable in device pixels. XdgSurfaceWindow::moveResizeInternal()
decides whether a geometry change can be applied immediately or must wait for
the client to acknowledge a configure by comparing the requested client size
with the committed client size, truncated via QSizeF::toSize(). The two values
are derived differently, and at a fractional scale they can disagree
permanently (e.g. logical width 1041 at 125% = 1301.25 device pixels: KWin's
side becomes 1040.8 -> 1040 while the client's committed size rounds to 1041).
.
From that point on, every placement of the affected window - including plain
same-size moves - schedules a configure the client can never satisfy exactly.
Clients that suspend rendering while fully off-screen (Chromium- and
Electron-based applications, Firefox) then never acknowledge it, and the
window becomes permanently invisible and unmovable until the application
happens to redraw or is closed. Scrolling/tiling window managers (e.g. the
Karousel KWin script) trigger this readily; see
https://github.com/peterfajdiga/karousel/issues/166 for user reports.
.
Upstream fixed this in Plasma 6.7.0, incidentally, as part of the scale
override preparation work: both sides of the comparison now go through
SurfaceInterface::snappedSize(), which rounds both values to the surface's
pixel grid before comparing. That commit is entangled with new scale-override
infrastructure, so this patch is a minimal equivalent for 6.3.6: snap both
sides to the same target scale that the requested frame size was already
snapped with two lines above. It sits on top of Debian's
upstream_e8541cf2_wayland-Fix-resizing-with-fractional-increments.patch,
which reworked the surrounding lines but kept the truncated comparison.
.
Verified on Debian trixie (Plasma 6.3.6, Wayland, 125% scale): before the
patch the deadlock is reproducible within a minute of normal use with a
scrolling tiler; after it, it cannot be reproduced at all. At 100% scale the
bug never occurs, with or without the patch.
Origin: backport, https://invent.kde.org/plasma/kwin/-/commit/16affa41f2877799edaf073ddb440ef1e7c38d9f
Applied-Upstream: 6.7.0
Author: James <jelloir>
Last-Update: 2026-08-29
--- a/src/xdgshellwindow.cpp
+++ b/src/xdgshellwindow.cpp
@@ -272,9 +272,14 @@ void XdgSurfaceWindow::moveResizeInternal(const QRectF &rect, MoveResizeMode mo
// and current client sizes have to be rounded to integers
const QSizeF requestedFrameSize = snapToPixels(rect.size(), nextTargetScale());
const QSizeF requestedClientSize = nextFrameSizeToClientSize(requestedFrameSize);
- const QSize roundedRequestedClientSize = requestedClientSize.toSize();
+ const qreal snapScale = nextTargetScale();
+ const auto snapSizeToScale = [snapScale](const QSizeF &size) {
+ return QSizeF(qreal(qRound(size.width() * snapScale)) / snapScale,
+ qreal(qRound(size.height() * snapScale)) / snapScale);
+ };
+ const QSizeF roundedRequestedClientSize = snapSizeToScale(requestedClientSize);
- const QSize roundedClientSize = clientSize().toSize();
+ const QSizeF roundedClientSize = snapSizeToScale(clientSize());
if (roundedRequestedClientSize == roundedClientSize) {
const QRectF snappedRect = QRectF(rect.topLeft(), nextClientSizeToFrameSize(snapToPixels(roundedClientSize, nextTargetScale())));
updateGeometry(gravitateGeometry(snappedRect, rect, m_nextGravity));