Qt: Asynchronous loading of covers

This commit is contained in:
Connor McLaughlin 2022-07-17 15:24:59 +10:00 committed by refractionpcsx2
parent cbcfe37e28
commit be26c04dd1
4 changed files with 114 additions and 39 deletions

View File

@ -32,7 +32,7 @@
</ClCompile> </ClCompile>
<Link> <Link>
<AdditionalLibraryDirectories>$(QtLibDir);%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories> <AdditionalLibraryDirectories>$(QtLibDir);%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>Qt6Core$(QtLibSuffix).lib;Qt6Gui$(QtLibSuffix).lib;Qt6Widgets$(QtLibSuffix).lib;Qt6Network$(QtLibSuffix).lib;%(AdditionalDependencies)</AdditionalDependencies> <AdditionalDependencies>Qt6Core$(QtLibSuffix).lib;Qt6Gui$(QtLibSuffix).lib;Qt6Widgets$(QtLibSuffix).lib;Qt6Network$(QtLibSuffix).lib;Qt6Concurrent$(QtLibSuffix).lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link> </Link>
</ItemDefinitionGroup> </ItemDefinitionGroup>
@ -118,7 +118,7 @@
<!--Copy the needed dlls--> <!--Copy the needed dlls-->
<ItemGroup> <ItemGroup>
<QtLibNames Include="Qt6Core$(QtLibSuffix);Qt6Gui$(QtLibSuffix);Qt6Widgets$(QtLibSuffix);Qt6Network$(QtLibSuffix);Qt6Svg$(QtLibSuffix)" /> <QtLibNames Include="Qt6Core$(QtLibSuffix);Qt6Gui$(QtLibSuffix);Qt6Widgets$(QtLibSuffix);Qt6Network$(QtLibSuffix);Qt6Svg$(QtLibSuffix);Qt6Concurrent$(QtLibSuffix)" />
<QtDlls Include="@(QtLibNames -> '$(QtBinDir)%(Identity).dll')" /> <QtDlls Include="@(QtLibNames -> '$(QtBinDir)%(Identity).dll')" />
<!--Filter plugins to copy based on the observation that all debug versions end in "d"--> <!--Filter plugins to copy based on the observation that all debug versions end in "d"-->
<QtAllPlugins Include="$(QtPluginsDir)**\*$(QtLibSuffix).dll" /> <QtAllPlugins Include="$(QtPluginsDir)**\*$(QtLibSuffix).dll" />

View File

@ -24,6 +24,9 @@
#include "fmt/format.h" #include "fmt/format.h"
#include <QtCore/QDate> #include <QtCore/QDate>
#include <QtCore/QDateTime> #include <QtCore/QDateTime>
#include <QtCore/QFuture>
#include <QtCore/QFutureWatcher>
#include <QtConcurrent/QtConcurrent>
#include <QtGui/QGuiApplication> #include <QtGui/QGuiApplication>
#include <QtGui/QIcon> #include <QtGui/QIcon>
#include <QtGui/QPainter> #include <QtGui/QPainter>
@ -34,6 +37,7 @@ static constexpr std::array<const char*, GameListModel::Column_Count> s_column_n
static constexpr int COVER_ART_WIDTH = 350; static constexpr int COVER_ART_WIDTH = 350;
static constexpr int COVER_ART_HEIGHT = 512; static constexpr int COVER_ART_HEIGHT = 512;
static constexpr int COVER_ART_SPACING = 32; static constexpr int COVER_ART_SPACING = 32;
static constexpr int MIN_COVER_CACHE_SIZE = 256;
static int DPRScale(int size, float dpr) static int DPRScale(int size, float dpr)
{ {
@ -125,31 +129,111 @@ const char* GameListModel::getColumnName(Column col)
GameListModel::GameListModel(QObject* parent /* = nullptr */) GameListModel::GameListModel(QObject* parent /* = nullptr */)
: QAbstractTableModel(parent) : QAbstractTableModel(parent)
, m_cover_pixmap_cache(MIN_COVER_CACHE_SIZE)
{ {
loadCommonImages(); loadCommonImages();
setColumnDisplayNames(); setColumnDisplayNames();
} }
GameListModel::~GameListModel() = default; GameListModel::~GameListModel() = default;
void GameListModel::refreshImages()
{
loadCommonImages();
refresh();
}
void GameListModel::setCoverScale(float scale) void GameListModel::setCoverScale(float scale)
{ {
if (m_cover_scale == scale) if (m_cover_scale == scale)
return; return;
m_cover_pixmap_cache.clear(); m_cover_pixmap_cache.Clear();
m_cover_scale = scale; 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() void GameListModel::refreshCovers()
{ {
m_cover_pixmap_cache.clear(); m_cover_pixmap_cache.Clear();
refresh(); refresh();
} }
void GameListModel::refreshImages() void GameListModel::updateCacheSize(int width, int height)
{ {
loadCommonImages(); // This is a bit conversative, since it doesn't consider padding, but better to be over than under.
refresh(); 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<int>(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<QPixmap> 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<u32> 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<int>(row.value()), Column_Cover));
emit dataChanged(mi, mi, {Qt::DecorationRole});
} }
int GameListModel::getCoverArtWidth() const int GameListModel::getCoverArtWidth() const
@ -289,31 +373,14 @@ QVariant GameListModel::data(const QModelIndex& index, int role) const
case Column_Cover: case Column_Cover:
{ {
auto it = m_cover_pixmap_cache.find(ge->path); QPixmap* pm = m_cover_pixmap_cache.Lookup(ge->path);
if (it != m_cover_pixmap_cache.end()) if (pm)
return it->second; return *pm;
QPixmap image; // We insert the placeholder into the cache, so that we don't repeatedly
std::string path = GameList::GetCoverImagePathForEntry(ge); // queue loading jobs for this game.
if (!path.empty()) const_cast<GameListModel*>(this)->loadOrGenerateCover(ge);
{ return *m_cover_pixmap_cache.Insert(ge->path, m_loading_pixmap);
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;
} }
break; 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_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)); m_placeholder_pixmap.load(QStringLiteral("%1/cover-placeholder.png").arg(base_path));
setCoverScale(1.0f);
} }
void GameListModel::setColumnDisplayNames() void GameListModel::setColumnDisplayNames()

