From 0867decc876591599689541dc6724bc640c5cffb Mon Sep 17 00:00:00 2001 From: Stenzek Date: Mon, 1 Sep 2025 20:15:39 +1000 Subject: [PATCH] Qt: Use widget-local device pixel ratio for game list Fixes blurry icons in mixed DPI environments. At least on Windows. --- src/duckstation-qt/gamelistwidget.cpp | 92 ++++++++++++++++----------- src/duckstation-qt/gamelistwidget.h | 9 ++- 2 files changed, 60 insertions(+), 41 deletions(-) diff --git a/src/duckstation-qt/gamelistwidget.cpp b/src/duckstation-qt/gamelistwidget.cpp index da125f5b5..636f69bdd 100644 --- a/src/duckstation-qt/gamelistwidget.cpp +++ b/src/duckstation-qt/gamelistwidget.cpp @@ -132,8 +132,9 @@ const char* GameListModel::getColumnName(Column col) return s_column_names[static_cast(col)]; } -GameListModel::GameListModel(QObject* parent) - : QAbstractTableModel(parent), m_memcard_pixmap_cache(MIN_COVER_CACHE_SIZE) +GameListModel::GameListModel(GameListWidget* parent) + : QAbstractTableModel(parent), m_device_pixel_ratio(QtUtils::GetDevicePixelRatioForWidget(parent)), + m_memcard_pixmap_cache(MIN_COVER_CACHE_SIZE) { m_cover_scale = Host::GetBaseFloatSettingValue("UI", "GameListCoverArtScale", 0.45f); m_icon_size = Host::GetBaseFloatSettingValue("UI", "GameListIconSize", MIN_ICON_SIZE); @@ -222,18 +223,16 @@ void GameListModel::updateCoverScale() { m_cover_pixmap_cache.Clear(); - const qreal dpr = qApp->devicePixelRatio(); - QImage loading_image; if (loading_image.load(QStringLiteral("%1/images/placeholder.png").arg(QtHost::GetResourcesBasePath()))) { - loading_image.setDevicePixelRatio(dpr); + loading_image.setDevicePixelRatio(m_device_pixel_ratio); resizeAndPadImage(&loading_image, getCoverArtSize(), getCoverArtSize(), false); } else { loading_image = QImage(getCoverArtSize(), getCoverArtSize(), QImage::Format_RGB32); - loading_image.setDevicePixelRatio(dpr); + loading_image.setDevicePixelRatio(m_device_pixel_ratio); loading_image.fill(QColor(0, 0, 0, 0)); } m_loading_pixmap = QPixmap::fromImage(loading_image); @@ -241,13 +240,13 @@ void GameListModel::updateCoverScale() m_placeholder_image = QImage(); if (m_placeholder_image.load(QStringLiteral("%1/images/cover-placeholder.png").arg(QtHost::GetResourcesBasePath()))) { - m_placeholder_image.setDevicePixelRatio(dpr); + m_placeholder_image.setDevicePixelRatio(m_device_pixel_ratio); resizeAndPadImage(&m_placeholder_image, getCoverArtSize(), getCoverArtSize(), false); } else { m_placeholder_image = QImage(getCoverArtSize(), getCoverArtSize(), QImage::Format_RGB32); - m_placeholder_image.setDevicePixelRatio(dpr); + m_placeholder_image.setDevicePixelRatio(m_device_pixel_ratio); m_placeholder_image.fill(QColor(0, 0, 0, 0)); } @@ -270,6 +269,20 @@ void GameListModel::updateCacheSize(int num_rows, int num_columns) m_cover_pixmap_cache.SetMaxCapacity(static_cast(std::max(num_items, MIN_COVER_CACHE_SIZE))); } +void GameListModel::setDevicePixelRatio(qreal dpr) +{ + if (m_device_pixel_ratio == dpr) + return; + + WARNING_LOG("NEW DPR {}", dpr); + m_device_pixel_ratio = dpr; + m_placeholder_image.setDevicePixelRatio(dpr); + m_loading_pixmap.setDevicePixelRatio(dpr); + loadCommonImages(); + refreshCovers(); + refreshIcons(); +} + void GameListModel::reloadThemeSpecificImages() { loadSizeDependentPixmaps(); @@ -281,8 +294,7 @@ void GameListModel::loadOrGenerateCover(const GameList::Entry* ge) QtAsyncTask::create(this, [path = ge->path, serial = ge->serial, save_title = std::string(ge->GetSaveTitle()), display_title = QtUtils::StringViewToQString(ge->GetDisplayTitle(m_show_localized_titles)), placeholder_image = m_placeholder_image, list = this, width = getCoverArtSize(), - height = getCoverArtSize(), scale = m_cover_scale, - dpr = qApp->devicePixelRatio()]() mutable { + height = getCoverArtSize(), scale = m_cover_scale, dpr = m_device_pixel_ratio]() mutable { QImage image; loadOrGenerateCover(image, placeholder_image, width, height, scale, dpr, path, serial, save_title, display_title); return [path = std::move(path), image = std::move(image), list, scale]() { list->coverLoaded(path, image, scale); }; @@ -425,7 +437,19 @@ const QPixmap& GameListModel::getIconPixmapForEntry(const GameList::Entry* ge) c QPixmap pm; if (!path.empty() && pm.load(QString::fromStdString(path))) { - const_cast(this)->fixIconPixmapSize(pm); + const int pm_width = pm.width(); + const int pm_height = pm.height(); + + const qreal scale = + (static_cast(m_icon_size) / static_cast(MEMORY_CARD_ICON_SIZE)) * m_device_pixel_ratio; + const int scaled_pm_width = static_cast(static_cast(pm_width) * scale); + const int scaled_pm_height = static_cast(static_cast(pm_height) * scale); + + if (pm_width != scaled_pm_width || pm_height != scaled_pm_height) + QtUtils::ResizeSharpBilinear(pm, std::max(scaled_pm_width, scaled_pm_height), MEMORY_CARD_ICON_SIZE); + + pm.setDevicePixelRatio(m_device_pixel_ratio); + return *m_memcard_pixmap_cache.Insert(ge->serial, std::move(pm)); } @@ -484,22 +508,6 @@ QIcon GameListModel::getIconForGame(const QString& path) return ret; } -void GameListModel::fixIconPixmapSize(QPixmap& pm) -{ - const float dpr = qApp->devicePixelRatio(); - const int width = static_cast(static_cast(pm.width())); - const int height = static_cast(static_cast(pm.height())); - - const qreal scale = (static_cast(m_icon_size) / static_cast(MEMORY_CARD_ICON_SIZE)) * dpr; - const int new_width = static_cast(static_cast(width) * scale); - const int new_height = static_cast(static_cast(height) * scale); - - if (width != new_width || height != new_height) - QtUtils::ResizeSharpBilinear(pm, std::max(new_width, new_height), MEMORY_CARD_ICON_SIZE); - - pm.setDevicePixelRatio(dpr); -} - int GameListModel::getCoverArtSize() const { return std::max(static_cast(static_cast(COVER_ART_SIZE) * m_cover_scale), 1); @@ -1009,9 +1017,12 @@ bool GameListModel::lessThan(const GameList::Entry* left, const GameList::Entry* void GameListModel::loadSizeDependentPixmaps() { // nasty magic number here, +8 gets us a height of 24 at 16 icon size, which looks good. - const int icon_height = m_icon_size + 8; + const QSize icon_size = QSize(m_icon_size + 8, m_icon_size + 8); for (u32 i = 0; i < static_cast(GameList::EntryType::MaxCount); i++) - m_type_pixmaps[i] = QtUtils::GetIconForEntryType(static_cast(i)).pixmap(icon_height); + { + m_type_pixmaps[i] = + QtUtils::GetIconForEntryType(static_cast(i)).pixmap(icon_size, m_device_pixel_ratio); + } } void GameListModel::loadCommonImages() @@ -1020,18 +1031,18 @@ void GameListModel::loadCommonImages() for (u32 i = 0; i < static_cast(GameDatabase::CompatibilityRating::Count); i++) { - m_compatibility_pixmaps[i] = - QtUtils::GetIconForCompatibility(static_cast(i)).pixmap(96, 24); + m_compatibility_pixmaps[i] = QtUtils::GetIconForCompatibility(static_cast(i)) + .pixmap(QSize(96, 24), m_device_pixel_ratio); } - constexpr int ACHIEVEMENT_ICON_SIZE = 16; + constexpr QSize ACHIEVEMENT_ICON_SIZE(16, 16); m_no_achievements_pixmap = QIcon(QString::fromStdString(QtHost::GetResourcePath("images/trophy-icon-gray.svg", true))) - .pixmap(ACHIEVEMENT_ICON_SIZE); + .pixmap(ACHIEVEMENT_ICON_SIZE, m_device_pixel_ratio); m_has_achievements_pixmap = QIcon(QString::fromStdString(QtHost::GetResourcePath("images/trophy-icon.svg", true))) - .pixmap(ACHIEVEMENT_ICON_SIZE); + .pixmap(ACHIEVEMENT_ICON_SIZE, m_device_pixel_ratio); m_mastered_achievements_pixmap = QIcon(QString::fromStdString(QtHost::GetResourcePath("images/trophy-icon-star.svg", true))) - .pixmap(ACHIEVEMENT_ICON_SIZE); + .pixmap(ACHIEVEMENT_ICON_SIZE, m_device_pixel_ratio); } void GameListModel::setColumnDisplayNames() @@ -1679,10 +1690,15 @@ void GameListWidget::onIconSizeChanged(int size) onScaleChanged(); } -void GameListWidget::resizeEvent(QResizeEvent* event) +bool GameListWidget::event(QEvent* e) { - QWidget::resizeEvent(event); - updateBackground(false); + const QEvent::Type type = e->type(); + if (type == QEvent::Resize) + updateBackground(false); + else if (type == QEvent::DevicePixelRatioChange) + m_model->setDevicePixelRatio(QtUtils::GetDevicePixelRatioForWidget(this)); + + return QWidget::event(e); } const GameList::Entry* GameListWidget::getSelectedEntry() const diff --git a/src/duckstation-qt/gamelistwidget.h b/src/duckstation-qt/gamelistwidget.h index 952279aa2..8603b4b0b 100644 --- a/src/duckstation-qt/gamelistwidget.h +++ b/src/duckstation-qt/gamelistwidget.h @@ -62,7 +62,7 @@ public: static std::optional getColumnIdForName(std::string_view name); static const char* getColumnName(Column col); - explicit GameListModel(QObject* parent); + explicit GameListModel(GameListWidget* parent); ~GameListModel(); int rowCount(const QModelIndex& parent = QModelIndex()) const override; @@ -108,6 +108,8 @@ public: void refreshCovers(); void updateCacheSize(int num_rows, int num_columns); + void setDevicePixelRatio(qreal dpr); + Q_SIGNALS: void coverScaleChanged(float scale); void iconSizeChanged(int size); @@ -132,7 +134,8 @@ private: const QPixmap& getIconPixmapForEntry(const GameList::Entry* ge) const; const QPixmap& getFlagPixmapForEntry(const GameList::Entry* ge) const; - void fixIconPixmapSize(QPixmap& pm); + + qreal m_device_pixel_ratio = 1.0; std::optional m_taken_entries; @@ -282,7 +285,7 @@ public Q_SLOTS: void focusSearchWidget(); protected: - void resizeEvent(QResizeEvent* event); + bool event(QEvent* e) override; private: void setViewMode(int stack_index);