aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorkraibse <33869366+kraibse@users.noreply.github.com>2026-11-02 16:26:09 +0100
committerkraibse <33869366+kraibse@users.noreply.github.com>2026-11-02 16:26:09 +0100
commitcaed5a34484942d3ecf9b9a84c83f31d1a6e3020 (patch)
treea9884e94b012326718fcf119f5298b53911ebb9f /src
parentce1b1de6f1beed36958368f89cda466937ff9716 (diff)
parent39e1babb1c1a04b8d5fe7c0023654ad458c4ddd9 (diff)
Merge branch 'codex/upstream-loading-hotkeys' into nx2
Diffstat (limited to 'src')
-rw-r--r--src/core/loader/deconstructed_rom_directory.cpp6
-rw-r--r--src/qt_common/config/uisettings.cpp3
-rw-r--r--src/qt_common/config/uisettings.h2
-rw-r--r--src/qt_common/game_list/discovery.h113
-rw-r--r--src/qt_common/game_list/model.cpp21
-rw-r--r--src/qt_common/game_list/model.h2
-rw-r--r--src/qt_common/game_list/worker.cpp160
-rw-r--r--src/qt_common/game_list/worker.h7
-rw-r--r--src/yuzu/game/game_list.cpp4
-rw-r--r--src/yuzu/game/game_list.h1
-rw-r--r--src/yuzu/main_window.cpp137
-rw-r--r--src/yuzu/main_window.h3
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)