View File

@ -15,9 +15,11 @@
#pragma once #pragma once
#include "pcsx2/Frontend/GameList.h" #include "pcsx2/Frontend/GameList.h"
#include "common/LRUCache.h"
#include <QtCore/QAbstractTableModel> #include <QtCore/QAbstractTableModel>
#include <QtGui/QPixmap> #include <QtGui/QPixmap>
#include <algorithm> #include <algorithm>
#include <atomic>
#include <array> #include <array>
#include <optional> #include <optional>
#include <unordered_map> #include <unordered_map>
@ -59,6 +61,7 @@ public:
__fi const QString& getColumnDisplayName(int column) { return m_column_display_names[column]; } __fi const QString& getColumnDisplayName(int column) { return m_column_display_names[column]; }
void refresh(); void refresh();
void refreshImages();
bool titlesLessThan(int left_row, int right_row) const; bool titlesLessThan(int left_row, int right_row) const;
@ -73,20 +76,24 @@ public:
int getCoverArtHeight() const; int getCoverArtHeight() const;
int getCoverArtSpacing() const; int getCoverArtSpacing() const;
void refreshCovers(); void refreshCovers();
void refreshImages(); void updateCacheSize(int width, int height);
private: private:
void loadCommonImages(); void loadCommonImages();
void setColumnDisplayNames(); 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<u32> m_cover_scale_counter{0};
bool m_show_titles_for_covers = false; bool m_show_titles_for_covers = false;
std::array<QString, Column_Count> m_column_display_names; std::array<QString, Column_Count> m_column_display_names;
std::array<QPixmap, static_cast<u32>(GameList::EntryType::Count)> m_type_pixmaps; std::array<QPixmap, static_cast<u32>(GameList::EntryType::Count)> m_type_pixmaps;
std::array<QPixmap, static_cast<u32>(GameList::Region::Count)> m_region_pixmaps; std::array<QPixmap, static_cast<u32>(GameList::Region::Count)> m_region_pixmaps;
QPixmap m_placeholder_pixmap; QPixmap m_placeholder_pixmap;
QPixmap m_loading_pixmap;
std::array<QPixmap, static_cast<int>(GameList::CompatibilityRatingCount)> m_compatibility_pixmaps; std::array<QPixmap, static_cast<int>(GameList::CompatibilityRatingCount)> m_compatibility_pixmaps;
mutable std::unordered_map<std::string, QPixmap> m_cover_pixmap_cache; mutable LRUCache<std::string, QPixmap> m_cover_pixmap_cache;
}; };

View File

@ -111,6 +111,7 @@ void GameListWidget::initialize()
m_model = new GameListModel(this); m_model = new GameListModel(this);
m_model->setCoverScale(Host::GetBaseFloatSettingValue("UI", "GameListCoverArtScale", 0.45f)); m_model->setCoverScale(Host::GetBaseFloatSettingValue("UI", "GameListCoverArtScale", 0.45f));
m_model->setShowCoverTitles(Host::GetBaseBoolSettingValue("UI", "GameListShowCoverTitles", true)); m_model->setShowCoverTitles(Host::GetBaseBoolSettingValue("UI", "GameListShowCoverTitles", true));
m_model->updateCacheSize(width(), height());
m_sort_model = new GameListSortModel(m_model); m_sort_model = new GameListSortModel(m_model);
m_sort_model->setSourceModel(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); const float new_scale = std::clamp(m_model->getCoverScale() + delta, MIN_SCALE, MAX_SCALE);
QtHost::SetBaseFloatSettingValue("UI", "GameListCoverArtScale", new_scale); QtHost::SetBaseFloatSettingValue("UI", "GameListCoverArtScale", new_scale);
m_model->setCoverScale(new_scale); m_model->setCoverScale(new_scale);
m_model->updateCacheSize(width(), height());
updateListFont(); updateListFont();
updateToolbar(); updateToolbar();
m_model->refresh();
} }
void GameListWidget::gridZoomIn() void GameListWidget::gridZoomIn()
@ -370,10 +370,9 @@ void GameListWidget::gridIntScale(int int_scale)
QtHost::SetBaseFloatSettingValue("UI", "GameListCoverArtScale", new_scale); QtHost::SetBaseFloatSettingValue("UI", "GameListCoverArtScale", new_scale);
m_model->setCoverScale(new_scale); m_model->setCoverScale(new_scale);
m_model->updateCacheSize(width(), height());
updateListFont(); updateListFont();
updateToolbar(); updateToolbar();
m_model->refresh();
} }
void GameListWidget::refreshGridCovers() void GameListWidget::refreshGridCovers()
@ -460,6 +459,7 @@ void GameListWidget::resizeEvent(QResizeEvent* event)
{ {
QWidget::resizeEvent(event); QWidget::resizeEvent(event);
resizeTableViewColumnsToFit(); resizeTableViewColumnsToFit();
m_model->updateCacheSize(width(), height());
} }
void GameListWidget::resizeTableViewColumnsToFit() void GameListWidget::resizeTableViewColumnsToFit()