From 9a626caad95e655f33d398905a64ee53c2a0efa4 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Sat, 3 Aug 2024 00:50:02 +1000 Subject: [PATCH] Qt: Reduce game list jank after shutting down VM Prevents progress bar briefly appearing, and the list scrolling to the top when you exit a game. --- src/core/game_list.cpp | 17 ++++-- src/core/game_list.h | 6 +++ src/duckstation-qt/gamelistmodel.cpp | 54 ++++++++++++++++---- src/duckstation-qt/gamelistmodel.h | 7 +++ src/duckstation-qt/gamelistrefreshthread.cpp | 12 ++++- src/duckstation-qt/gamelistrefreshthread.h | 8 ++- src/duckstation-qt/gamelistwidget.cpp | 19 ++++--- src/duckstation-qt/gamelistwidget.h | 2 +- 8 files changed, 98 insertions(+), 27 deletions(-) diff --git a/src/core/game_list.cpp b/src/core/game_list.cpp index 0c109dca8..1922d912b 100644 --- a/src/core/game_list.cpp +++ b/src/core/game_list.cpp @@ -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 s_entries; +static EntryList s_entries; static std::recursive_mutex s_mutex; -static GameList::CacheMap s_cache_map; -static std::vector s_memcard_timestamp_cache_entries; +static CacheMap s_cache_map; +static std::vector s_memcard_timestamp_cache_entries; static bool s_game_list_loaded = false; +} // namespace GameList + const char* GameList::GetEntryTypeName(EntryType type) { static std::array(EntryType::Count)> names = { @@ -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); diff --git a/src/core/game_list.h b/src/core/game_list.h index b2a393f19..723f4885d 100644 --- a/src/core/game_list.h +++ b/src/core/game_list.h @@ -71,6 +71,8 @@ struct Entry ALWAYS_INLINE EntryType GetSortType() const { return (type == EntryType::DiscSet) ? EntryType::Disc : type; } }; +using EntryList = std::vector; + const char* GetEntryTypeName(EntryType type); const char* GetEntryTypeDisplayName(EntryType type); @@ -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); diff --git a/src/duckstation-qt/gamelistmodel.cpp b/src/duckstation-qt/gamelistmodel.cpp index 02215f5d2..957c1a551 100644 --- a/src/duckstation-qt/gamelistmodel.cpp +++ b/src/duckstation-qt/gamelistmodel.cpp @@ -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" @@ -21,8 +20,6 @@ #include #include -Log_SetChannel(GameList); - static constexpr std::array s_column_names = { {"Icon", "Serial", "Title", "File Title", "Developer", "Publisher", "Genre", "Year", "Players", "Time Played", "Last Played", "Size", "File Size", "Region", "Compatibility", "Cover"}}; @@ -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(m_taken_entries->size()); + + const auto lock = GameList::GetLock(); return static_cast(GameList::GetEntryCount()); } @@ -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(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(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(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: @@ -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(); diff --git a/src/duckstation-qt/gamelistmodel.h b/src/duckstation-qt/gamelistmodel.h index 2fdbe06b3..1886b07f4 100644 --- a/src/duckstation-qt/gamelistmodel.h +++ b/src/duckstation-qt/gamelistmodel.h @@ -56,6 +56,9 @@ public: ALWAYS_INLINE const QString& getColumnDisplayName(int column) { return m_column_display_names[column]; } + bool hasTakenGameList() const; + void takeGameList(); + void refresh(); void reloadThemeSpecificImages(); @@ -98,6 +101,8 @@ private: }; #pragma pack(pop) + QVariant data(const QModelIndex& index, int role, const GameList::Entry* ge) const; + void loadCommonImages(); void loadThemeSpecificImages(); void setColumnDisplayNames(); @@ -109,6 +114,8 @@ private: static QString formatTimespan(time_t timespan); + std::optional m_taken_entries; + float m_cover_scale = 0.0f; bool m_show_titles_for_covers = false; bool m_show_game_icons = false; diff --git a/src/duckstation-qt/gamelistrefreshthread.cpp b/src/duckstation-qt/gamelistrefreshthread.cpp index c51f18a91..d493e2ebf 100644 --- a/src/duckstation-qt/gamelistrefreshthread.cpp +++ b/src/duckstation-qt/gamelistrefreshthread.cpp @@ -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. @@ -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) @@ -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(); diff --git a/src/duckstation-qt/gamelistrefreshthread.h b/src/duckstation-qt/gamelistrefreshthread.h index 9c0e736f9..d2a409ffc 100644 --- a/src/duckstation-qt/gamelistrefreshthread.h +++ b/src/duckstation-qt/gamelistrefreshthread.h @@ -16,6 +16,8 @@ class AsyncRefreshProgressCallback : public ProgressCallback public: AsyncRefreshProgressCallback(GameListRefreshThread* parent); + float timeSinceStart() const; + void Cancel(); void PushState() override; @@ -33,7 +35,7 @@ private: 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; @@ -47,10 +49,12 @@ public: 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: diff --git a/src/duckstation-qt/gamelistwidget.cpp b/src/duckstation-qt/gamelistwidget.cpp index c03227fd8..a1e0e4271 100644 --- a/src/duckstation-qt/gamelistwidget.cpp +++ b/src/duckstation-qt/gamelistwidget.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #include "gamelistwidget.h" @@ -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); @@ -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); @@ -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() diff --git a/src/duckstation-qt/gamelistwidget.h b/src/duckstation-qt/gamelistwidget.h index a7334804a..b3e325b26 100644 --- a/src/duckstation-qt/gamelistwidget.h +++ b/src/duckstation-qt/gamelistwidget.h @@ -69,7 +69,7 @@ Q_SIGNALS: 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);