Uploaded image for project: 'Qt'
  1. Qt
  2. QTBUG-101269

Pixel-perfect rendering with QtQuick in non-integer hidpi scaling

    XMLWordPrintable

Details

    • User Story
    • Resolution: Unresolved
    • Not Evaluated
    • None
    • 5.12.12, 5.15.2
    • GUI: High-DPI
    • None

    Description

      I am an avid contributor to the open source painting application Krita. Being a visual art software, it is important for us to render the canvas with a one-to-one pixel mapping, even when fractional high-DPI scaling is active. With QOpenGLWidget, it has been possible for us to calculate the viewport size correctly and render the canvas accurately. Consider an example with devicePixelRatio == 1.5 and the size of the QOpenGLWidget is 299x299. The calculated physical pixel size would be 299 * 1.5 = 448.5, but there is no such thing as half a pixel, so QOpenGLWidget truncates the dimensions when setting up the viewport for paintGL, i.e. the viewport is 448 px in size. From this, we have to internally adjust the logical size to be 448 / 1.5 = 298.6666... units (which I dubbed widgetSizeAlignedToDevicePixel at one point) in order for the dimensions to align with the physical pixels boundary. With these metrics, we can set up a model-view-projection matrix in logical pixel space that correctly scales by devicePixelRatio and finally render a pixel-perfect canvas.

      In the past weeks, I have been experimenting with porting the canvas to QtQuick2. Getting the same pixel-perfect rendering has been difficult. One would think that, since QQuickItem::size() is a QSizeF, we could just use a QQuickFramebufferObject, set its size to a value that is aligned to the device pixel (like 298.6666... in the above example) and everything is sorted, right? Unfortunately no...

      The first roadblock is how QQuickFramebufferObject calculates the desired FBO size: int(int(size) * devicePixelRatio). If the item size is set to 298.6666... the result will be floor(298 * 1.5) = 447 which is 1px smaller than expected. Fortunately it can be easily bypassed by calculating the FBO size ourselves in createFramebufferObject and ignoring the size passed in the argument.

      However. The size of QQuickWindow is an integer QSize in logical pixels, like QOpenGLWidget. Let's say its size is 299 units. It also has to make the viewport in integer device pixel, but unlike QOpenGLWidget, QQuickWindowPrivate::renderSceneGraph rounds to nearest instead of truncating (operator *(qreal, QSize) rounds the result) so it becomes 449 px. (This doesn't make a huge difference in practice, though there is a chance the viewport may get 1px larger than the actual native window and results in an edge being cropped.) Now, its actual logical size in theory would be 449 / 1.5 = 299.3333..., but check the source – renderer->setProjectionMatrixToRect(QRect(QPoint(0, 0), size)) – it sets up the projection matrix with the integer logical size 299 units, which means, if I set the item size to either 298.6666... or 299.3333... the QQuickFramebufferObject texture will be stretched slightly. In the end the output will not be pixel-perfect, but a blurry interpolated mess.

      The previous link shows the code in Qt 5.12.12. In Qt 5.15.2 there is an additional surfaceSize argument to renderSceneGraph. See how it sets logicalSize = QSizeF(surfaceSize) / devicePixelRatio? This would be exactly what we want in the first place! If only this argument is used everywhere...

      Should it be so complicated to achieve pixel-perfect rendering with QtQuick? From my view, the root of evil is QWindow having integer size in logical pixels. (Same thing can be said for QWidget and QScreen.) If QWindow/QQuickWindow would handle its size as floating point to reflect the real logical size, and use QSizeF for everything (including QResizeEvent), then the whole thing may be simpler. But as the current Qt implementation stands, it is very difficult, if not impossible, to account for every numerical rounding that may happen in the process to trick QtQuick into render our canvas in a pixel-perfect way.

      (Also in regard to laying out items, I wish Qt would have a built-in feature that aligns items to the physical pixel grid. WPF (the .NET Framework UI that is not WinForms) has the properties UseLayoutRounding and SnapsToDevicePixels to control such behaviour, which can make sure that certain elements will be positioned on pixel boundaries regardless of the scaling.)

      Attachments

        No reviews matched the request. Check your Options in the drop-down menu of this sections header.

        Activity

          People

            sorvig Morten Sørvig
            alvinhochun Alvin Wong
            Votes:
            0 Vote for this issue
            Watchers:
            2 Start watching this issue

            Dates

              Created:
              Updated:

              Gerrit Reviews

                There are no open Gerrit changes