Skip to content

Commit

Permalink
Qt: Reduce game list jank after shutting down VM
Browse files Browse the repository at this point in the history
Prevents progress bar briefly appearing, and the list scrolling to the
top when you exit a game.
  • Loading branch information
stenzek committed Aug 2, 2024
1 parent 3a83c42 commit 9a626ca
Show file tree
Hide file tree
Showing 8 changed files with 98 additions and 27 deletions.
17 changes: 12 additions & 5 deletions src/core/game_list.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -119,15 +119,15 @@ static std::string GetCustomPropertiesFile();
static FileSystem::ManagedCFilePtr OpenMemoryCardTimestampCache(bool for_write);
static bool UpdateMemcardTimestampCache(const MemcardTimestampCacheEntry& entry);

} // namespace GameList

static std::vector<GameList::Entry> s_entries;
static EntryList s_entries;
static std::recursive_mutex s_mutex;
static GameList::CacheMap s_cache_map;
static std::vector<GameList::MemcardTimestampCacheEntry> s_memcard_timestamp_cache_entries;
static CacheMap s_cache_map;
static std::vector<MemcardTimestampCacheEntry> s_memcard_timestamp_cache_entries;

static bool s_game_list_loaded = false;

} // namespace GameList

const char* GameList::GetEntryTypeName(EntryType type)
{
static std::array<const char*, static_cast<int>(EntryType::Count)> names = {
Expand Down Expand Up @@ -823,6 +823,13 @@ void GameList::Refresh(bool invalidate_cache, bool only_cache, ProgressCallback*
CreateDiscSetEntries(played_time);
}

GameList::EntryList GameList::TakeEntryList()
{
EntryList ret = std::move(s_entries);
s_entries = {};
return ret;
}

void GameList::CreateDiscSetEntries(const PlayedTimeMap& played_time_map)
{
std::unique_lock lock(s_mutex);
Expand Down
6 changes: 6 additions & 0 deletions src/core/game_list.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ struct Entry
ALWAYS_INLINE EntryType GetSortType() const { return (type == EntryType::DiscSet) ? EntryType::Disc : type; }
};

using EntryList = std::vector<Entry>;

const char* GetEntryTypeName(EntryType type);
const char* GetEntryTypeDisplayName(EntryType type);

Expand All @@ -97,6 +99,10 @@ bool IsGameListLoaded();
/// If only_cache is set, no new files will be scanned, only those present in the cache.
void Refresh(bool invalidate_cache, bool only_cache = false, ProgressCallback* progress = nullptr);

/// Moves the current game list, which can be temporarily displayed in the UI until refresh completes.
/// The caller **must** call Refresh() afterward, otherwise it will be permanently lost.
EntryList TakeEntryList();

/// Add played time for the specified serial.
void AddPlayedTimeForSerial(const std::string& serial, std::time_t last_time, std::time_t add_time);
void ClearPlayedTimeForSerial(const std::string& serial);
Expand Down
54 changes: 43 additions & 11 deletions src/duckstation-qt/gamelistmodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
#include "core/system.h"

#include "common/file_system.h"
#include "common/log.h"
#include "common/path.h"
#include "common/string_util.h"

Expand All @@ -21,8 +20,6 @@
#include <QtGui/QIcon>
#include <QtGui/QPainter>

Log_SetChannel(GameList);

static constexpr std::array<const char*, GameListModel::Column_Count> s_column_names = {
{"Icon", "Serial", "Title", "File Title", "Developer", "Publisher", "Genre", "Year", "Players", "Time Played",
"Last Played", "Size", "File Size", "Region", "Compatibility", "Cover"}};
Expand Down Expand Up @@ -334,9 +331,13 @@ int GameListModel::getCoverArtSpacing() const

int GameListModel::rowCount(const QModelIndex& parent) const
{
if (parent.isValid())
if (parent.isValid()) [[unlikely]]
return 0;

if (m_taken_entries.has_value())
return static_cast<int>(m_taken_entries->size());

const auto lock = GameList::GetLock();
return static_cast<int>(GameList::GetEntryCount());
}

Expand All @@ -350,18 +351,32 @@ int GameListModel::columnCount(const QModelIndex& parent) const

QVariant GameListModel::data(const QModelIndex& index, int role) const
{
if (!index.isValid())
if (!index.isValid()) [[unlikely]]
return {};

const int row = index.row();
if (row < 0 || row >= static_cast<int>(GameList::GetEntryCount()))
return {};
DebugAssert(row >= 0);

const auto lock = GameList::GetLock();
const GameList::Entry* ge = GameList::GetEntryByIndex(row);
if (!ge)
return {};
if (m_taken_entries.has_value())
{
if (static_cast<u32>(row) >= m_taken_entries->size())
return {};

return data(index, role, &m_taken_entries.value()[row]);
}
else
{
const auto lock = GameList::GetLock();
const GameList::Entry* ge = GameList::GetEntryByIndex(static_cast<u32>(row));
if (!ge)
return {};

return data(index, role, ge);
}
}

QVariant GameListModel::data(const QModelIndex& index, int role, const GameList::Entry* ge) const
{
switch (role)
{
case Qt::DisplayRole:
Expand Down Expand Up @@ -544,10 +559,27 @@ QVariant GameListModel::headerData(int section, Qt::Orientation orientation, int
return m_column_display_names[section];
}

bool GameListModel::hasTakenGameList() const
{
return m_taken_entries.has_value();
}

void GameListModel::takeGameList()
{
const auto lock = GameList::GetLock();
m_taken_entries = GameList::TakeEntryList();

// If it's empty (e.g. first boot), don't use it.
if (m_taken_entries->empty())
m_taken_entries.reset();
}

void GameListModel::refresh()
{
beginResetModel();

m_taken_entries.reset();

// Invalidate memcard LRU cache, forcing a re-query of the memcard timestamps.
m_memcard_pixmap_cache.Clear();

Expand Down
7 changes: 7 additions & 0 deletions src/duckstation-qt/gamelistmodel.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ class GameListModel final : public QAbstractTableModel

ALWAYS_INLINE const QString& getColumnDisplayName(int column) { return m_column_display_names[column]; }

bool hasTakenGameList() const;
void takeGameList();

void refresh();
void reloadThemeSpecificImages();

Expand Down Expand Up @@ -98,6 +101,8 @@ class GameListModel final : public QAbstractTableModel
};
#pragma pack(pop)

QVariant data(const QModelIndex& index, int role, const GameList::Entry* ge) const;

void loadCommonImages();
void loadThemeSpecificImages();
void setColumnDisplayNames();
Expand All @@ -109,6 +114,8 @@ class GameListModel final : public QAbstractTableModel

static QString formatTimespan(time_t timespan);

std::optional<GameList::EntryList> m_taken_entries;

float m_cover_scale = 0.0f;
bool m_show_titles_for_covers = false;
bool m_show_game_icons = false;
Expand Down
12 changes: 11 additions & 1 deletion src/duckstation-qt/gamelistrefreshthread.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ AsyncRefreshProgressCallback::AsyncRefreshProgressCallback(GameListRefreshThread
{
}

float AsyncRefreshProgressCallback::timeSinceStart() const
{
return m_start_time.GetTimeSeconds();
}

void AsyncRefreshProgressCallback::Cancel()
{
// Not atomic, but we don't need to cancel immediately.
Expand Down Expand Up @@ -87,7 +92,7 @@ void AsyncRefreshProgressCallback::ModalInformation(const std::string_view messa

void AsyncRefreshProgressCallback::fireUpdate()
{
m_parent->refreshProgress(m_status_text, m_last_value, m_last_range);
m_parent->refreshProgress(m_status_text, m_last_value, m_last_range, m_start_time.GetTimeSeconds());
}

GameListRefreshThread::GameListRefreshThread(bool invalidate_cache)
Expand All @@ -97,6 +102,11 @@ GameListRefreshThread::GameListRefreshThread(bool invalidate_cache)

GameListRefreshThread::~GameListRefreshThread() = default;

float GameListRefreshThread::timeSinceStart() const
{
return m_progress.timeSinceStart();
}

void GameListRefreshThread::cancel()
{
m_progress.Cancel();
Expand Down
8 changes: 6 additions & 2 deletions src/duckstation-qt/gamelistrefreshthread.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ class AsyncRefreshProgressCallback : public ProgressCallback
public:
AsyncRefreshProgressCallback(GameListRefreshThread* parent);

float timeSinceStart() const;

void Cancel();

void PushState() override;
Expand All @@ -33,7 +35,7 @@ class AsyncRefreshProgressCallback : public ProgressCallback
void fireUpdate();

GameListRefreshThread* m_parent;
Common::Timer m_last_update_time;
Common::Timer m_start_time;
QString m_status_text;
int m_last_range = 1;
int m_last_value = 0;
Expand All @@ -47,10 +49,12 @@ class GameListRefreshThread final : public QThread
GameListRefreshThread(bool invalidate_cache);
~GameListRefreshThread();

float timeSinceStart() const;

void cancel();

Q_SIGNALS:
void refreshProgress(const QString& status, int current, int total);
void refreshProgress(const QString& status, int current, int total, float time);
void refreshComplete();

protected:
Expand Down
19 changes: 12 additions & 7 deletions src/duckstation-qt/gamelistwidget.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin <stenzek@gmail.com>
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)

#include "gamelistwidget.h"
Expand Down Expand Up @@ -182,9 +182,6 @@ void GameListWidget::initialize()
connect(m_ui.searchText, &QLineEdit::textChanged, this,
[this](const QString& text) { m_sort_model->setFilterName(text); });

// Works around a strange bug where after hiding the game list, the cursor for the whole window changes to a beam..
// m_ui.searchText->setCursor(QCursor(Qt::ArrowCursor));

m_table_view = new QTableView(m_ui.stack);
m_table_view->setModel(m_sort_model);
m_table_view->setSortingEnabled(true);
Expand Down Expand Up @@ -285,6 +282,9 @@ void GameListWidget::refresh(bool invalidate_cache)
{
cancelRefresh();

if (!invalidate_cache)
m_model->takeGameList();

m_refresh_thread = new GameListRefreshThread(invalidate_cache);
connect(m_refresh_thread, &GameListRefreshThread::refreshProgress, this, &GameListWidget::onRefreshProgress,
Qt::QueuedConnection);
Expand Down Expand Up @@ -314,14 +314,19 @@ void GameListWidget::reloadThemeSpecificImages()
m_model->reloadThemeSpecificImages();
}

void GameListWidget::onRefreshProgress(const QString& status, int current, int total)
void GameListWidget::onRefreshProgress(const QString& status, int current, int total, float time)
{
// Avoid spamming the UI on very short refresh (e.g. game exit).
static constexpr float SHORT_REFRESH_TIME = 0.5f;
if (!m_model->hasTakenGameList())
m_model->refresh();

// switch away from the placeholder while we scan, in case we find anything
if (m_ui.stack->currentIndex() == 2)
m_ui.stack->setCurrentIndex(Host::GetBaseBoolSettingValue("UI", "GameListGridView", false) ? 1 : 0);

m_model->refresh();
emit refreshProgress(status, current, total);
if (!m_model->hasTakenGameList() || time >= SHORT_REFRESH_TIME)
emit refreshProgress(status, current, total);
}

void GameListWidget::onRefreshComplete()
Expand Down
2 changes: 1 addition & 1 deletion src/duckstation-qt/gamelistwidget.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ class GameListWidget : public QWidget
void layoutChange();

private Q_SLOTS:
void onRefreshProgress(const QString& status, int current, int total);
void onRefreshProgress(const QString& status, int current, int total, float time);
void onRefreshComplete();

void onSelectionModelCurrentChanged(const QModelIndex& current, const QModelIndex& previous);
Expand Down

0 comments on commit 9a626ca

Please sign in to comment.