From db11399785b65c8798f75696fd91bedf8342204c Mon Sep 17 00:00:00 2001 From: Masamune3210 <1053504+Masamune3210@users.noreply.github.com> Date: Sat, 16 May 2026 11:38:29 -0500 Subject: [PATCH] citra_qt: cache game list to disk, rescan only on explicit request Previously the game list rescanned game directories on every launch and after any event that might have changed game files. This commit replaces that with a persistent JSON cache so subsequent launches populate the list instantly from disk. Key changes: - game_list_cache.h/cpp: new JSON cache at ConfigDir/game_list_cache.json. Keyed by a DirsSignature fingerprint (path + deep_scan for every dir); invalidated automatically when the directory list changes. - GameListWorker: emits CacheEntryReady in parallel with EntryReady, carrying raw metadata so entries can be rebuilt without touching ROMs. - GameList::PopulateAsync: checks cache first. On a hit it rebuilds the model synchronously from cached data (PopulateFromCache) with no worker thread. On a miss it runs the full scan and saves the new cache in DonePopulating. - GameList::ForceRescan: invalidates the cache then calls PopulateAsync. - GameList::RefreshPlayTimes: updates only the play-time column in-place; called after a game session ends instead of a full rescan. - QFileSystemWatcher is kept for CIA-install plumbing but is no longer connected to any automatic rescan. - File menu: new "Refresh Game List" action (F5) triggers ForceRescan. - OnCIAInstallFinished: uses ForceRescan (new title needs a fresh scan). - OnStopGame: uses RefreshPlayTimes (only play time changed). Co-Authored-By: Claude Sonnet 4.6 --- src/citra_qt/CMakeLists.txt | 2 + src/citra_qt/citra_qt.cpp | 12 ++- src/citra_qt/citra_qt.h | 1 + src/citra_qt/game_list.cpp | 140 ++++++++++++++++++++++++------ src/citra_qt/game_list.h | 25 ++++++ src/citra_qt/game_list_cache.cpp | 122 ++++++++++++++++++++++++++ src/citra_qt/game_list_cache.h | 50 +++++++++++ src/citra_qt/game_list_worker.cpp | 32 +++++-- src/citra_qt/game_list_worker.h | 5 ++ src/citra_qt/main.ui | 9 ++ 10 files changed, 363 insertions(+), 35 deletions(-) create mode 100644 src/citra_qt/game_list_cache.cpp create mode 100644 src/citra_qt/game_list_cache.h diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt index 4c20b510b..6de23df2e 100644 --- a/src/citra_qt/CMakeLists.txt +++ b/src/citra_qt/CMakeLists.txt @@ -135,6 +135,8 @@ add_library(citra_qt STATIC EXCLUDE_FROM_ALL dumping/options_dialog.ui game_list.cpp game_list.h + game_list_cache.cpp + game_list_cache.h game_list_p.h game_list_worker.cpp game_list_worker.h diff --git a/src/citra_qt/citra_qt.cpp b/src/citra_qt/citra_qt.cpp index 68fb8e8da..f62117a75 100644 --- a/src/citra_qt/citra_qt.cpp +++ b/src/citra_qt/citra_qt.cpp @@ -1227,6 +1227,7 @@ void GMainWindow::ConnectMenuEvents() { // Help connect_menu(ui->action_Open_Citra_Folder, &GMainWindow::OnOpenCitraFolder); + connect_menu(ui->action_Refresh_Game_List, &GMainWindow::OnRefreshGameList); connect_menu(ui->action_Open_Log_Folder, []() { QString path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::LogDir)); QDesktopServices::openUrl(QUrl::fromLocalFile(path)); @@ -2644,7 +2645,8 @@ void GMainWindow::OnCIAInstallFinished() { progress_bar->setValue(0); game_list->SetDirectoryWatcherEnabled(true); ui->action_Install_CIA->setEnabled(true); - game_list->PopulateAsync(UISettings::values.game_dirs); + // A new title was installed; invalidate the cache so the new entry appears. + game_list->ForceRescan(UISettings::values.game_dirs); } void GMainWindow::UninstallTitles( @@ -2782,8 +2784,8 @@ void GMainWindow::OnStopGame() { SetTurboEnabled(false); play_time_manager->Stop(); - // Update game list to show new play time - game_list->PopulateAsync(UISettings::values.game_dirs); + // Only the play-time column changed; a full rescan is unnecessary. + game_list->RefreshPlayTimes(); ShutdownGame(); graphics_api_button->setEnabled(true); @@ -3193,6 +3195,10 @@ void GMainWindow::OnOpenCitraFolder() { QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::UserDir)))); } +void GMainWindow::OnRefreshGameList() { + game_list->ForceRescan(UISettings::values.game_dirs); +} + void GMainWindow::OnToggleFilterBar() { game_list->SetFilterVisible(ui->action_Show_Filter_Bar->isChecked()); if (ui->action_Show_Filter_Bar->isChecked()) { diff --git a/src/citra_qt/citra_qt.h b/src/citra_qt/citra_qt.h index 3ad25e5ae..f5009c860 100644 --- a/src/citra_qt/citra_qt.h +++ b/src/citra_qt/citra_qt.h @@ -248,6 +248,7 @@ private slots: void OnGameListDumpRomFS(QString game_path, u64 program_id); void OnGameListOpenDirectory(const QString& directory); void OnGameListAddDirectory(); + void OnRefreshGameList(); void OnGameListShowList(bool show); void OnGameListOpenPerGameProperties(const QString& file); void OnConfigurePerGame(); diff --git a/src/citra_qt/game_list.cpp b/src/citra_qt/game_list.cpp index 5579e61ce..c0ce6c9bc 100644 --- a/src/citra_qt/game_list.cpp +++ b/src/citra_qt/game_list.cpp @@ -30,6 +30,7 @@ #include "citra_qt/citra_qt.h" #include "citra_qt/compatibility_list.h" #include "citra_qt/game_list.h" +#include "citra_qt/game_list_cache.h" #include "citra_qt/game_list_p.h" #include "citra_qt/game_list_worker.h" #include "citra_qt/uisettings.h" @@ -350,8 +351,8 @@ public: GameList::GameList(PlayTime::PlayTimeManager& play_time_manager_, GMainWindow* parent) : QWidget{parent}, play_time_manager{play_time_manager_} { watcher = new QFileSystemWatcher(this); - connect(watcher, &QFileSystemWatcher::directoryChanged, this, &GameList::RefreshGameDirectory, - Qt::UniqueConnection); + // The watcher is kept for CIA-install disable/re-enable plumbing but no longer drives + // automatic rescans. The game list is populated from cache or on explicit user request. this->main_window = parent; layout = new QVBoxLayout; @@ -412,14 +413,9 @@ void GameList::SetFilterVisible(bool visibility) { search_field->setVisible(visibility); } -void GameList::SetDirectoryWatcherEnabled(bool enabled) { - if (enabled) { - connect(watcher, &QFileSystemWatcher::directoryChanged, this, - &GameList::RefreshGameDirectory, Qt::UniqueConnection); - } else { - disconnect(watcher, &QFileSystemWatcher::directoryChanged, this, - &GameList::RefreshGameDirectory); - } +void GameList::SetDirectoryWatcherEnabled([[maybe_unused]] bool enabled) { + // Automatic filesystem-watcher rescans have been removed; the game list now uses an + // explicit cache. This function is intentionally a no-op. } void GameList::ClearFilter() { @@ -477,6 +473,17 @@ bool GameList::IsEmpty() const { } void GameList::DonePopulating(const QStringList& watch_list) { + // Save accumulated cache entries from this scan, then clear them. + if (current_scan_dirs && !pending_cache_entries.isEmpty()) { + SaveGameListCache(*current_scan_dirs, pending_cache_entries); + } + pending_cache_entries.clear(); + current_scan_dirs = nullptr; + + FinalizePopulate(); +} + +void GameList::FinalizePopulate() { emit ShowList(!IsEmpty()); item_model->invisibleRootItem()->appendRow(new GameListAddDir()); @@ -490,21 +497,6 @@ void GameList::DonePopulating(const QStringList& watch_list) { AddFavorite(id); } - // Clear out the old directories to watch for changes and add the new ones - auto watch_dirs = watcher->directories(); - if (!watch_dirs.isEmpty()) { - watcher->removePaths(watch_dirs); - } - // Workaround: Add the watch paths in chunks to allow the gui to refresh - // This prevents the UI from stalling when a large number of watch paths are added - // Also artificially caps the watcher to a certain number of directories - constexpr qsizetype LIMIT_WATCH_DIRECTORIES = 5000; - constexpr int SLICE_SIZE = 25; - const qsizetype len = std::min(watch_list.length(), LIMIT_WATCH_DIRECTORIES); - for (qsizetype i = 0; i < len; i += SLICE_SIZE) { - watcher->addPaths(watch_list.mid(i, i + SLICE_SIZE)); - QCoreApplication::processEvents(); - } tree_view->setEnabled(true); const int folderCount = tree_view->model()->rowCount(); int children_total = 0; @@ -518,7 +510,7 @@ void GameList::DonePopulating(const QStringList& watch_list) { item_model->sort(tree_view->header()->sortIndicatorSection(), tree_view->header()->sortIndicatorOrder()); - // resize all columns except for Name to fit their contents + // Resize all columns except Name to fit their contents. for (int i = 1; i < COLUMN_COUNT; i++) { tree_view->resizeColumnToContents(i); } @@ -1115,11 +1107,26 @@ void GameList::PopulateAsync(QVector& game_dirs) { emit ShouldCancelWorker(); + // Try to load from the on-disk cache first. This avoids re-scanning all game + // directories on every launch. + QVector cached_entries; + if (LoadGameListCache(game_dirs, cached_entries)) { + PopulateFromCache(game_dirs, cached_entries); + FinalizePopulate(); + return; + } + + // Cache miss (first run, dirs changed, or cache invalidated): + // run the full worker scan and collect entries for the new cache. + current_scan_dirs = &game_dirs; + GameListWorker* worker = new GameListWorker(game_dirs, compatibility_list, play_time_manager); connect(worker, &GameListWorker::EntryReady, this, &GameList::AddEntry, Qt::QueuedConnection); connect(worker, &GameListWorker::DirEntryReady, this, &GameList::AddDirEntry, Qt::QueuedConnection); + connect(worker, &GameListWorker::CacheEntryReady, this, &GameList::OnCacheEntryReady, + Qt::QueuedConnection); connect(worker, &GameListWorker::Finished, this, &GameList::DonePopulating, Qt::QueuedConnection); // Use DirectConnection here because worker->Cancel() is thread-safe and we want it to @@ -1131,6 +1138,87 @@ void GameList::PopulateAsync(QVector& game_dirs) { current_worker = std::move(worker); } +void GameList::OnCacheEntryReady(GameListCacheEntry cache_entry) { + // Assign the dir index by counting how many directory rows are in the model so far. + // DirEntryReady for a dir is always enqueued before any CacheEntryReady for its children, + // so rowCount()-1 reliably identifies the current directory. + cache_entry.game_dir_index = item_model->rowCount() - 1; + pending_cache_entries.append(std::move(cache_entry)); +} + +void GameList::PopulateFromCache(QVector& game_dirs, + const QVector& entries) { + // Build and add a GameListDir for every configured game directory. + QVector dir_items; + dir_items.reserve(game_dirs.size()); + for (UISettings::GameDir& game_dir : game_dirs) { + GameListDir* dir_item; + if (game_dir.path == QStringLiteral("INSTALLED")) { + dir_item = new GameListDir(game_dir, GameListItemType::InstalledDir); + } else if (game_dir.path == QStringLiteral("SYSTEM")) { + dir_item = new GameListDir(game_dir, GameListItemType::SystemDir); + } else { + dir_item = new GameListDir(game_dir); + } + AddDirEntry(dir_item); + dir_items.append(dir_item); + } + + // Reconstruct each game entry from the cached data. + for (const GameListCacheEntry& e : entries) { + if (e.game_dir_index < 0 || e.game_dir_index >= dir_items.size()) { + continue; + } + + // Wrap the SMDH bytes as a span so the item constructors can read them. + const std::span smdh_span( + reinterpret_cast(e.smdh.constData()), + static_cast(e.smdh.size())); + + QList items = { + new GameListItemPath(e.path, smdh_span, e.program_id, e.extdata_id, + e.media_type, e.is_encrypted, e.can_insert), + new GameListItemCompat(e.compatibility), + new GameListItemRegion(smdh_span), + new GameListItem(e.file_type), + new GameListItemSize(e.file_size), + new GameListItemPlayTime(play_time_manager.GetPlayTime(e.program_id)), + }; + + AddEntry(items, dir_items[e.game_dir_index]); + } +} + +void GameList::ForceRescan(QVector& game_dirs) { + InvalidateGameListCache(); + PopulateAsync(game_dirs); +} + +void GameList::RefreshPlayTimes() { + const int folder_count = item_model->rowCount(); + for (int i = 0; i < folder_count; ++i) { + auto* folder = item_model->item(i); + if (!folder) { + continue; + } + for (int j = 0; j < folder->rowCount(); ++j) { + auto* name_item = folder->child(j, 0); + if (!name_item || + static_cast(name_item->type()) != GameListItemType::Game) { + continue; + } + const u64 program_id = + name_item->data(GameListItemPath::ProgramIdRole).toULongLong(); + auto* time_item = folder->child(j, COLUMN_PLAY_TIME); + if (!time_item) { + continue; + } + time_item->setData(play_time_manager.GetPlayTime(program_id), + GameListItemPlayTime::PlayTimeRole); + } + } +} + void GameList::SaveInterfaceLayout() { UISettings::values.gamelist_header_state = tree_view->header()->saveState(); } diff --git a/src/citra_qt/game_list.h b/src/citra_qt/game_list.h index a1ee941db..690f5050c 100644 --- a/src/citra_qt/game_list.h +++ b/src/citra_qt/game_list.h @@ -10,6 +10,7 @@ #include #include #include "citra_qt/compatibility_list.h" +#include "citra_qt/game_list_cache.h" #include "common/common_types.h" #include "common/play_time_manager.h" #include "uisettings.h" @@ -77,8 +78,18 @@ public: bool IsEmpty() const; void LoadCompatibilityList(); + + /// Populates the list, loading from cache when it is valid and up to date. + /// Falls back to a full directory scan if the cache is absent or the directory list changed. void PopulateAsync(QVector& game_dirs); + /// Invalidates the cache and unconditionally runs a full directory scan. + void ForceRescan(QVector& game_dirs); + + /// Updates only the play-time column in the current list from the play-time manager. + /// Cheaper than a full rescan when only play times have changed (e.g. after a game stops). + void RefreshPlayTimes(); + void SaveInterfaceLayout(); void LoadInterfaceLayout(); @@ -123,9 +134,17 @@ private slots: private: void AddDirEntry(GameListDir* entry_items); void AddEntry(const QList& entry_items, GameListDir* parent); + void OnCacheEntryReady(GameListCacheEntry cache_entry); void ValidateEntry(const QModelIndex& item); void DonePopulating(const QStringList& watch_list); + /// Populates the list from a pre-loaded cache without spawning a worker thread. + void PopulateFromCache(QVector& game_dirs, + const QVector& entries); + + /// Common finalization shared by both the scan path and the cache-load path. + void FinalizePopulate(); + void PopupContextMenu(const QPoint& menu_location); void PopupHeaderContextMenu(const QPoint& menu_location); void AddGamePopup(QMenu& context_menu, const QString& path, const QString& name, u64 program_id, @@ -153,6 +172,12 @@ private: const PlayTime::PlayTimeManager& play_time_manager; + // Accumulates raw cache entries while a scan worker is running. + // Saved to disk in DonePopulating, then cleared. + QVector pending_cache_entries; + // The game_dirs in use for the current scan; needed when saving the cache. + QVector* current_scan_dirs = nullptr; + std::chrono::time_point time_last_refresh; }; diff --git a/src/citra_qt/game_list_cache.cpp b/src/citra_qt/game_list_cache.cpp new file mode 100644 index 000000000..1cb2b4565 --- /dev/null +++ b/src/citra_qt/game_list_cache.cpp @@ -0,0 +1,122 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include "citra_qt/game_list_cache.h" +#include "common/file_util.h" +#include "common/logging/log.h" +#include "core/hle/service/fs/archive.h" + +namespace { + +constexpr int CACHE_VERSION = 1; + +QString GetCachePath() { + return QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::ConfigDir)) + + QStringLiteral("game_list_cache.json"); +} + +/// Returns a compact string fingerprint of @p game_dirs. +/// Any change to the list (paths, deep_scan flags, or order) yields a different string. +QString DirsSignature(const QVector& game_dirs) { + QStringList parts; + parts.reserve(game_dirs.size()); + for (const auto& dir : game_dirs) { + parts += dir.path + QLatin1Char('|') + (dir.deep_scan ? QLatin1Char('1') : QLatin1Char('0')); + } + return parts.join(QLatin1Char(';')); +} + +} // namespace + +bool LoadGameListCache(const QVector& game_dirs, + QVector& out_entries) { + const QString path = GetCachePath(); + QFile file(path); + if (!file.open(QIODevice::ReadOnly)) { + return false; + } + + QJsonParseError err; + const QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &err); + if (err.error != QJsonParseError::NoError || !doc.isObject()) { + LOG_WARNING(Frontend, "Game list cache parse error: {}", err.errorString().toStdString()); + return false; + } + + const QJsonObject root = doc.object(); + if (root[QStringLiteral("version")].toInt() != CACHE_VERSION) { + return false; + } + if (root[QStringLiteral("dirs_signature")].toString() != DirsSignature(game_dirs)) { + return false; + } + + const QJsonArray arr = root[QStringLiteral("entries")].toArray(); + out_entries.clear(); + out_entries.reserve(arr.size()); + for (const QJsonValue& val : arr) { + if (!val.isObject()) { + out_entries.clear(); + return false; + } + const QJsonObject obj = val.toObject(); + GameListCacheEntry e; + e.path = obj[QStringLiteral("path")].toString(); + e.program_id = obj[QStringLiteral("program_id")].toString().toULongLong(nullptr, 16); + e.extdata_id = obj[QStringLiteral("extdata_id")].toString().toULongLong(nullptr, 16); + e.media_type = static_cast( + obj[QStringLiteral("media_type")].toInt()); + e.is_encrypted = obj[QStringLiteral("encrypted")].toBool(); + e.can_insert = obj[QStringLiteral("can_insert")].toBool(); + e.smdh = QByteArray::fromBase64( + obj[QStringLiteral("smdh")].toString().toLatin1()); + e.compatibility = obj[QStringLiteral("compat")].toString(); + e.file_type = obj[QStringLiteral("file_type")].toString(); + // file_size is stored as a string to avoid JSON double precision limits. + e.file_size = obj[QStringLiteral("size")].toString().toULongLong(); + e.game_dir_index = obj[QStringLiteral("dir_idx")].toInt(); + out_entries.append(std::move(e)); + } + return true; +} + +void SaveGameListCache(const QVector& game_dirs, + const QVector& entries) { + QJsonArray arr; + for (const auto& e : entries) { + QJsonObject obj; + obj[QStringLiteral("path")] = e.path; + obj[QStringLiteral("program_id")] = QString::number(e.program_id, 16); + obj[QStringLiteral("extdata_id")] = QString::number(e.extdata_id, 16); + obj[QStringLiteral("media_type")] = static_cast(e.media_type); + obj[QStringLiteral("encrypted")] = e.is_encrypted; + obj[QStringLiteral("can_insert")] = e.can_insert; + obj[QStringLiteral("smdh")] = QString::fromLatin1(e.smdh.toBase64()); + obj[QStringLiteral("compat")] = e.compatibility; + obj[QStringLiteral("file_type")] = e.file_type; + obj[QStringLiteral("size")] = QString::number(e.file_size); + obj[QStringLiteral("dir_idx")] = e.game_dir_index; + arr.append(obj); + } + + QJsonObject root; + root[QStringLiteral("version")] = CACHE_VERSION; + root[QStringLiteral("dirs_signature")] = DirsSignature(game_dirs); + root[QStringLiteral("entries")] = arr; + + QFile file(GetCachePath()); + if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + LOG_ERROR(Frontend, "Failed to write game list cache: {}", GetCachePath().toStdString()); + return; + } + file.write(QJsonDocument(root).toJson(QJsonDocument::Compact)); +} + +void InvalidateGameListCache() { + QFile::remove(GetCachePath()); +} diff --git a/src/citra_qt/game_list_cache.h b/src/citra_qt/game_list_cache.h new file mode 100644 index 000000000..689234a4e --- /dev/null +++ b/src/citra_qt/game_list_cache.h @@ -0,0 +1,50 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include "citra_qt/uisettings.h" +#include "common/common_types.h" + +namespace Service::FS { +enum class MediaType : u32; +} + +/// Cached metadata for a single game entry, sufficient to rebuild all model columns. +struct GameListCacheEntry { + QString path; + QByteArray smdh; ///< Raw SMDH bytes; empty for games that have no icon data. + u64 program_id = 0; + u64 extdata_id = 0; + Service::FS::MediaType media_type{}; + bool is_encrypted = false; + bool can_insert = false; + QString compatibility; ///< Tier string: "0"–"5" or "99" for not tested. + QString file_type; ///< Human-readable file-type string from the loader. + quint64 file_size = 0; + int game_dir_index = 0; ///< Index into UISettings::values.game_dirs for the parent dir. +}; + +/** + * Attempts to load the game list cache and validates it against the current game_dirs. + * + * Returns true and populates @p out_entries when the cache exists, parses without error, + * and its stored directory list matches the caller's game_dirs exactly. + * Returns false in all other cases (missing file, parse error, stale dirs). + */ +bool LoadGameListCache(const QVector& game_dirs, + QVector& out_entries); + +/** + * Serialises @p entries to the game list cache file, storing @p game_dirs alongside them + * so that future loads can detect when the directory list has changed. + */ +void SaveGameListCache(const QVector& game_dirs, + const QVector& entries); + +/// Removes the cache file so the next PopulateAsync performs a full scan. +void InvalidateGameListCache(); diff --git a/src/citra_qt/game_list_worker.cpp b/src/citra_qt/game_list_worker.cpp index cc65b5082..54bffcb21 100644 --- a/src/citra_qt/game_list_worker.cpp +++ b/src/citra_qt/game_list_worker.cpp @@ -109,21 +109,41 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign if (it != compatibility_list.end()) compatibility = it->second.first; + const bool is_encrypted = res == Loader::ResultStatus::ErrorEncrypted; + const bool can_insert = loader->GetFileType() == Loader::FileType::CCI; + const QString file_type_str = QString::fromStdString( + Loader::GetFileTypeString(loader->GetFileType(), loader->IsFileCompressed())); + const quint64 file_size = FileUtil::GetSize(physical_name); + emit EntryReady( { new GameListItemPath(QString::fromStdString(physical_name), smdh, program_id, - extdata_id, media_type, - res == Loader::ResultStatus::ErrorEncrypted, - loader->GetFileType() == Loader::FileType::CCI), + extdata_id, media_type, is_encrypted, can_insert), new GameListItemCompat(compatibility), new GameListItemRegion(smdh), - new GameListItem(QString::fromStdString(Loader::GetFileTypeString( - loader->GetFileType(), loader->IsFileCompressed()))), - new GameListItemSize(FileUtil::GetSize(physical_name)), + new GameListItem(file_type_str), + new GameListItemSize(file_size), new GameListItemPlayTime(play_time_manager.GetPlayTime(program_id)), }, parent_dir); + { + GameListCacheEntry cache_entry; + cache_entry.path = QString::fromStdString(physical_name); + cache_entry.smdh = QByteArray(reinterpret_cast(smdh.data()), + static_cast(smdh.size())); + cache_entry.program_id = program_id; + cache_entry.extdata_id = extdata_id; + cache_entry.media_type = media_type; + cache_entry.is_encrypted = is_encrypted; + cache_entry.can_insert = can_insert; + cache_entry.compatibility = compatibility; + cache_entry.file_type = file_type_str; + cache_entry.file_size = file_size; + // game_dir_index is filled in by GameList when it receives this signal. + emit CacheEntryReady(std::move(cache_entry)); + } + } else if (is_dir && recursion > 0) { watch_list.append(QString::fromStdString(physical_name)); AddFstEntriesToGameList(physical_name, recursion - 1, parent_dir, media_type); diff --git a/src/citra_qt/game_list_worker.h b/src/citra_qt/game_list_worker.h index f149a7924..a0cc0c8e9 100644 --- a/src/citra_qt/game_list_worker.h +++ b/src/citra_qt/game_list_worker.h @@ -17,6 +17,7 @@ #include #include #include "citra_qt/compatibility_list.h" +#include "citra_qt/game_list_cache.h" #include "common/common_types.h" #include "common/play_time_manager.h" @@ -54,6 +55,10 @@ signals: void DirEntryReady(GameListDir* entry_items); void EntryReady(QList entry_items, GameListDir* parent_dir); + /// Emitted in parallel with EntryReady, carrying the raw data needed to rebuild this entry + /// from the cache without touching the ROM file again. + void CacheEntryReady(GameListCacheEntry cache_entry); + /** * After the worker has traversed the game directory looking for entries, this signal is emitted * with a list of folders that should be watched for changes as well. diff --git a/src/citra_qt/main.ui b/src/citra_qt/main.ui index 9ed0fafbc..8b53d8093 100644 --- a/src/citra_qt/main.ui +++ b/src/citra_qt/main.ui @@ -88,6 +88,7 @@ + @@ -747,6 +748,14 @@ Open Azahar Folder + + + Refresh Game List + + + F5 + + false