Skip to content

[Bug]: Rendering on Wayland with fractional scaling looks bad #30218

Closed

Description

@cbrnr

Bug summary

When rendering a plot on Wayland with fractional scaling turned on (e.g., 150%), the resulting plot content looks bad (i.e., jagged edges in lines and text). The UI is fine though (i.e., the toolbar looks nice and sharp).

Code for reproduction

import matplotlib

matplotlib.use("qtagg")

import matplotlib.pyplot as plt

plt.plot([1, 10, -3, 17, 0, -6, 2, 12, 18, -3])
plt.show()

Actual outcome

Image

Expected outcome

I would expect the plot to render nicely.

Additional information

Maybe there is something wrong with detecting the actual DPI when fractional scaling is enabled? I also noticed that when creating the figure with 100% and then switching to 150% scaling in my desktop environment (I'm using GNOME), the figure remains sharp (i.e., plot elements are anti-aliased). It's only when I create the figure from 150% scaling that it is jagged. (I use a 4K screen with 150% fractional scaling.)

Operating system

Arch Linux

Matplotlib Version

3.10.3

Matplotlib Backend

qtagg

Python version

3.13.5

Jupyter version

No response

Installation

None

Activity

  1. timhoffm commented on Jun 27, 2025

    @timhoffm
    Member

    Just to clarify the the scope: It's not happening for me with x11. So likely indeed wayland topic.

    • Which Qt version are you using?
    • Does it work with 200% scaling?

    Maybe related: https://forum.qt.io/topic/160425/wayland-and-fractional-screen-scaling-reported-as-integer-by-wayland-info-and-q-screen

  2. cbrnr commented on Jun 27, 2025

    @cbrnr
    ContributorAuthor

    Yes, this is specific to Wayland. I'm using PySide6 6.9.1, which uses Qt 6.9.1. Everything looks fine with 200% scaling, so it only affects fractional scaling. I'm not sure the linked issue is related, because (at least in my opinion) PySide6 UI elements look fine, it's only Matplotlib plots that look very jagged.

  3. timhoffm commented on Jun 27, 2025

    @timhoffm
    Member

    Can you please try

    import matplotlib.pyplot as plt
    fig = plt.figure()
    print(fig.canvas.devicePixelRatioF())
    
  4. cbrnr commented on Jul 3, 2025

    @cbrnr
    ContributorAuthor

    Indeed, I get

    2.0
    

    with a 150% scale setting.

  5. cbrnr commented on Jul 16, 2025

    @cbrnr
    ContributorAuthor

    Is this a known Qt issue? If not, I'm happy to report it. Given that the UI (toolbar etc.) looks good, and this only affects the Matplotlib part, could you please explain why you think this is a Qt bug?

  6. timhoffm commented on Jul 16, 2025

    @timhoffm
    Member

    If devicePixelRatioF() yields 2.0 for a scaling of 150% this is clearly a bug.

    I'm not 100% sure why it "only affects the matplotlib part". Speculation: We calculate the physical pixel size of the image from logical pixel size of the window * devicePixelRatioF and render a pixel image to that size. If devicePixelRatioF is 2.0 instead of 1.5 our pixel image is larger than needed and Qt downsamples it to display it in the window.

    Not clear why the toolbar looks good. Two possible explanations: Either it's just rendered larger. You should be able to test this: If dpi scaling works correctly for it the physical size on screen should be the same no matter whether you scale 100%, 150%, 200%. If the 150% toolbar is larger this would be an indication that it internally uses devicePixelRatioF 2.0, but other than with the matplotlib image, it's not downsampled but just takes more screen space.
    Alternatively, it could be that the effect is not present, because it's drawn via graphics primitives (lines rectangles etc.) and not as a a pixel image.

  7. cbrnr commented on Jul 16, 2025

    @cbrnr
    ContributorAuthor

    Thanks! Just to be sure, would the following example show that it's a Qt issue if the devicePixelRatioF() is 2.0 instead of the expected 1.5?

    import sys
    
    from PySide6.QtGui import QGuiApplication
    from PySide6.QtWidgets import QApplication, QLabel, QVBoxLayout, QWidget
    
    
    class PixelRatioWindow(QWidget):
        def __init__(self):
            super().__init__()
            self.setWindowTitle("Device Pixel Ratio Example")
    
            screen = QGuiApplication.primaryScreen()
    
            screen_dpr = screen.devicePixelRatio()
            widget_dpr = self.devicePixelRatio()
            widget_dpr_f = self.devicePixelRatioF()
    
            label = QLabel(
                f"QScreen.devicePixelRatio(): {screen_dpr}\n"
                f"QWidget.devicePixelRatio(): {widget_dpr}\n"
                f"QWidget.devicePixelRatioF(): {widget_dpr_f}"
            )
    
            layout = QVBoxLayout()
            layout.addWidget(label)
            self.setLayout(layout)
    
    
    if __name__ == "__main__":
        app = QApplication(sys.argv)
        window = PixelRatioWindow()
        window.show()
        sys.exit(app.exec())

    You mentioned that you cannot reproduce the issue on X11, do you get the expected 2.0 value? I can try this tomorrow on my work PC.

  8. tacaswell commented on Jul 16, 2025

    @tacaswell
    Member

    I also see this issue with gnome on wayland. 100%, 125%, 175%, and, 200% look good to me, but 150% looks bad.

    It looks like going 225% ups the scaling to 3.0 so the "fraction" scaling is snapping to integers. I agree with Tim's theory of what is going on. If the down sampling is being done effectively via "nearest", then it may also be the case that the icons don't show the effects because they are very blocky and mostly horizontal/vertical/45 degree lines that the effect is just less visible.

    I see this with PyQt6 with:

    In [15]: PyQt6.QtCore.QT_VERSION_STR
    Out[15]: '6.9.1'
    
    In [16]: PyQt6.QtCore.PYQT_VERSION_STR
    Out[16]: '6.9.0'
    
    

    so the issue is in Qt or wayland, not pyside or pyqt.

  9. added this to the v3.10.4 milestone on Jul 16, 2025
  10. tacaswell commented on Jul 16, 2025

    @tacaswell
    Member
    In [17]: print(fig.canvas.devicePixelRatio())
    1.5
    
    

    gives the right result so maybe we just need to change the API we are calling? I think we need to do some archaeology as to why to use this API (I have a memory and the name suggests that DevicePixelRatio used to return an integer).

    https://doc.qt.io/qt-6/qpaintdevice.html#devicePixelRatioF is not really clear why there are two, but in https://doc.qt.io/archives/qt-5.15/qpaintdevice.html#devicePixelRatioF we can see that for Qt5 the non-F version return an int not a qreal.


    I think the core bug here is that the two methods do not return the same thing is (some versions) of Qt6. As a workaround I guess we bring back a shim to get the have some qt5 vs qt6 dependent behavior and prefer devicePixelRatio with Qt6 and devicePixelRatioF with Qt5.

  11. 1 remaining item

  12. cbrnr commented on Jul 17, 2025

    @cbrnr
    ContributorAuthor

    I get the following output from the example I posted above using any fractional scaling between 100% and 200%:

    Image

    For 100%, I get 1.0. For anything >200% and <300%, I get 3.0, and so on.

    So it seems like both devicePixelRatio and devicePixelRatioF are returning identical integer values (as floats) in my case (also when accessing these methods from fig.canvas).

    Thanks for discovering this Qt issue @QuLogic, I think that's exactly the problem we're seeing here. I'll post my example over there.

  13. timhoffm commented on Jul 17, 2025

    @timhoffm
    Member

    So it seems like both devicePixelRatio and devicePixelRatioF are returning identical integer values (as floats

    This is also consistent with the code https://codebrowser.dev/qt6/qtbase/src/gui/painting/qpaintdevice.h.html#_ZNK12QPaintDevice17devicePixelRatioFEv

    Image

    Not sure how @tacaswell can arrive at different values.

  14. cbrnr commented on Jul 17, 2025

    @cbrnr
    ContributorAuthor

    This makes sense then LOL! Maybe he's using X11?

  15. cbrnr commented on Jul 17, 2025

    @cbrnr
    ContributorAuthor

    According to https://bugreports.qt.io/browse/QTBUG-123125, the correct ratio can only be determined after a first paint event! So running this modified example now correctly returns 1.5!

    import matplotlib.pyplot as plt
    
    fig, ax = plt.subplots()
    ax.plot([1, 2, 3], [4, 5, 6])
    plt.show()
    
    print(fig.canvas.devicePixelRatio())
    print(fig.canvas.devicePixelRatioF())
  16. tacaswell commented on Jul 17, 2025

    @tacaswell
    Member

    I was definitely using wayland, but tested devicePixelRatio after I had closed the figure in an IPython shell so post-draw. It had not occurred to me that the result could depend on drawing so did not think to check that devicePixelRatioF was still the same 🤦🏻 .

    Some thoughts:

    • does QScreen give the right answer? If so we could query those and check if the the window gives us a different answer, but if there is more than one screen with different scaling we would have to sort out which screen it is on (and I'm not sure off the top of my head if Qt gives us a way to ask that).
    • How does this play with moving windows between screens with different ratios and the notification chain?
    • Can we add a check at the bottom of our internal draw that checks if the windows ratio changed from before and after we let Qt do it's thing and then ask for an additional draw_idle? This would require testing exactly when the value changes and if our code is "inside" or "outside" of that loop. Maybe do this in the code, maybe do this with a one-shot 'draw_event' callback?
  17. jklymak commented on Jul 17, 2025

    @jklymak
    Member

    Does qt have a surfacepixelratio? The argument from the Wayland folks is that apps should be consulting a ratio local to the application (or "surface") rather than a device pixel ratio.

  18. cbrnr commented on Jul 22, 2025

    @cbrnr
    ContributorAuthor

    As a quick fix, I think it would be great if Matplotlib triggered a one-shot draw event on Linux (technically only on Wayland) so that the devicePixelRatio() reflected the correct value. Would that be possible without too much effort (I don't know anything about the Matplotlib internals)?

  19. QuLogic commented on Jul 23, 2025

    @QuLogic
    Member

    We do use the surface scale, I believe, as we ask for QPaintDevice.devicePixelRatio which is a base class of the QWidget. We don't use QScreen.devicePixelRatio for the screen ratio. The problem though, is that we trigger changes based on the QScreen.physicalDotsPerInchChanged and QScreen.logicalDotsPerInchChanged signals, and it seems that that doesn't correspond with the window scale actually changing.

    It looks like since Qt 6.6, we can maybe use QEvent::DevicePixelRatioChange on the window instead, which might work more reliably.

  20. added 6 commits that reference this issue on Jul 23, 2025
    1b4e322
    e8fa3b3
    e987b61
    97c3be7
    a23bb28
    ff5c842
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions