https://bugs.kde.org/show_bug.cgi?id=414266

--- Comment #6 from nyanpasu64 <nyanpas...@tuta.io> ---
Created attachment 134836
  --> https://bugs.kde.org/attachment.cgi?id=134836&action=edit
Video of a Qt Quick 2 button glitching when resized, with patched
qqc2-desktop-style

I've done a deep-dive investigation, looking into multiple issues and Qt Quick
design defects causing this bug's symptoms to occur. For reference, the C++
implementation of the theme is at
https://invent.kde.org/frameworks/qqc2-desktop-style/-/blob/7cdeaab75171b4126ba9026875539108a834a577/plugin/kquickstyleitem.cpp.

## Background

Qt Quick 2 positions and sizes items in device-independent pixels.
`KQuickStyleItem` (defined by qqc2-desktop-style in the above link) is a
subclass of `QQuickItem` used to render widgets and output them on-screen. In
simple QQC2 apps (like System Settings panels), the position and size of each
item are integers in device-independent pixels. Perhaps in more complex apps,
the widgets could be translated or rotated, making them no longer integers.

When `KQuickStyleItem::updatePolish()` is called, it renders the widget using
`QPainter`/`QStyle` onto a member-variable `QImage m_image`. (Note that this is
an abuse of updatePolish() which is intended to relayout and not redraw.) When
`KQuickStyleItem::updatePaintNode()` is called, it uploads the `QImage` to a
GPU texture, and outputs a scene graph item (`QSGNinePatchNode *` upcasted to
`QSGNode *`) pointing to the texture. Qt Quick 2 turns it into a rectangle (two
tris) and sends the mesh/texture to OpenGL.

The size and position of the `KQuickStyleItem`/`QQuickItem` is not determined
by `KQuickStyleItem::updatePaintNode()` or `KQuickStyleItem::updatePolish()`,
but merely consumed. `QQuickItem::boundingRect()` is an undocumented method
used in `KQuickStyleItem::updatePaintNode()` (and assumed to match
`width()`/`height()` which is what `updatePolish()` uses). Based on my debug
prints, `boundingRect()` returns a rectangle where the top-left lies at (0, 0),
even if the item does not lie at the window's origin. You need to use
`mapToGlobal()` to find the true position in the window.

By running RenderDoc on QQC2 apps (kcm_fonts and a single-button test app), I
found that the rectangle's coordinates are sent to OpenGL as device-independent
pixels (`qt_VertexPosition`), and the vertex shader transforms them into
`gl_Position` coordinates. Then it samples the texture using nearest-neighbor
interpolation, even if you modify the `createTextureFromImage()` value by
calling `setFiltering(QSGTexture::Linear)`. I was unable to get RenderDoc to
replay the trace with linear filtering enabled.

## Bug

There are multiple layers of bugs:

1. With DPI display scaling enabled, the polygon drawn on the GPU, as well as
the region used by mouse handling, no longer lies on integer physical pixels.
The visual polygon's size may not match the QImage's size.

    - I "fixed" this by having `KQuickStyleItem::updatePaintNode()` return a
scene-graph node which matches the QImage's size and not the `QQuickItem`'s
size (a).

2. Even if the visual polygon's size is an integer number of physical pixels,
the x or y coordinates can lie on a 0.5-pixel boundary. This causes rounding
errors and sometimes results in the "diagonal seam" effect (and might also
cause duplicated/missing rows/columns) where the two triangles of the rectangle
round their boundaries and texture coordinates differently. With
`QQuickWindow::TextureCanUseAtlas` in place, the diagonal seam effect is
inconsistent, doesn't appear in all buttons, and can appear/disappear when you
hover a button (because many `KQuickStyleItem`s are placed onto a single
texture uploaded to the GPU, resulting in inconsistent rounding). With that
parameter removed (b), the effect is consistent and appears in all buttons with
the same x or y endpoints, in both hovered/unhovered states.

    - I recorded and attached a video demonstrating this bug occurring when I
resize a window, with a patched version of qqc2-desktop-style which sets the
returned node's size to the QImage's size in physical pixels (a) and also
disables Qt's texture atlas (b). I saw both duplicated rows/columns of pixels
and diagonal seams.

    - I don't know the best fix for this. I had a branch where
`KQuickStyleItem::updatePaintNode()` used `mapToGlobal` and `mapFromGlobal` to
quantize the output polygon to global physical pixels (c). However, this method
is only called when a widget is hovered, not upon window resize (which also
changes the widget's position and affects pixel rounding), so it's not a
workable solution. Worse yet, this solution fails completely if you rotate a
widget (but I suspect the current qqc2 theme had the same behavior). Also I was
not able to find a way to enable linear filtering for `KQuickStyleItem`'s
returned paint node, which would cause non-integer nodes/items to be blurry and
ugly, but obviously stand out and not have diagonal/horizontal/vertical seams.

3. I made a test QQC2 app, and resized the window interior to 110x42 physical
pixels (at 125% display scale). The vertex shader's transformation matrix
(`qt_Matrix`) is used to map device-independent pixels (`qt_VertexPosition`) to
OpenGL coordinates. However the matrix's vertical transform is wrong because it
thinks the output texture size is 110 x 42.5 physical pixels (corresponding to
34 device-independent pixels, an integer). This causes missing and/or duplicate
rows of pixels, even on my branch where I implemented a partial fix for Issue 2
(c) .

    - I think this is a Qt bug, not a KDE theme bug.

I have a private Git repo with my attempted bugfixes. Unfortunately it's a `git
init` repo based off Arch's `qqc2-desktop-style-5.78.0` package, not a clone of
the upstream source. I can publish my repo if anyone is interested.

I have no desire to work on a Qt fix for Bug 3 (`qt_Matrix`), since Qt 5.15 is
no longer receiving public or open-source updates. I may investigate further if
Kirigami or qqc2-desktop-style ports to Qt 6 (which still has source code for
the time being), or if KDE/etc. forks Qt 5.15.

I don't know how to fix Bug 2 (non-integer positions and rounding errors)
because I don't know how to recompute the scene node's position whenever the
QML item is moved (eg. by the layout). Neither updatePolish() nor
updatePaintNode() are called when I resize the window.

Additionally I don't know if there's any interest in fixing this bug. I don't
know how many people use fractional scaling on KDE X11 (or Wayland if the bug
occurs there as well). Those people are going to run into this bug more and
more, as KDE ports system settings panels (and possibly other apps) to QML.

-- 
You are receiving this mail because:
You are watching all bug changes.

Reply via email to