diff --git a/src/duckstation-qt/gamelistmodel.cpp b/src/duckstation-qt/gamelistmodel.cpp index cd6664165..76b8995cf 100644 --- a/src/duckstation-qt/gamelistmodel.cpp +++ b/src/duckstation-qt/gamelistmodel.cpp @@ -27,49 +27,42 @@ static constexpr int COVER_ART_HEIGHT = 512; static constexpr int COVER_ART_SPACING = 32; static constexpr int MIN_COVER_CACHE_SIZE = 256; -static int DPRScale(int size, float dpr) +static void resizeAndPadImage(QImage* image, int expected_width, int expected_height) { - return static_cast(static_cast(size) * dpr); -} - -static int DPRUnscale(int size, float dpr) -{ - return static_cast(static_cast(size) / dpr); -} - -static void resizeAndPadPixmap(QPixmap* pm, int expected_width, int expected_height, float dpr) -{ - const int dpr_expected_width = DPRScale(expected_width, dpr); - const int dpr_expected_height = DPRScale(expected_height, dpr); - if (pm->width() == dpr_expected_width && pm->height() == dpr_expected_height) + const qreal dpr = image->devicePixelRatio(); + const int dpr_expected_width = static_cast(static_cast(expected_width) * dpr); + const int dpr_expected_height = static_cast(static_cast(expected_height) * dpr); + if (image->width() == dpr_expected_width && image->height() == dpr_expected_height) return; - *pm = pm->scaled(dpr_expected_width, dpr_expected_height, Qt::KeepAspectRatio, Qt::SmoothTransformation); - if (pm->width() == dpr_expected_width && pm->height() == dpr_expected_height) + if (image->width() > image->height()) + *image = image->scaledToWidth(dpr_expected_width, Qt::SmoothTransformation); + else + *image = image->scaledToHeight(dpr_expected_height, Qt::SmoothTransformation); + + if (image->width() == dpr_expected_width && image->height() == dpr_expected_height) return; // QPainter works in unscaled coordinates. int xoffs = 0; int yoffs = 0; - if (pm->width() < dpr_expected_width) - xoffs = DPRUnscale((dpr_expected_width - pm->width()) / 2, dpr); - if (pm->height() < dpr_expected_height) - yoffs = DPRUnscale((dpr_expected_height - pm->height()) / 2, dpr); + if (image->width() < dpr_expected_width) + xoffs = static_cast(static_cast((dpr_expected_width - image->width()) / 2) / dpr); + if (image->height() < dpr_expected_height) + yoffs = static_cast(static_cast((dpr_expected_height - image->height()) / 2) / dpr); - QPixmap padded_image(dpr_expected_width, dpr_expected_height); + QImage padded_image(dpr_expected_width, dpr_expected_height, image->format()); padded_image.setDevicePixelRatio(dpr); padded_image.fill(Qt::transparent); QPainter painter; if (painter.begin(&padded_image)) { painter.setCompositionMode(QPainter::CompositionMode_Source); - painter.drawPixmap(xoffs, yoffs, *pm); - painter.setCompositionMode(QPainter::CompositionMode_Destination); - painter.fillRect(padded_image.rect(), QColor(0, 0, 0, 0)); + painter.drawImage(xoffs, yoffs, *image); painter.end(); } - *pm = padded_image; + *image = std::move(padded_image); } GameListCoverLoader::GameListCoverLoader(const GameList::Entry* ge, const QImage& placeholder_image, int width, @@ -92,7 +85,7 @@ void GameListCoverLoader::loadOrGenerateCover() if (!m_image.isNull()) { m_image.setDevicePixelRatio(m_dpr); - resizeAndPadImage(); + resizeAndPadImage(&m_image, m_width, m_height); } } @@ -103,9 +96,9 @@ void GameListCoverLoader::loadOrGenerateCover() // Can't create pixmaps on the worker thread, have to create it on the UI thread. QtHost::RunOnUIThread([this]() { if (!m_image.isNull()) - emit coverLoaded(m_path, QPixmap::fromImage(m_image)); + emit coverLoaded(m_path, m_image, m_scale); else - emit coverLoaded(m_path, QPixmap()); + emit coverLoaded(m_path, m_image, m_scale); delete this; }); } @@ -113,11 +106,10 @@ void GameListCoverLoader::loadOrGenerateCover() void GameListCoverLoader::createPlaceholderImage() { m_image = m_placeholder_image.copy(); - m_image.setDevicePixelRatio(m_dpr); if (m_image.isNull()) return; - resizeAndPadImage(); + resizeAndPadImage(&m_image, m_width, m_height); QPainter painter; if (painter.begin(&m_image)) @@ -134,39 +126,6 @@ void GameListCoverLoader::createPlaceholderImage() } } -void GameListCoverLoader::resizeAndPadImage() -{ - const int dpr_expected_width = DPRScale(m_width, m_dpr); - const int dpr_expected_height = DPRScale(m_height, m_dpr); - if (m_image.width() == dpr_expected_width && m_image.height() == dpr_expected_height) - return; - - m_image = m_image.scaled(dpr_expected_width, dpr_expected_height, Qt::KeepAspectRatio, Qt::SmoothTransformation); - if (m_image.width() == dpr_expected_width && m_image.height() == dpr_expected_height) - return; - - // QPainter works in unscaled coordinates. - int xoffs = 0; - int yoffs = 0; - if (m_image.width() < dpr_expected_width) - xoffs = DPRUnscale((dpr_expected_width - m_image.width()) / 2, m_dpr); - if (m_image.height() < dpr_expected_height) - yoffs = DPRUnscale((dpr_expected_height - m_image.height()) / 2, m_dpr); - - QPixmap padded_image(dpr_expected_width, dpr_expected_height); - padded_image.setDevicePixelRatio(m_dpr); - padded_image.fill(Qt::transparent); - QPainter painter; - if (painter.begin(&padded_image)) - { - painter.setCompositionMode(QPainter::CompositionMode_Source); - painter.drawImage(xoffs, yoffs, m_image); - painter.setCompositionMode(QPainter::CompositionMode_Destination); - painter.fillRect(padded_image.rect(), QColor(0, 0, 0, 0)); - painter.end(); - } -} - std::optional GameListModel::getColumnIdForName(std::string_view name) { for (int column = 0; column < Column_Count; column++) @@ -186,7 +145,7 @@ const char* GameListModel::getColumnName(Column col) GameListModel::GameListModel(float cover_scale, bool show_cover_titles, bool show_game_icons, QObject* parent /* = nullptr */) : QAbstractTableModel(parent), m_show_titles_for_covers(show_cover_titles), m_show_game_icons(show_game_icons), - m_memcard_pixmap_cache(128) + m_memcard_pixmap_cache(MIN_COVER_CACHE_SIZE) { loadCommonImages(); setCoverScale(cover_scale); @@ -198,11 +157,7 @@ GameListModel::GameListModel(float cover_scale, bool show_cover_titles, bool sho connect(g_emu_thread, &EmuThread::gameListRowsChanged, this, &GameListModel::rowsChanged); } -GameListModel::~GameListModel() -{ - // wait for all cover loads to finish, they're using m_placeholder_image - System::WaitForAllAsyncTasks(); -} +GameListModel::~GameListModel() = default; void GameListModel::setShowGameIcons(bool enabled) { @@ -222,15 +177,34 @@ void GameListModel::setCoverScale(float scale) m_cover_pixmap_cache.Clear(); m_cover_scale = scale; - if (m_loading_pixmap.load(QStringLiteral("%1/images/placeholder.png").arg(QtHost::GetResourcesBasePath()))) + + const qreal dpr = qApp->devicePixelRatio(); + + QImage loading_image; + if (loading_image.load(QStringLiteral("%1/images/placeholder.png").arg(QtHost::GetResourcesBasePath()))) { - m_loading_pixmap.setDevicePixelRatio(qApp->devicePixelRatio()); - resizeAndPadPixmap(&m_loading_pixmap, getCoverArtWidth(), getCoverArtHeight(), qApp->devicePixelRatio()); + loading_image.setDevicePixelRatio(dpr); + resizeAndPadImage(&loading_image, getCoverArtWidth(), getCoverArtHeight()); } else { - m_loading_pixmap = QPixmap(getCoverArtWidth(), getCoverArtHeight()); - m_loading_pixmap.fill(QColor(0, 0, 0, 0)); + loading_image = QImage(getCoverArtWidth(), getCoverArtHeight(), QImage::Format_RGB32); + loading_image.setDevicePixelRatio(dpr); + loading_image.fill(QColor(0, 0, 0, 0)); + } + m_loading_pixmap = QPixmap::fromImage(loading_image); + + m_placeholder_image = QImage(); + if (m_placeholder_image.load(QStringLiteral("%1/images/cover-placeholder.png").arg(QtHost::GetResourcesBasePath()))) + { + m_placeholder_image.setDevicePixelRatio(dpr); + resizeAndPadImage(&m_placeholder_image, getCoverArtWidth(), getCoverArtHeight()); + } + else + { + m_placeholder_image = QImage(getCoverArtWidth(), getCoverArtHeight(), QImage::Format_RGB32); + m_placeholder_image.setDevicePixelRatio(dpr); + m_placeholder_image.fill(QColor(0, 0, 0, 0)); } emit coverScaleChanged(); @@ -267,9 +241,17 @@ void GameListModel::loadOrGenerateCover(const GameList::Entry* ge) System::QueueAsyncTask([loader]() { loader->loadOrGenerateCover(); }); } -void GameListModel::coverLoaded(const std::string& path, const QPixmap& pixmap) +void GameListModel::coverLoaded(const std::string& path, const QImage& image, float scale) { - m_cover_pixmap_cache.Insert(path, pixmap); + // old request before cover scale change? + if (m_cover_scale != scale) + return; + + if (!image.isNull()) + m_cover_pixmap_cache.Insert(path, QPixmap::fromImage(image)); + else + m_cover_pixmap_cache.Insert(path, QPixmap()); + invalidateCoverForPath(path); } @@ -883,8 +865,6 @@ void GameListModel::loadCommonImages() QtUtils::GetIconForCompatibility(static_cast(i)).pixmap(96, 24); } - m_placeholder_image.load(QStringLiteral("%1/images/cover-placeholder.png").arg(QtHost::GetResourcesBasePath())); - constexpr int ACHIEVEMENT_ICON_SIZE = 16; m_no_achievements_pixmap = QIcon(QString::fromStdString(QtHost::GetResourcePath("images/trophy-icon-gray.svg", true))) .pixmap(ACHIEVEMENT_ICON_SIZE); diff --git a/src/duckstation-qt/gamelistmodel.h b/src/duckstation-qt/gamelistmodel.h index 4064ab40e..52d8bb591 100644 --- a/src/duckstation-qt/gamelistmodel.h +++ b/src/duckstation-qt/gamelistmodel.h @@ -93,7 +93,7 @@ Q_SIGNALS: void coverScaleChanged(); private Q_SLOTS: - void coverLoaded(const std::string& path, const QPixmap& pixmap); + void coverLoaded(const std::string& path, const QImage& image, float scale); void rowsChanged(const QList& rows); private: @@ -147,16 +147,15 @@ public: void loadOrGenerateCover(); Q_SIGNALS: - void coverLoaded(const std::string& path, const QPixmap& pixmap); + void coverLoaded(const std::string& path, const QImage& image, float scale); private: void createPlaceholderImage(); - void resizeAndPadImage(); std::string m_path; std::string m_serial; std::string m_title; - const QImage& m_placeholder_image; + QImage m_placeholder_image; int m_width; int m_height; float m_scale;