aboutsummaryrefslogtreecommitdiff
path: root/src/qt_common/game_list/worker.cpp
diff options
context:
space:
mode:
authornéo Luigi <85024393+neoluigi4123@users.noreply.github.com>2026-11-03 00:49:36 +0100
committernéo Luigi <85024393+neoluigi4123@users.noreply.github.com>2026-11-03 00:49:36 +0100
commitc84e5caf82a786f748c233e27b7f4ed9aaad9f44 (patch)
tree3ec9cab90d74292445d0a076370933bac946fc4d /src/qt_common/game_list/worker.cpp
parent2b413a46a2820f0a646baaa8dbd683009f096f32 (diff)
parentefa7c19e59839f0d464c2e9bd41f9ca8f867fef0 (diff)
Merge remote-tracking branch 'origin/nx2' into neo.ai/vibration-fix
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> # Conflicts: # src/core/hle/service/hid/hid_server.cpp # src/core/hle/service/hid/hid_server.h
Diffstat (limited to 'src/qt_common/game_list/worker.cpp')
-rw-r--r--src/qt_common/game_list/worker.cpp165
1 files changed, 68 insertions, 97 deletions
diff --git a/src/qt_common/game_list/worker.cpp b/src/qt_common/game_list/worker.cpp
index d18f84089a..bd3d81dac4 100644
--- a/src/qt_common/game_list/worker.cpp
+++ b/src/qt_common/game_list/worker.cpp
@@ -5,6 +5,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
+#include <chrono>
#include <filesystem>
#include <memory>
#include <string>
@@ -36,10 +37,23 @@
#include "qt_common/game_list/game_list_p.h"
#include "qt_common/game_list/model.h"
+#include "qt_common/game_list/discovery.h"
#include "qt_common/game_list/worker.h"
namespace {
+struct PendingRow {
+ explicit PendingRow(QList<QStandardItem*> row) : items{std::move(row)} {}
+ PendingRow(const PendingRow&) = delete;
+ QList<QStandardItem*> items;
+ ~PendingRow() {
+ qDeleteAll(items);
+ }
+ QList<QStandardItem*> Release() {
+ return std::exchange(items, {});
+ }
+};
+
QString GetGameListCachedObject(const std::string& filename, const std::string& ext,
const std::function<QString()>& generator) {
if (!UISettings::values.cache_game_list || filename == "0000000000000000") {
@@ -212,36 +226,6 @@ QString LooseExeFSName(const std::string& physical_name) {
return name;
}
-// A loose dump's game list entry is its small main executable, so the size column would only
-// show a few dozen MB. Sum the dump's real contents instead: the ExeFS directory plus the
-// RomFS directory next to it, matching the loader's loose RomFS discovery.
-u64 LooseDumpSize(const std::filesystem::path& main_path) {
- const auto exefs_dir = main_path.parent_path();
-
- u64 total{};
- std::error_code ec;
- const auto accumulate = [&total, &ec](const std::filesystem::path& dir) {
- for (std::filesystem::recursive_directory_iterator it{
- dir, std::filesystem::directory_options::skip_permission_denied, ec};
- !ec && it != std::filesystem::recursive_directory_iterator(); it.increment(ec)) {
- std::error_code entry_ec;
- if (it->is_regular_file(entry_ec)) {
- const auto size = it->file_size(entry_ec);
- if (!entry_ec) {
- total += size;
- }
- }
- }
- };
-
- accumulate(exefs_dir);
- const auto romfs_dir = exefs_dir.parent_path() / "romfs";
- if (std::filesystem::is_directory(romfs_dir, ec)) {
- accumulate(romfs_dir);
- }
- return total;
-}
-
QString FormatPatchNameVersions(const FileSys::PatchManager& patch_manager,
Loader::AppLoader& loader, bool updatable = true) {
QString out;
@@ -359,7 +343,7 @@ void GameListWorker::RecordEvent(F&& func) {
emit DataAvailable();
}
-void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir) {
+void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir, GameListItemType parent_type) {
using namespace FileSys;
const auto& cache = system.GetContentProviderUnion();
@@ -367,13 +351,13 @@ void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir) {
auto installed_games = cache.ListEntriesFilterOrigin(std::nullopt, TitleType::Application,
ContentRecordType::Program);
- if (parent_dir->type() == static_cast<int>(GameListItemType::SdmcDir)) {
+ if (parent_type == GameListItemType::SdmcDir) {
installed_games = cache.ListEntriesFilterOrigin(
ContentProviderUnionSlot::SDMC, TitleType::Application, ContentRecordType::Program);
- } else if (parent_dir->type() == static_cast<int>(GameListItemType::UserNandDir)) {
+ } else if (parent_type == GameListItemType::UserNandDir) {
installed_games = cache.ListEntriesFilterOrigin(
ContentProviderUnionSlot::UserNAND, TitleType::Application, ContentRecordType::Program);
- } else if (parent_dir->type() == static_cast<int>(GameListItemType::SysNandDir)) {
+ } else if (parent_type == GameListItemType::SysNandDir) {
installed_games = cache.ListEntriesFilterOrigin(
ContentProviderUnionSlot::SysNAND, TitleType::Application, ContentRecordType::Program);
}
@@ -408,11 +392,12 @@ void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir) {
auto entry = MakeGameListEntry(file->GetFullPath(), name, file->GetSize(), icon, *loader,
program_id, play_time_manager, patch);
- RecordEvent([=](GameListModel* model) { model->AddEntry(entry, parent_dir); });
+ auto pending = std::make_shared<PendingRow>(std::move(entry));
+ RecordEvent([=](GameListModel* model) { model->AddEntry(pending->Release(), parent_dir); });
}
}
-void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_path, bool deep_scan,
+void GameListWorker::ScanFileSystem(ScanTarget target, const std::vector<std::filesystem::path>& files,
GameListDir* parent_dir) {
const auto process_candidate = [this, target,
parent_dir](const std::filesystem::path& path) {
@@ -434,7 +419,8 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
if (target == ScanTarget::PopulateGameList &&
(file_type == Loader::FileType::XCI || file_type == Loader::FileType::XCZ ||
- file_type == Loader::FileType::NSP || file_type == Loader::FileType::NSZ) &&
+ file_type == Loader::FileType::NSP || file_type == Loader::FileType::NSZ ||
+ file_type == Loader::FileType::NUP) &&
!Loader::IsBootableGameContainer(file, file_type)) {
return;
}
@@ -458,7 +444,7 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
std::vector<u64> program_ids;
loader->ReadProgramIds(program_ids);
- const auto addEntry = [this, &path, physical_name,
+ const auto addEntry = [this, physical_name,
parent_dir](std::unique_ptr<Loader::AppLoader>& app_loader,
const u64 id) {
std::vector<u8> icon;
@@ -476,14 +462,21 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
const FileSys::PatchManager patch{id, system.GetFileSystemController(),
system.GetContentProvider()};
- const u64 size = IsExtractedNCAMain(physical_name)
- ? LooseDumpSize(path)
- : Common::FS::GetSize(physical_name);
+ const bool loose_dump = IsExtractedNCAMain(physical_name);
+ const u64 size = loose_dump ? 0 : Common::FS::GetSize(physical_name);
auto entry = MakeGameListEntry(physical_name, name, size, icon, *app_loader, id,
play_time_manager, patch);
- RecordEvent([=](GameListModel* model) { model->AddEntry(entry, parent_dir); });
+ if (loose_dump) {
+ entry[GameListModel::COLUMN_SIZE]->setText(tr("Not scanned"));
+ entry[GameListModel::COLUMN_SIZE]->setToolTip(
+ tr("Folder size is not calculated during game discovery."));
+ }
+ auto pending = std::make_shared<PendingRow>(std::move(entry));
+ RecordEvent([=](GameListModel* model) {
+ model->AddEntry(pending->Release(), parent_dir);
+ });
};
if (res2 == Loader::ResultStatus::Success && program_ids.size() > 1 &&
@@ -508,67 +501,27 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
}
};
- const auto callback = [this, target, deep_scan,
- &process_candidate](const std::filesystem::path& path) -> bool {
+ for (const auto& path : files) {
if (stop_requested) {
- // Breaks the callback loop.
- return false;
- }
-
- if (Common::FS::IsDir(path)) {
- watch_list.append(QString::fromStdString(Common::FS::PathToUTF8String(path)));
-
- // Shallow scans stop at the first level, so probe the standard layouts of loose
- // ExeFS dumps: <game>/exefs/main, <game>/base/exefs/main and their updates in
- // <root>/updates/<title id>/exefs/main.
- if (!deep_scan && target == ScanTarget::PopulateGameList) {
- for (const auto& content_root : {path, path / "base"}) {
- const auto exefs_main = content_root / "exefs" / "main";
- if (Common::FS::IsFile(exefs_main)) {
- process_candidate(exefs_main);
- }
-
- const auto updates = content_root / "updates";
- if (Common::FS::IsDir(updates)) {
- Common::FS::IterateDirEntries(
- updates,
- [&process_candidate](const std::filesystem::path& entry) {
- const auto update_main = entry / "exefs" / "main";
- if (Common::FS::IsFile(update_main)) {
- process_candidate(update_main);
- }
- return true;
- },
- Common::FS::DirEntryFilter::Directory);
- }
- }
- }
-
- return true;
+ break;
}
-
- const auto physical_name = Common::FS::PathToUTF8String(path);
- if (HasSupportedFileExtension(physical_name) || IsExtractedNCAMain(physical_name)) {
- process_candidate(path);
+ // Loose executables cannot contribute NCA content to the manual provider.
+ if (target == ScanTarget::FillManualContentProvider &&
+ IsExtractedNCAMain(Common::FS::PathToUTF8String(path))) {
+ continue;
}
-
- return true;
- };
-
- if (deep_scan) {
- Common::FS::IterateDirEntriesRecursively(dir_path, callback,
- Common::FS::DirEntryFilter::All);
- } else {
- Common::FS::IterateDirEntries(dir_path, callback, Common::FS::DirEntryFilter::All);
+ process_candidate(path);
}
}
void GameListWorker::run() {
+ const auto started = std::chrono::steady_clock::now();
watch_list.clear();
provider->ClearAllEntries();
const auto DirEntryReady = [&](GameListDir* game_list_dir) {
- RecordEvent([=](GameListModel* model) { model->AddDirEntry(game_list_dir); });
+ auto pending = std::make_shared<std::unique_ptr<GameListDir>>(game_list_dir);
+ RecordEvent([=](GameListModel* model) { model->AddDirEntry(pending->release()); });
};
for (UISettings::GameDir& game_dir : game_dirs) {
@@ -595,17 +548,35 @@ void GameListWorker::run() {
scan = true;
}
+ const auto parent_type = static_cast<GameListItemType>(game_list_dir->type());
DirEntryReady(game_list_dir);
if (scan) {
- ScanFileSystem(ScanTarget::FillManualContentProvider, game_dir.path, game_dir.deep_scan,
- game_list_dir);
- ScanFileSystem(ScanTarget::PopulateGameList, game_dir.path, game_dir.deep_scan,
- game_list_dir);
+ const auto discovery = GameListDiscovery::Discover(
+ std::filesystem::u8path(game_dir.path), game_dir.deep_scan,
+ [](const auto& path) {
+ const auto name = Common::FS::PathToUTF8String(path);
+ return HasSupportedFileExtension(name) || IsExtractedNCAMain(name);
+ },
+ [this] { return stop_requested.load(); });
+ for (const auto& directory : discovery.directories) {
+ watch_list.append(QString::fromStdString(Common::FS::PathToUTF8String(directory)));
+ }
+ LOG_INFO(Frontend,
+ "Discovery: candidates={} directories={} skipped_romfs={} errors={} cancelled={}",
+ discovery.files.size(), discovery.directories.size(), discovery.skipped_romfs,
+ discovery.errors, stop_requested.load());
+ ScanFileSystem(ScanTarget::FillManualContentProvider, discovery.files, game_list_dir);
+ ScanFileSystem(ScanTarget::PopulateGameList, discovery.files, game_list_dir);
} else {
- AddTitlesToGameList(game_list_dir);
+ AddTitlesToGameList(game_list_dir, parent_type);
}
}
+ watch_list.removeDuplicates();
+ LOG_INFO(Frontend, "Game discovery finished: elapsed_ms={} watched={} cancelled={}",
+ std::chrono::duration_cast<std::chrono::milliseconds>(
+ std::chrono::steady_clock::now() - started).count(),
+ watch_list.size(), stop_requested.load());
RecordEvent([this](GameListModel* model) { model->DonePopulating(watch_list); });
processing_completed.Set();
}