You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Bug]: Rendering on Wayland with fractional scaling looks bad #30218
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).
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.)
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.
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?
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.
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.
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).
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.
I get the following output from the example I posted above using any fractional scaling between 100% and 200%:
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.
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!
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?
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.
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)?
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.
Activity
timhoffm commentedon Jun 27, 2025
Just to clarify the the scope: It's not happening for me with x11. So likely indeed wayland topic.
Maybe related: https://forum.qt.io/topic/160425/wayland-and-fractional-screen-scaling-reported-as-integer-by-wayland-info-and-q-screen
cbrnr commentedon Jun 27, 2025
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.
timhoffm commentedon Jun 27, 2025
Can you please try
cbrnr commentedon Jul 3, 2025
Indeed, I get
with a 150% scale setting.
cbrnr commentedon Jul 16, 2025
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?
timhoffm commentedon Jul 16, 2025
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.
cbrnr commentedon Jul 16, 2025
Thanks! Just to be sure, would the following example show that it's a Qt issue if the
devicePixelRatioF()is2.0instead of the expected1.5?You mentioned that you cannot reproduce the issue on X11, do you get the expected
2.0value? I can try this tomorrow on my work PC.tacaswell commentedon Jul 16, 2025
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:
so the issue is in Qt or wayland, not pyside or pyqt.
tacaswell commentedon Jul 16, 2025
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
devicePixelRatiowith Qt6 anddevicePixelRatioFwith Qt5.1 remaining item
cbrnr commentedon Jul 17, 2025
I get the following output from the example I posted above using any fractional scaling between 100% and 200%:
For 100%, I get 1.0. For anything >200% and <300%, I get
3.0, and so on.So it seems like both
devicePixelRatioanddevicePixelRatioFare returning identical integer values (as floats) in my case (also when accessing these methods fromfig.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.
timhoffm commentedon Jul 17, 2025
This is also consistent with the code https://codebrowser.dev/qt6/qtbase/src/gui/painting/qpaintdevice.h.html#_ZNK12QPaintDevice17devicePixelRatioFEv
Not sure how @tacaswell can arrive at different values.
cbrnr commentedon Jul 17, 2025
This makes sense then LOL! Maybe he's using X11?
cbrnr commentedon Jul 17, 2025
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!
tacaswell commentedon Jul 17, 2025
I was definitely using wayland, but tested
devicePixelRatioafter 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 thatdevicePixelRatioFwas still the same 🤦🏻 .Some thoughts:
QScreengive 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).'draw_event'callback?jklymak commentedon Jul 17, 2025
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.
cbrnr commentedon Jul 22, 2025
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)?QuLogic commentedon Jul 23, 2025
We do use the surface scale, I believe, as we ask for
QPaintDevice.devicePixelRatiowhich is a base class of theQWidget. We don't useQScreen.devicePixelRatiofor the screen ratio. The problem though, is that we trigger changes based on theQScreen.physicalDotsPerInchChangedandQScreen.logicalDotsPerInchChangedsignals, 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::DevicePixelRatioChangeon the window instead, which might work more reliably.qt: Use better devicePixelRatio event to refresh scaling
qt: Use better devicePixelRatio event to refresh scaling
qt: Use better devicePixelRatio event to refresh scaling
qt: Use better devicePixelRatio event to refresh scaling
qt: Use better devicePixelRatio event to refresh scaling
qt: Use better devicePixelRatio event to refresh scaling