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()