-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix scaling of cover art on high pixel density screens #2247
Conversation
src/library/coverartcache.cpp
Outdated
@@ -48,6 +48,7 @@ CoverArtCache::~CoverArtCache() { | |||
QPixmap CoverArtCache::requestCover(const CoverInfo& requestInfo, | |||
const QObject* pRequestor, | |||
const int desiredWidth, | |||
const double devicePixelRatio, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that the devicePixelRatio should be specified separately for each widget that accesses CoverArtCache. I thought about setting a global scale factor for CoverArtCache, but that would not work well in case widgets on different screens with different scale factors access the CoverArtCache singleton.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an important info why the design is as it is now. Please add the comment to the code as well.
The generated image now depends on |
Why not just used the scaled size in the cache key? |
But then I don't get the idea behind this PR. If only the image size matters, why let the cache know anything about the device pixel ratio if it doesn't affect the resulting images that are reused for matching requests? If you request the same size but with a different device pixel ratio you will always get the same image. If on the other hand it does affect the cached image it must be reflected in the cache key. Otherwise the caches doesn't work as expected. My first idea was that the cache should never care about where images are actually displayed. It should simply pre-compute and store images of different (pixel) sizes and the client needs to decide which size is appropriate for display. Maybe I don't understand how |
It's confusing and not documented very well IMO. Without CoverArtCache calling this, everywhere cover art is used would have to do it. As I understand it, if the devicePixelRatio of the QImage/QPixmap are not equal to the screen, Qt will automatically scale the QImage/QPixmap up when a QPainter draws it on screen. But if the QImage/QPixmap's devicePixelRatio is equal to the screen's, then Qt skips this automatic scaling. This also requires the real dimensions of the QImage/QPixmap to be scaled by the devicePixelRatio.
I think you are right. I'll do that. However, this doesn't solve the issue with only one CoverArtDelegate's cover art getting drawn at a time. Do you have any ideas about that? |
It turns out this had nothing to do with caching. I was inappropriately scaling the target QRect for the drawing target, so the covers were being drawn, but they were being drawn outside the boundaries of the CoverArtDelegate. Ready to merge? |
src/library/coverartcache.cpp
Outdated
.arg(QString::number(hash)).arg(width); | ||
QString pixmapCacheKey(quint16 hash, int width, double devicePixelRatio) { | ||
return QString("CoverArtCache_%1_%2_%3") | ||
.arg(QString::number(hash)).arg(width).arg(devicePixelRatio); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest to scale and round the device pixel ratio before formatting into an integer, e.g. 1.5 becomes "150":
...arg(static_cast<int>(std::round(devicePixelRatio * 100)))
Maybe add some assertions to detect unexpected values, e.g. devicePixelRatio >= 0.1 and devicePixelRatio <= 10.0 (for 10x up/down scaling)
I'm still not sure if the addition of device pixel ratio here is really necessary. Assume that devicePixelRatio = 2.0 and you request the image with the original size by setting desiredWidth = 0. Then you get back a QImage, say 400x400. You also could have requested it with desiredWidth = 200 and devicePixelRatio = 2.0 and you still get the same 400x400 image. The parameter
For caching purposes only the pixel resolution of the image matters, but not the actual device resolution! Internally |
We need to ensure that the public API uses either actual pixels or (virtual) device pixels. If it uses the latter then the |
Done. However, I think it is very unlikely that an image on another screen will be scaled to the same real pixel size as one on another screen with a different scale factor. The ratio of the device-independent scaled areas on different screens would need to be equal to the ratio of the devicePixelRatios of each screen. Also remember CoverArtCache only caches resized images; full size images do not get cached so there is nothing to share. |
src/library/coverartcache.cpp
Outdated
const bool onlyCached, | ||
const bool signalWhenDone) { | ||
if (sDebug) { | ||
kLogger.debug() << "requestCover" | ||
<< requestInfo << pRequestor << | ||
desiredWidth << onlyCached << signalWhenDone; | ||
deviceIndependentWidth << onlyCached << signalWhenDone; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New parameter devicePixelRatio is missing
src/library/coverartcache.cpp
Outdated
const bool signalWhenDone) { | ||
if (sDebug) { | ||
kLogger.debug() << "loadCover" | ||
<< info << desiredWidth << signalWhenDone; | ||
<< info << deviceIndependentWidth << signalWhenDone; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New parameter devicePixelRatio is missing
src/library/coverartcache.cpp
Outdated
@@ -173,9 +182,10 @@ void CoverArtCache::coverLoaded() { | |||
// we have to be sure that res.cover.hash is unique | |||
// because insert replaces the images with the same key | |||
QString cacheKey = pixmapCacheKey( | |||
res.cover.hash, res.cover.resizedToWidth); | |||
res.cover.hash, res.cover.resizedToWidth * res.cover.devicePixelRatio); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The multiplication could be wrapped into a member function CoverArt::actualWidth()
src/library/coverartcache.h
Outdated
@@ -26,11 +26,13 @@ class CoverArtCache : public QObject, public Singleton<CoverArtCache> { | |||
QPixmap requestCover(const CoverInfo& info, | |||
const QObject* pRequestor, | |||
const int desiredWidth, | |||
const double devicePixelRatio, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please finish the renaming in the header file: desiredWidth -> deviceIndependentWidth
src/library/coverartdelegate.cpp
Outdated
QRect source(0, 0, target.width(), target.height()); | ||
int width = math_min(pixmap.width(), option.rect.width()) * scaleFactor; | ||
int height = math_min(pixmap.height(), option.rect.height()) * scaleFactor; | ||
QRect target(option.rect.x(), option.rect.y(), width, height); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we have to scale manually to the device resolution instead of letting Qt do the work by consistently using setDevicePixelRatio() and doing all sizing in device independent coordinates? This is how I thought it is supposed to work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I'll take another look at this logic. pixmap.width()/height() should be in scaled sizes already, but it looks like math_min picks the option.rect.width()/height(), which then needs to be scaled.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When I was first writing this, I multiplied by scaleFactor everywhere until I got it to work without really understanding what was going on. I may be able to simplify the code now...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All this manual scaling is confusing, I still don't get it. Why doesn't Qt provide a consistent measurement system? My assumption was that setting device pixel ratio does all the magic for us. These conversions back and forth between different coordinate systems are really error prone.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, it is awfully confusing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the original code is now outdated since we have non ^2 scale favtors.
Originally this code prevents us from upscaling an actual 15×15 to 20x20 for instance. This will likely look blurry. Instead we print the 15x15 and pad it for 20x20
This approach still makes sense, but we need to considder the scaleFactor.
If it is 1 or 2 the code works. If it is 1.5 the code fails.
20x20 is in this case 30x30 and the pixmap will look perfectly scaled by 2.
I must admit that this is a constructed example, but it is relevant.
No matter what you set in
Now we are doing the transformation between device independent pixels twice, both inside the cache and outside when calculating the layout before painting the image. This is bad. We should clearly separate the responsibilities to avoid confusion:
Would this help reduce the number of transformations and keep them within a single abstraction layer? Because now the multiplication (device independent size -> actual pixel size) is done within the cache and whereas the division (actual pixel size -> device independent size) is done by the widget. The widgets should solely be responsible for this transformation. Or you wrap the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had a brief look.
src/library/coverartcache.cpp
Outdated
@@ -48,6 +48,7 @@ CoverArtCache::~CoverArtCache() { | |||
QPixmap CoverArtCache::requestCover(const CoverInfo& requestInfo, | |||
const QObject* pRequestor, | |||
const int desiredWidth, | |||
const double devicePixelRatio, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an important info why the design is as it is now. Please add the comment to the code as well.
src/library/coverartdelegate.cpp
Outdated
QRect target(option.rect.x(), option.rect.y(), | ||
width, height); | ||
QRect source(0, 0, target.width(), target.height()); | ||
int width = math_min(pixmap.width(), option.rect.width()) * scaleFactor; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
math_min compares the actualWidth of the pimamp with the device independent width from opition. I think this is desired here, but needs a comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The pixmap
dimensions are measured in device coords, but what about option.rect
, source
, target
?
I agree with Daniel that this code seems to be incorrect, because it mixes up different measurements. The final multiplication scales the pixmap's coords up again.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After some experimentation I realized QPainter::drawPixmap(QRect, QPixmap, QRect)
wasn't working how I thought at all and it was also overcomplicated. The simpler QPainter::drawPixmap(QPoint, QPixmap)
works fine without any complicated size conversions.
src/library/coverartdelegate.cpp
Outdated
QRect source(0, 0, target.width(), target.height()); | ||
int width = math_min(pixmap.width(), option.rect.width()) * scaleFactor; | ||
int height = math_min(pixmap.height(), option.rect.height()) * scaleFactor; | ||
QRect target(option.rect.x(), option.rect.y(), width, height); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the original code is now outdated since we have non ^2 scale favtors.
Originally this code prevents us from upscaling an actual 15×15 to 20x20 for instance. This will likely look blurry. Instead we print the 15x15 and pad it for 20x20
This approach still makes sense, but we need to considder the scaleFactor.
If it is 1 or 2 the code works. If it is 1.5 the code fails.
20x20 is in this case 30x30 and the pixmap will look perfectly scaled by 2.
I must admit that this is a constructed example, but it is relevant.
69b2d74
to
2998cae
Compare
ccd039b
to
6810aa4
Compare
There ended up being commits that just reverted changes from earlier commits in this branch. Shall I rebase to condense this down to one commit? |
Yes, that's a good idea. |
6810aa4
to
17ce4c7
Compare
Ok, I'll check this PR asap. Unfortunately, I'm currently experiencing issues on Fedora 31: https://bugs.launchpad.net/mixxx/+bug/1850729 |
I guess the "resize to a integer scale" code is not working for HiDPI screens in coverartdelegate.cpp. See the outdated discussion above. |
Just some minor nitpicks, otherwise reasonable and concise. LGTM. |
I don't know what you mean. It looks fine on my screen, otherwise I would not have pushed the code. |
I tried to build this branch, but I'm getting errors: $ rm -rf lin64_build/ .scon* cache/ config.log *.plist scons.txt
$ scons -j16
[...]
In file included from src/controllers/dlgcontrollerlearning.cpp:12:
src/controllers/dlgcontrollerlearning.h:16:10: fatal error: controllers/ui_dlgcontrollerlearning.h: No such file or directory
16 | #include "controllers/ui_dlgcontrollerlearning.h"
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
compilation terminated.
scons: *** [lin64_build/controllers/dlgcontrollerlearning.o] Error 1
In file included from src/controllers/dlgprefcontroller.h:18,
from src/controllers/dlgprefcontroller.cpp:16:
src/controllers/dlgcontrollerlearning.h:16:10: fatal error: controllers/ui_dlgcontrollerlearning.h: No such file or directory
16 | #include "controllers/ui_dlgcontrollerlearning.h"
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
compilation terminated.
scons: *** [lin64_build/controllers/dlgprefcontroller.o] Error 1
scons: building terminated because of errors. |
@Holzhaus The 2.2.x build still requires Python 2. We didn't backport your Python 3 fixes. |
Can you also confirm the blurryness issue shown in #2247 (comment) |
No, but I don't know if I did it correctly. I opened the coverart display, and then made the window slightly larger. This branch is on the left, master is on the right: However, if I do not resize the cover art windows and keep them at their default size, the fonts on right one looks slightly better to read despite being more pixelated. Is that the issue what you're referring to? |
No, not exactly. It looks like you have discovered an other issue. My issue happens in the cover art column. I have just double checked the issue and all of my covers are big enough that the dicovered corner case is not that relevant. The issue that you have discovered is that the window size is scaled from the original pixel size to the display pixel size. With a FullHD screen the cover is displayed unscaled pixel by pixel. In your case it is upscaled using anti aliasing which adds some noise. Do you think it makes sence to open the window by default pixel correct? This means the full screen cover will be smaller by default on HighDPI screens. |
Is there no way to query the current scale factor from the application and then multiply it with the scale factor of the image, so that the actual size of the window on HighDPI screens stays the same? If that is not possible, I'd probably keep the current behavior. |
It is possible, but you cannot have both. I think we need to be clear of the use case of the DlgCoverArtFullSize. I think we orignally have displayed it to see the cover at 100 % pixel correct. If you have a device pixel ratio of 2, the cover will appear smaller that it will appear on a ratio 1 screen. If we just want to show the cover big, there is no reason to show a 50 x 50 cover smaller than a 500 x 500 cover. Currently we are stuck in the middle. The HiDPI screes see a anti aliased blurry cover, with no easy way to see it pixel accurate. Covers with less resolution are still displayed small, but also blurry. |
@@ -131,8 +132,9 @@ void DlgCoverArtFullSize::slotCoverFound(const QObject* pRequestor, | |||
dialogSize.scale(availableScreenSpace.width(), dialogSize.height(), | |||
Qt::KeepAspectRatio); | |||
} | |||
QPixmap resizedPixmap = m_pixmap.scaled(size(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The issue is in line 125 above, where the logical dialogSize is taken form the native pixel size.
Can you try to use
QSize dialogSize = m_pixmap.size() / getDevicePixelRatioF(this);
there?
@Be-ing: The full size view of the cover is the last open thing. This should be pixel exact as discussed above. From this size the user can resize to any size. |
In Mixxx 2.1, the cover art was displayed pixel exact without scaling on high DPI screens, so many covers were shown quite small. I prefer how it is here. I don't care that it isn't pixel exact and may have some minor aliasing artifacts. I did mind before when it was scaled twice and there were lots of artifacts. |
I don't think there is any problem here, so ready for merge? @Holzhaus what do you think? |
LGTM. I rely on the opinion of those that are affected by scaling issues. One option could be to scale the image up until at least one side of its dimensions is half the size of the corresponding screen dimension. Just an idea, does not need to be solved now. |
I don't think there is a pressing need to make the scaling any more complicated. This is DJ software, not a raster graphics editor. |
My remark was only intended to establish a sensible lower bound for the display to avoid diminishing small covers on HiDPI screens if you would decide to display the cover without scaling if possible. If we always apply a sensible scaling, then this distinction is not necessary. |
OK so we can merge. Thank you for clarifications. |
Before, cover art images were getting scaled down to logical/device independent/1x pixel scale then automatically scaled up by the screen scale ratio which made them look blocky. By using QPixmap/QImage::setDevicePixelRatio and drawing QPixmaps/QImages at sizes scaled by the device pixel ratio, the cover art images only get scaled once to the size they are really shown at on screen.
Unfortunately there is a regression that somehow makes only one CoverArtDelegate show a cover at a time. I tried increasing the cache size by a factor of 100 and that made no difference. I would appreciate any hints where the problem might be with this.
Before:
After:
Before:
After: