diff options
| author | kraibse <33869366+kraibse@users.noreply.github.com> | 2026-11-02 16:26:09 +0100 |
|---|---|---|
| committer | kraibse <33869366+kraibse@users.noreply.github.com> | 2026-11-02 16:26:09 +0100 |
| commit | caed5a34484942d3ecf9b9a84c83f31d1a6e3020 (patch) | |
| tree | a9884e94b012326718fcf119f5298b53911ebb9f /src | |
| parent | ce1b1de6f1beed36958368f89cda466937ff9716 (diff) | |
| parent | 39e1babb1c1a04b8d5fe7c0023654ad458c4ddd9 (diff) | |
Merge branch 'codex/upstream-loading-hotkeys' into nx2
Diffstat (limited to 'src')
| -rw-r--r-- | src/core/loader/deconstructed_rom_directory.cpp | 6 | ||||
| -rw-r--r-- | src/qt_common/config/uisettings.cpp | 3 | ||||
| -rw-r--r-- | src/qt_common/config/uisettings.h | 2 | ||||
| -rw-r--r-- | src/qt_common/game_list/discovery.h | 113 | ||||
| -rw-r--r-- | src/qt_common/game_list/model.cpp | 21 | ||||
| -rw-r--r-- | src/qt_common/game_list/model.h | 2 | ||||
| -rw-r--r-- | src/qt_common/game_list/worker.cpp | 160 | ||||
| -rw-r--r-- | src/qt_common/game_list/worker.h | 7 | ||||
| -rw-r--r-- | src/yuzu/game/game_list.cpp | 4 | ||||
| -rw-r--r-- | src/yuzu/game/game_list.h | 1 | ||||
| -rw-r--r-- | src/yuzu/main_window.cpp | 137 | ||||
| -rw-r--r-- | src/yuzu/main_window.h | 3 |
12 files changed, 328 insertions, 131 deletions
diff --git a/src/core/loader/deconstructed_rom_directory.cpp b/src/core/loader/deconstructed_rom_directory.cpp index 04b05d188f..60cf87db14 100644 --- a/src/core/loader/deconstructed_rom_directory.cpp +++ b/src/core/loader/deconstructed_rom_directory.cpp @@ -6,6 +6,7 @@ #include <algorithm> #include <cctype> +#include <chrono> #include <cstring> #include <map> #include <memory> @@ -103,7 +104,12 @@ FileSys::VirtualFile FindLooseRomFS(const FileSys::VirtualDir& exefs_dir) { return built; } LOG_INFO(Loader, "Using RomFS directory next to the ExeFS"); + const auto started = std::chrono::steady_clock::now(); auto built = FileSys::CreateRomFS(romfs_dir); + LOG_INFO(Loader, "Loose RomFS image ready: elapsed_ms={}", + std::chrono::duration_cast<std::chrono::milliseconds>( + std::chrono::steady_clock::now() - started) + .count()); cached = built; return built; } diff --git a/src/qt_common/config/uisettings.cpp b/src/qt_common/config/uisettings.cpp index 6f1e7df3d5..8d593b8d9e 100644 --- a/src/qt_common/config/uisettings.cpp +++ b/src/qt_common/config/uisettings.cpp @@ -30,7 +30,7 @@ namespace UISettings { // This must be in alphabetical order according to action name as it must have the same order as // UISetting::values.shortcuts, which is alphabetically ordered. // clang-format off -const std::array<Shortcut, 33> default_hotkeys{{ +const std::array<Shortcut, 32> default_hotkeys{{ {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Audio Mute/Unmute")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+M"), std::string("Home+Dpad_Right"), Qt::WindowShortcut, false}}, {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Audio Volume Down")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("-"), std::string("Home+Dpad_Down"), Qt::ApplicationShortcut, true}}, {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Audio Volume Up")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("="), std::string("Home+Dpad_Up"), Qt::ApplicationShortcut, true}}, @@ -45,7 +45,6 @@ const std::array<Shortcut, 33> default_hotkeys{{ {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Exit Eglantine")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+Q"), std::string("Home+Minus"), Qt::WindowShortcut, false}}, {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Fullscreen")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("F11"), std::string("Home+B"), Qt::WindowShortcut, false}}, {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Load File")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+O"), std::string(""), Qt::WidgetWithChildrenShortcut, false}}, - {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Load/Remove Amiibo")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("F2"), std::string("Home+A"), Qt::WidgetWithChildrenShortcut, false}}, {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Browse Public Game Lobby")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+B"), std::string(""), Qt::ApplicationShortcut, false}}, {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Create Room")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+N"), std::string(""), Qt::ApplicationShortcut, false}}, {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Direct Connect to Room")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+C"), std::string(""), Qt::ApplicationShortcut, false}}, diff --git a/src/qt_common/config/uisettings.h b/src/qt_common/config/uisettings.h index a790cfcc22..89185061cc 100644 --- a/src/qt_common/config/uisettings.h +++ b/src/qt_common/config/uisettings.h @@ -244,7 +244,7 @@ void SaveWindowState(); void RestoreWindowState(std::unique_ptr<QtConfig>& qtConfig); // sync with uisettings.cpp -extern const std::array<Shortcut, 33> default_hotkeys; +extern const std::array<Shortcut, 32> default_hotkeys; } // namespace UISettings diff --git a/src/qt_common/game_list/discovery.h b/src/qt_common/game_list/discovery.h new file mode 100644 index 0000000000..8d2054d308 --- /dev/null +++ b/src/qt_common/game_list/discovery.h @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <algorithm> +#include <cctype> +#include <filesystem> +#include <unordered_set> +#include <utility> +#include <vector> + +namespace GameListDiscovery { + +struct Result { + std::vector<std::filesystem::path> files; + std::vector<std::filesystem::path> directories; + std::size_t skipped_romfs{}; + std::size_t errors{}; +}; + +inline bool IsExeFS(const std::filesystem::path& path) { + std::error_code ec; + return std::filesystem::is_regular_file(path / "main", ec) && + std::filesystem::is_regular_file(path / "main.npdm", ec); +} + +template <typename Supported, typename Stopped> +Result Discover(const std::filesystem::path& root, bool deep, Supported supported, + Stopped stopped) { + namespace fs = std::filesystem; + Result result; + std::unordered_set<fs::path> files; + std::unordered_set<fs::path> directories; + const auto add_file = [&](const fs::path& path) { + if (files.insert(path).second) { + result.files.push_back(path); + } + }; + const auto watch = [&](const fs::path& path) { + if (directories.insert(path).second) { + result.directories.push_back(path); + } + }; + const auto probe = [&](const fs::path& path) { + if (IsExeFS(path)) { + watch(path); + add_file(path / "main"); + } + }; + const auto probe_layouts = [&](const fs::path& path) { + for (const auto& content : {path, path / "base"}) { + probe(content / "exefs"); + const auto updates = content / "updates"; + std::error_code ec; + if (!fs::is_directory(updates, ec)) { + continue; + } + watch(updates); + for (fs::directory_iterator it{updates, fs::directory_options::skip_permission_denied, + ec}; + !ec && it != fs::directory_iterator{} && !stopped(); it.increment(ec)) { + probe(it->path() / "exefs"); + } + } + }; + + std::vector<std::pair<fs::path, unsigned>> pending{{root, 0}}; + while (!pending.empty() && !stopped()) { + auto [directory, depth] = std::move(pending.back()); + pending.pop_back(); + watch(directory); + if (IsExeFS(directory)) { + add_file(directory / "main"); + continue; + } + if (!deep) { + probe_layouts(directory); + } + std::error_code ec; + for (fs::directory_iterator it{directory, fs::directory_options::skip_permission_denied, + ec}; + !ec && it != fs::directory_iterator{} && !stopped(); it.increment(ec)) { + const auto& entry = *it; + std::error_code entry_ec; + if (entry.is_directory(entry_ec)) { + if (entry.is_symlink(entry_ec)) { + continue; + } + auto name = entry.path().filename().u8string(); + std::transform(name.begin(), name.end(), name.begin(), [](char8_t c) { + return static_cast<char8_t>(std::tolower(static_cast<unsigned char>(c))); + }); + if (name == u8"romfs" && IsExeFS(directory / "exefs")) { + ++result.skipped_romfs; + continue; + } + if (deep || depth == 0) { + pending.emplace_back(entry.path(), depth + 1); + } + } else if (entry.is_regular_file(entry_ec) && depth == 0 && !deep) { + if (supported(entry.path())) { + add_file(entry.path()); + } + } else if (entry.is_regular_file(entry_ec) && deep && supported(entry.path())) { + add_file(entry.path()); + } + } + result.errors += ec ? 1 : 0; + } + return result; +} + +} // namespace GameListDiscovery diff --git a/src/qt_common/game_list/model.cpp b/src/qt_common/game_list/model.cpp index 4f3916b04d..23d8ec1f43 100644 --- a/src/qt_common/game_list/model.cpp +++ b/src/qt_common/game_list/model.cpp @@ -44,9 +44,21 @@ GameListModel::GameListModel(std::shared_ptr<FileSys::VfsFilesystem> vfs_, setSortRole(GameListItemPath::SortRole); } -GameListModel::~GameListModel() = default; +GameListModel::~GameListModel() { + StopWorker(); +} + +void GameListModel::SetScanEnabled(bool enabled) { + scan_enabled = enabled; + if (!enabled) { + StopWorker(); + } +} void GameListModel::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) { + if (!scan_enabled || system.IsPoweredOn()) { + return; + } emit PopulatingStarted(); StopWorker(); @@ -62,11 +74,14 @@ void GameListModel::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) { } void GameListModel::WorkerEvent() { - current_worker->ProcessEvents(this); + if (current_worker) { + current_worker->ProcessEvents(this); + } } void GameListModel::AddDirEntry(GameListDir* entry_items) { if (m_flat) { + delete entry_items; return; } invisibleRootItem()->appendRow(entry_items); @@ -172,7 +187,7 @@ void GameListModel::StopWorker() { void GameListModel::Repopulate() { // CreateFactories replaces the content providers a running game reads from. Stopping // emulation repopulates the list anyway, so skip watcher-driven reloads until then. - if (QtCommon::system->IsPoweredOn()) { + if (!scan_enabled || QtCommon::system->IsPoweredOn()) { LOG_INFO(Frontend, "Emulation is running, deferring game list reload."); return; } diff --git a/src/qt_common/game_list/model.h b/src/qt_common/game_list/model.h index b850895d2f..e664623640 100644 --- a/src/qt_common/game_list/model.h +++ b/src/qt_common/game_list/model.h @@ -51,6 +51,7 @@ public: void PopulateAsync(QVector<UISettings::GameDir>& game_dirs); void WorkerEvent(); + void SetScanEnabled(bool enabled); bool IsEmpty() const; @@ -81,6 +82,7 @@ private: void StopWorker(); bool m_flat = false; + bool scan_enabled = true; std::shared_ptr<FileSys::VfsFilesystem> vfs; FileSys::ManualContentProvider* provider; diff --git a/src/qt_common/game_list/worker.cpp b/src/qt_common/game_list/worker.cpp index 48cfe523b3..46460c1f66 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) { @@ -477,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 && @@ -509,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) { @@ -596,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(); } diff --git a/src/qt_common/game_list/worker.h b/src/qt_common/game_list/worker.h index cbc1fa7021..b627d9eb94 100644 --- a/src/qt_common/game_list/worker.h +++ b/src/qt_common/game_list/worker.h @@ -8,8 +8,10 @@ #include <atomic> #include <deque> +#include <filesystem> #include <memory> #include <string> +#include <vector> #include <QList> #include <QObject> @@ -28,6 +30,7 @@ class System; class GameListDir; class GameListModel; class QStandardItem; +enum class GameListItemType; namespace FileSys { class NCA; @@ -73,14 +76,14 @@ private: void RecordEvent(F&& func); private: - void AddTitlesToGameList(GameListDir* parent_dir); + void AddTitlesToGameList(GameListDir* parent_dir, GameListItemType parent_type); enum class ScanTarget { FillManualContentProvider, PopulateGameList, }; - void ScanFileSystem(ScanTarget target, const std::string& dir_path, bool deep_scan, + void ScanFileSystem(ScanTarget target, const std::vector<std::filesystem::path>& files, GameListDir* parent_dir); std::shared_ptr<FileSys::VfsFilesystem> vfs; diff --git a/src/yuzu/game/game_list.cpp b/src/yuzu/game/game_list.cpp index 8e3eb7bc52..d2613697bf 100644 --- a/src/yuzu/game/game_list.cpp +++ b/src/yuzu/game/game_list.cpp @@ -160,6 +160,10 @@ void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) { item_model->PopulateAsync(game_dirs); } +void GameList::SetScanEnabled(bool enabled) { + item_model->SetScanEnabled(enabled); +} + void GameList::SaveInterfaceLayout() { tree_view->SaveInterfaceLayout(); } diff --git a/src/yuzu/game/game_list.h b/src/yuzu/game/game_list.h index 67713691fd..b33dfdd9c8 100644 --- a/src/yuzu/game/game_list.h +++ b/src/yuzu/game/game_list.h @@ -76,6 +76,7 @@ public: bool IsEmpty() const; void PopulateAsync(QVector<UISettings::GameDir>& game_dirs); + void SetScanEnabled(bool enabled); void SaveInterfaceLayout(); void LoadInterfaceLayout(); diff --git a/src/yuzu/main_window.cpp b/src/yuzu/main_window.cpp index 2b600946bc..70000a22c9 100644 --- a/src/yuzu/main_window.cpp +++ b/src/yuzu/main_window.cpp @@ -73,8 +73,11 @@ #include <QClipboard> #include <QDesktopServices> #include <QDir> +#include <QElapsedTimer> +#include <QEventLoop> #include <QFileDialog> #include <QFileInfo> +#include <QFutureWatcher> #include <QGuiApplication> #include <QInputDialog> #include <QMimeData> @@ -1293,7 +1296,6 @@ void MainWindow::InitializeHotkeys() { hotkey_registry.LoadHotkeys(); LinkActionShortcut(ui->action_Load_File, QStringLiteral("Load File")); - LinkActionShortcut(ui->action_Load_Amiibo, QStringLiteral("Load/Remove Amiibo")); LinkActionShortcut(ui->action_Exit, QStringLiteral("Exit Eglantine")); LinkActionShortcut(ui->action_Restart, QStringLiteral("Restart Emulation")); LinkActionShortcut(ui->action_Pause, QStringLiteral("Continue/Pause Emulation")); @@ -1833,24 +1835,6 @@ bool MainWindow::LoadROM(const QString& filename, Service::AM::FrontendAppletPar const Core::SystemResultStatus result{ QtCommon::system->Load(*render_window, filename.toStdString(), params)}; - const auto drd_callout = (UISettings::values.callout_flags.GetValue() & - static_cast<u32>(CalloutFlag::DRDDeprecation)) == 0; - - if (result == Core::SystemResultStatus::Success && - QtCommon::system->GetAppLoader().GetFileType() == - Loader::FileType::DeconstructedRomDirectory && - drd_callout) { - UISettings::values.callout_flags = UISettings::values.callout_flags.GetValue() | - static_cast<u32>(CalloutFlag::DRDDeprecation); - QMessageBox::warning( - this, tr("Warning: Outdated Game Format"), - tr("You are using the deconstructed ROM directory format for this game, which is an " - "outdated format that has been superseded by others such as NCA, NAX, XCI, or " - "NSP. Deconstructed ROM directories lack icons, metadata, and update " - "support.<br>For an explanation of the various Switch formats Eglantine supports, " - "out our user handbook. This message will not be shown again.")); - } - if (result != Core::SystemResultStatus::Success) { switch (result) { case Core::SystemResultStatus::ErrorGetLoader: @@ -1920,6 +1904,31 @@ void MainWindow::BootGame(const QString& filename, Service::AM::FrontendAppletPa "converted.")); return; } + if (boot_in_progress || close_pending) { + return; + } + boot_in_progress = true; + game_list->SetScanEnabled(false); + QElapsedTimer boot_timer; + boot_timer.start(); + SCOPE_EXIT { + boot_in_progress = false; + LOG_INFO(Frontend, "Launch finished: elapsed_ms={} running={}", boot_timer.elapsed(), + emulation_running); + if (!emulation_running && !close_pending) { + game_list->SetScanEnabled(true); + game_list->PopulateAsync(UISettings::values.game_dirs); + } + if (close_pending) { + QTimer::singleShot(0, this, &QWidget::close); + } + }; + if (emulation_running) { + ShutdownGame(); + } + if (close_pending) { + return; + } LOG_INFO(Frontend, "Eglantine starting..."); if (params.program_id == 0 || @@ -1973,9 +1982,43 @@ void MainWindow::BootGame(const QString& filename, Service::AM::FrontendAppletPa // behavior of asking. user_flag_cmd_line = false; + FileSys::VirtualFile prepared_romfs; + if (loader && loader->GetFileType() == Loader::FileType::DeconstructedRomDirectory) { + QProgressDialog progress(tr("Preparing game files..."), QString{}, 0, 0, this); + progress.setWindowModality(Qt::ApplicationModal); + progress.setWindowFlag(Qt::WindowCloseButtonHint, false); + progress.setCancelButton(nullptr); + progress.setMinimumDuration(0); + QFutureWatcher<FileSys::VirtualFile> watcher; + QEventLoop loop; + connect(&watcher, &QFutureWatcher<FileSys::VirtualFile>::finished, &loop, &QEventLoop::quit); + watcher.setFuture(QtConcurrent::run([&loader] { + FileSys::VirtualFile image; + loader->ReadRomFS(image); + return image; + })); + progress.show(); + loop.exec(); + try { + prepared_romfs = watcher.result(); + } catch (const std::exception& error) { + LOG_ERROR(Frontend, "Loose RomFS preparation failed: {}", error.what()); + QMessageBox::critical(this, tr("Unable to prepare game files"), + tr("Game preparation failed. See the log for details.")); + return; + } + if (close_pending) { + return; + } + } + + QElapsedTimer load_timer; + load_timer.start(); if (!LoadROM(filename, params)) { + LOG_ERROR(Frontend, "Core load failed: elapsed_ms={}", load_timer.elapsed()); return; } + LOG_INFO(Frontend, "Core load completed: elapsed_ms={}", load_timer.elapsed()); QtCommon::system->SetShuttingDown(false); game_list->setDisabled(true); @@ -2110,6 +2153,9 @@ bool MainWindow::OnShutdownBegin() { Settings::values.current_speed_mode = Settings::SpeedMode::Standard; if (QtCommon::system->IsShuttingDown()) { + status_bar_update_timer.stop(); + connect(QtCommon::emu_thread.get(), &QThread::finished, this, + &MainWindow::OnEmulationStopped, Qt::UniqueConnection); return false; } @@ -2120,6 +2166,8 @@ bool MainWindow::OnShutdownBegin() { } QtCommon::system->SetShuttingDown(true); + status_bar_update_timer.stop(); + LOG_INFO(Frontend, "Shutdown requested; waiting for emulation thread"); discord_rpc->Pause(); RequestGameExit(); @@ -2139,7 +2187,8 @@ bool MainWindow::OnShutdownBegin() { shutdown_timer.setSingleShot(true); shutdown_timer.start(shutdown_time); - connect(&shutdown_timer, &QTimer::timeout, this, &MainWindow::OnEmulationStopTimeExpired); + connect(&shutdown_timer, &QTimer::timeout, this, &MainWindow::OnEmulationStopTimeExpired, + Qt::UniqueConnection); // Disable everything to prevent anything from being triggered here ui->action_Pause->setEnabled(false); @@ -2163,14 +2212,16 @@ void MainWindow::OnEmulationStopTimeExpired() { } void MainWindow::OnEmulationStopped() { + if (!emulation_running) { + return; + } + if (QtCommon::emu_thread && QtCommon::emu_thread->isRunning()) { + return; + } shutdown_timer.stop(); if (QtCommon::emu_thread) { QtCommon::emu_thread->disconnect(); - if (!QtCommon::emu_thread->wait(QDeadlineTimer(10'000))) { - LOG_ERROR(Frontend, "Emulation thread failed to stop, terminating it"); - QtCommon::emu_thread->terminate(); - QtCommon::emu_thread->wait(); - } + QtCommon::emu_thread->wait(); QtCommon::emu_thread.reset(); } @@ -2239,6 +2290,13 @@ void MainWindow::OnEmulationStopped() { Settings::RestoreGlobalState(QtCommon::system->IsPoweredOn()); QtCommon::system->HIDCore().ReloadInputDevices(); UpdateStatusButtons(); + LOG_INFO(Frontend, "Emulation cleanup completed"); + if (close_pending) { + QTimer::singleShot(0, this, &QWidget::close); + } else if (!boot_in_progress) { + game_list->SetScanEnabled(true); + game_list->PopulateAsync(UISettings::values.game_dirs); + } } void MainWindow::ShutdownGame() { @@ -2250,6 +2308,13 @@ void MainWindow::ShutdownGame() { play_time_manager->Stop(); OnShutdownBegin(); OnEmulationStopTimeExpired(); + if (QtCommon::emu_thread) { + QEventLoop loop; + connect(QtCommon::emu_thread.get(), &QThread::finished, &loop, &QEventLoop::quit); + if (QtCommon::emu_thread->isRunning()) { + loop.exec(QEventLoop::ExcludeUserInputEvents); + } + } OnEmulationStopped(); } @@ -3082,8 +3147,6 @@ void MainWindow::OnPauseContinueGame() { void MainWindow::OnStopGame() { if (ConfirmShutdownGame()) { play_time_manager->Stop(); - // Update game list to show new play time - game_list->PopulateAsync(UISettings::values.game_dirs); if (OnShutdownBegin()) { OnShutdownBeginDialog(); } else { @@ -4739,10 +4802,28 @@ bool MainWindow::ConfirmClose() { } void MainWindow::closeEvent(QCloseEvent* event) { - if (!ConfirmClose()) { + if (!close_pending && !ConfirmClose()) { event->ignore(); return; } + close_pending = true; + game_list->SetScanEnabled(false); + if (boot_in_progress) { + event->ignore(); + return; + } + if (QtCommon::emu_thread && emulation_running) { + event->ignore(); + if (!QtCommon::emu_thread->isRunning()) { + OnEmulationStopped(); + return; + } + if (OnShutdownBegin()) { + OnShutdownBeginDialog(); + } + return; + } + LOG_INFO(Frontend, "Closing frontend after emulation cleanup"); UpdateUISettings(); game_list->SaveInterfaceLayout(); diff --git a/src/yuzu/main_window.h b/src/yuzu/main_window.h index 766e4d7345..290ecac6fb 100644 --- a/src/yuzu/main_window.h +++ b/src/yuzu/main_window.h @@ -10,6 +10,7 @@ #include <optional> #include <filesystem> +#include <QElapsedTimer> #include <QMainWindow> #include <QMessageBox> #include <QFutureWatcher> @@ -543,6 +544,8 @@ private: // Whether emulation is currently running in yuzu. bool emulation_running = false; + bool boot_in_progress = false; + bool close_pending = false; // The path to the game currently running QString current_game_path; // Whether a user was set on the command line (skips UserSelector if it's forced to show up) |
