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>
<Link>
<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>
</ItemDefinitionGroup>
@ -118,7 +118,7 @@
<!--Copy the needed dlls-->
<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')" />
<!--Filter plugins to copy based on the observation that all debug versions end in "d"-->
<QtAllPlugins Include="$(QtPluginsDir)**\*$(QtLibSuffix).dll" />

View File

@ -24,6 +24,9 @@
#include "fmt/format.h"
#include <QtCore/QDate>
#include <QtCore/QDateTime>
#include <QtCore/QFuture>
#include <QtCore/QFutureWatcher>
#include <QtConcurrent/QtConcurrent>
#include <QtGui/QGuiApplication>
#include <QtGui/QIcon>
#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_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<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
@ -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<GameListModel*>(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()

View File

@ -15,9 +15,11 @@
#pragma once
#include "pcsx2/Frontend/GameList.h"
#include "common/LRUCache.h"
#include <QtCore/QAbstractTableModel>
#include <QtGui/QPixmap>
#include <algorithm>
#include <atomic>
#include <array>
#include <optional>
#include <unordered_map>
@ -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<u32> m_cover_scale_counter{0};
bool m_show_titles_for_covers = false;
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::Region::Count)> m_region_pixmaps;
QPixmap m_placeholder_pixmap;
QPixmap m_loading_pixmap;
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->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()