Details
-
Epic
-
Resolution: Unresolved
-
P2: Important
-
None
-
None
-
Painting glitches under fractional DPR scaling
Description
When painting code originally implemented for fixed (DPR=1) rendering is run with a fractional DPR, there will sometimes appear off-by-one glitches due to rounding.
To illustrate the basic issue, consider a horizontal line from x1=1 to x2=5, thus having length 4. Scaled by a factor of 1.33, this becomes x1=1.33 and x2=6.65, which rounds to a line with x1=1 and x2=7, having length 6. However if one scales the original length, 4*1.33=5.32, which rounds to 5, not 6. So, depending on whether the painted end result depends on the line's length or its endpoints, the scaled x2 should be either 6 or 7. Add a dimension, and this is the basic issue of scaling rectangles and then converting them to integer coordinates. The latter happens either when the painting code code converts to QRect or during aliased painting.
It is recognized that no general perfect solution exist, since the paint engine cannot know the intended end result . The goal of this task is then just to improve the default behavior, its consistency and predictability.
Key code points
The key points where most of the relevant scaling and rounding happen are
- QTransform::mapRect(...)
- QTransform::map(const QRegion&)
- QRectF::toRect()
- QRasterPaintEngine::toNormalizedFillRect(...)
In addition, the QHighDpi namespace uses its own scaling functions, e.g.
- QHighDpi::scale(const QRect&)
- QHighDpi::scale(const QRegion&)
The scaling/rounding of rectangles is not currently consistent between all these functions.
The history of QRectF::toRect() rounding
- Originally, it did round(top, left) & round(width, height). I.e. all rounding error was put to the bottomright coordinate, none to the size.
- From Qt 5.10, it did round(top, left) & round (bottom, right) - I.e. all rounding errors went to the size, none to the bottomright coordinate: https://codereview.qt-project.org/c/qt/qtbase/+/198112
- From Qt 6.0, it does round(top, left) and then tries to balance the rounding error between size and bottomright coordinate: https://codereview.qt-project.org/c/qt/qtbase/+/305554
Clipping and QRegion
Clipping areas are stored in a QRegion, which is based on QRects, i.e. integer coordinates. In the typical case, it contains only a single rectangle, but in case there are more, the basic assumption of QRegion is that an area can be expressed as edge-to-edge rectangles. As illustrated above, that assumption needs special care fractional scaling.
There may also be an issue here in that clip rects are scaled and rounded when they are set, and the original rect lost. So rounding errors may accumulate if further scaling is applied in later painting steps.
Anti-aliasing and coordinate system shift
Glitches due to rounding effects can sometimes be avoided by using anti-aliased painting. Then, instead of snapping to integer coordinates/whole pixel and filling them with 100% color, the paint engine will fill partially covered pixels with semi-transparent color.
A caveat when working with legacy code is to note that the coordinate system was shifted by a half unit (pixel) as of Qt 5.0. Earlier, (0.0, 0.0) designated the center point of the top left pixel. With Qt 5, (0.0, 0.0) designates the top left corner of that pixel, while (0.5, 0.5) designates its center point. The result is that by default, an anti-aliased 1-pixel-wide line, drawn with integer coordinates will now span two pixels in width, filling each of them 50%. By shifting the coordinates by 0.5, e.g. by applying a translation transform, one gets the same result as earlier Qt versions.
Although this shift is normally inconsequential under integer DPR and aliased painting, it can become significant with fractional DPR or anti-aliased painting.
Solution Discussion
Prioritising: those settings that are available as default selections on Windows: 25% increases from 100% to (typically) 300%
Ambition
Looking acceptable with fractional scaling, but accepting that there will be some blurriness. We can't accept gaps or stripes appearing, or incompletely updated regions leaving pixel dust behind.
(Note special situation for Mac: Typically no problems, since Mac gives us integer scaling (and does the fractional scaling behind the scenes). So the only way to get into trouble is to set a fractional QT_SCALE_FACTOR)
Proposals
A combination of:
- Scale everything to the nearest higher integer if we have a fractional DPR, then scale down when flushing the backing store. This requires HW-accelerated flushing and scaling of the backing store.
- brush patterns and similar might need special attention
- Change code drawing with QPainter in Qt (styles, in particular) use floating point APIs and types -> this will result in anti-aliased painting where today we have solid lines, and authors of custom widgets need to also do that
- make QPainter DPR aware, review QPainter code to make sure we round consistently
Possibly controlled by new render hints to allow application to select the best strategy.
Todos
- drag-drop robot graphicsview example w/scale factor (at least on mac)
- qwidget background fill and clip
- Need solution for custom widgets, customer painting code - new render hint?
Attachments
Issue Links
- covers
-
QTBUG-88934 [Reg 5.15 -> 6] QFrame in the menu isn't drawing
- Closed
- relates to
-
QTBUG-86344 Investigate UI rendering at fractional scale factors
- Open