diff --git a/common/vsprops/QtCompile.props b/common/vsprops/QtCompile.props index a122d98699..987a6a12dc 100644 --- a/common/vsprops/QtCompile.props +++ b/common/vsprops/QtCompile.props @@ -32,7 +32,7 @@ $(QtLibDir);%(AdditionalLibraryDirectories) - Qt6Core$(QtLibSuffix).lib;Qt6Gui$(QtLibSuffix).lib;Qt6Widgets$(QtLibSuffix).lib;Qt6Network$(QtLibSuffix).lib;%(AdditionalDependencies) + Qt6Core$(QtLibSuffix).lib;Qt6Gui$(QtLibSuffix).lib;Qt6Widgets$(QtLibSuffix).lib;Qt6Network$(QtLibSuffix).lib;Qt6Concurrent$(QtLibSuffix).lib;%(AdditionalDependencies) @@ -118,7 +118,7 @@ - + diff --git a/pcsx2-qt/GameList/GameListModel.cpp b/pcsx2-qt/GameList/GameListModel.cpp index 17a12ded37..009e22c6a2 100644 --- a/pcsx2-qt/GameList/GameListModel.cpp +++ b/pcsx2-qt/GameList/GameListModel.cpp @@ -24,6 +24,9 @@ #include "fmt/format.h" #include #include +#include +#include +#include #include #include #include @@ -34,6 +37,7 @@ static constexpr std::array s_column_n static constexpr int COVER_ART_WIDTH = 350; 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) { @@ -125,31 +129,111 @@ const char* GameListModel::getColumnName(Column col) GameListModel::GameListModel(QObject* parent /* = nullptr */) : QAbstractTableModel(parent) + , m_cover_pixmap_cache(MIN_COVER_CACHE_SIZE) { loadCommonImages(); setColumnDisplayNames(); } GameListModel::~GameListModel() = default; +void GameListModel::refreshImages() +{ + loadCommonImages(); + refresh(); +} + void GameListModel::setCoverScale(float scale) { if (m_cover_scale == scale) return; - m_cover_pixmap_cache.clear(); + m_cover_pixmap_cache.Clear(); m_cover_scale = scale; + m_cover_scale_counter.fetch_add(1, std::memory_order_release); + m_loading_pixmap = QPixmap(getCoverArtWidth(), getCoverArtHeight()); + m_loading_pixmap.fill(QColor(0, 0, 0, 0)); } void GameListModel::refreshCovers() { - m_cover_pixmap_cache.clear(); + m_cover_pixmap_cache.Clear(); refresh(); } -void GameListModel::refreshImages() +void GameListModel::updateCacheSize(int width, int height) { - loadCommonImages(); - refresh(); + // This is a bit conversative, since it doesn't consider padding, but better to be over than under. + const int cover_width = getCoverArtWidth(); + const int cover_height = getCoverArtHeight(); + const int num_columns = ((width + (cover_width - 1)) / cover_width); + const int num_rows = ((height + (cover_height - 1)) / cover_height); + m_cover_pixmap_cache.SetMaxCapacity(static_cast(std::max(num_columns * num_rows, MIN_COVER_CACHE_SIZE))); +} + +void GameListModel::loadOrGenerateCover(const GameList::Entry* ge) +{ + // Why this counter: Every time we change the cover scale, we increment the counter variable. This way if the scale is changed + // while there's outstanding jobs, the old jobs won't proceed (at the wrong size), or get added into the grid. + const u32 counter = m_cover_scale_counter.load(std::memory_order_acquire); + + QFuture future = QtConcurrent::run([this, path = ge->path, title = ge->title, serial = ge->serial, counter]()->QPixmap { + QPixmap image; + if (m_cover_scale_counter.load(std::memory_order_acquire) == counter) + { + const std::string cover_path(GameList::GetCoverImagePath(path, serial, title)); + if (!cover_path.empty()) + { + const float dpr = qApp->devicePixelRatio(); + image = QPixmap(QString::fromStdString(cover_path)); + if (!image.isNull()) + { + image.setDevicePixelRatio(dpr); + resizeAndPadPixmap(&image, getCoverArtWidth(), getCoverArtHeight(), dpr); + } + } + } + + if (image.isNull()) + image = createPlaceholderImage(m_placeholder_pixmap, getCoverArtWidth(), getCoverArtHeight(), m_cover_scale, title); + + if (m_cover_scale_counter.load(std::memory_order_acquire) != counter) + image = {}; + + return image; + }); + + // Context must be 'this' so we run on the UI thread. + future.then(this, [this, path = ge->path, counter](QPixmap pm) { + if (m_cover_scale_counter.load(std::memory_order_acquire) != counter) + return; + + m_cover_pixmap_cache.Insert(std::move(path), std::move(pm)); + invalidateCoverForPath(path); + }); +} + +void GameListModel::invalidateCoverForPath(const std::string& path) +{ + // This isn't ideal, but not sure how else we can get the row, when it might change while scanning... + auto lock = GameList::GetLock(); + const u32 count = GameList::GetEntryCount(); + std::optional row; + for (u32 i = 0; i < count; i++) + { + if (GameList::GetEntryByIndex(i)->path == path) + { + row = i; + break; + } + } + if (!row.has_value()) + { + // Game removed? + return; + } + + const QModelIndex mi(index(static_cast(row.value()), Column_Cover)); + emit dataChanged(mi, mi, {Qt::DecorationRole}); } int GameListModel::getCoverArtWidth() const @@ -289,31 +373,14 @@ QVariant GameListModel::data(const QModelIndex& index, int role) const case Column_Cover: { - auto it = m_cover_pixmap_cache.find(ge->path); - if (it != m_cover_pixmap_cache.end()) - return it->second; + QPixmap* pm = m_cover_pixmap_cache.Lookup(ge->path); + if (pm) + return *pm; - QPixmap image; - std::string path = GameList::GetCoverImagePathForEntry(ge); - if (!path.empty()) - { - const float dpr = qApp->devicePixelRatio(); - image = QPixmap(QString::fromStdString(path)); - if (!image.isNull()) - { - image.setDevicePixelRatio(dpr); - resizeAndPadPixmap(&image, getCoverArtWidth(), getCoverArtHeight(), dpr); - } - } - - if (image.isNull()) - { - image = createPlaceholderImage(m_placeholder_pixmap, getCoverArtWidth(), getCoverArtHeight(), m_cover_scale, - ge->title); - } - - m_cover_pixmap_cache.emplace(ge->path, image); - return image; + // We insert the placeholder into the cache, so that we don't repeatedly + // queue loading jobs for this game. + const_cast(this)->loadOrGenerateCover(ge); + return *m_cover_pixmap_cache.Insert(ge->path, m_loading_pixmap); } break; @@ -478,6 +545,7 @@ void GameListModel::loadCommonImages() m_compatibility_pixmaps[i].load(QStringLiteral("%1/icons/star-%2.png").arg(base_path).arg(i - 1)); m_placeholder_pixmap.load(QStringLiteral("%1/cover-placeholder.png").arg(base_path)); + setCoverScale(1.0f); } void GameListModel::setColumnDisplayNames() diff --git a/pcsx2-qt/GameList/GameListModel.h b/pcsx2-qt/GameList/GameListModel.h index 5e3d2e4d3b..12a153b4b5 100644 --- a/pcsx2-qt/GameList/GameListModel.h +++ b/pcsx2-qt/GameList/GameListModel.h @@ -15,9 +15,11 @@ #pragma once #include "pcsx2/Frontend/GameList.h" +#include "common/LRUCache.h" #include #include #include +#include #include #include #include @@ -59,6 +61,7 @@ public: __fi const QString& getColumnDisplayName(int column) { return m_column_display_names[column]; } void refresh(); + void refreshImages(); bool titlesLessThan(int left_row, int right_row) const; @@ -73,20 +76,24 @@ public: int getCoverArtHeight() const; int getCoverArtSpacing() const; void refreshCovers(); - void refreshImages(); + void updateCacheSize(int width, int height); private: void loadCommonImages(); void setColumnDisplayNames(); + void loadOrGenerateCover(const GameList::Entry* ge); + void invalidateCoverForPath(const std::string& path); - float m_cover_scale = 1.0f; + float m_cover_scale = 0.0f; + std::atomic m_cover_scale_counter{0}; bool m_show_titles_for_covers = false; std::array m_column_display_names; std::array(GameList::EntryType::Count)> m_type_pixmaps; std::array(GameList::Region::Count)> m_region_pixmaps; QPixmap m_placeholder_pixmap; + QPixmap m_loading_pixmap; std::array(GameList::CompatibilityRatingCount)> m_compatibility_pixmaps; - mutable std::unordered_map m_cover_pixmap_cache; -}; \ No newline at end of file + mutable LRUCache m_cover_pixmap_cache; +}; diff --git a/pcsx2-qt/GameList/GameListWidget.cpp b/pcsx2-qt/GameList/GameListWidget.cpp index f014b9e499..99ae51b626 100644 --- a/pcsx2-qt/GameList/GameListWidget.cpp +++ b/pcsx2-qt/GameList/GameListWidget.cpp @@ -111,6 +111,7 @@ void GameListWidget::initialize() m_model = new GameListModel(this); m_model->setCoverScale(Host::GetBaseFloatSettingValue("UI", "GameListCoverArtScale", 0.45f)); m_model->setShowCoverTitles(Host::GetBaseBoolSettingValue("UI", "GameListShowCoverTitles", true)); + m_model->updateCacheSize(width(), height()); m_sort_model = new GameListSortModel(m_model); m_sort_model->setSourceModel(m_model); @@ -348,10 +349,9 @@ void GameListWidget::listZoom(float delta) const float new_scale = std::clamp(m_model->getCoverScale() + delta, MIN_SCALE, MAX_SCALE); QtHost::SetBaseFloatSettingValue("UI", "GameListCoverArtScale", new_scale); m_model->setCoverScale(new_scale); + m_model->updateCacheSize(width(), height()); updateListFont(); updateToolbar(); - - m_model->refresh(); } void GameListWidget::gridZoomIn() @@ -370,10 +370,9 @@ void GameListWidget::gridIntScale(int int_scale) QtHost::SetBaseFloatSettingValue("UI", "GameListCoverArtScale", new_scale); m_model->setCoverScale(new_scale); + m_model->updateCacheSize(width(), height()); updateListFont(); updateToolbar(); - - m_model->refresh(); } void GameListWidget::refreshGridCovers() @@ -460,6 +459,7 @@ void GameListWidget::resizeEvent(QResizeEvent* event) { QWidget::resizeEvent(event); resizeTableViewColumnsToFit(); + m_model->updateCacheSize(width(), height()); } void GameListWidget::resizeTableViewColumnsToFit()