From 13e8c06f44f4bc63a8edb54b40d3c07e953333bc Mon Sep 17 00:00:00 2001 From: collecting Date: Sun, 8 Feb 2026 19:08:59 -0500 Subject: [PATCH] fix: Race Condition w/ Shutdown Logic --- src/citron/game_list.cpp | 7 ++++++ src/citron/game_list.h | 3 +-- src/citron/game_list_worker.cpp | 36 +++++++++++++++++++++------ src/citron/game_list_worker.h | 19 +++++++++------ src/citron/main.cpp | 18 ++++++++++++++ src/common/fs/fs.cpp | 43 ++++++++++++++++++++++----------- src/common/fs/fs.h | 6 ++--- 7 files changed, 99 insertions(+), 33 deletions(-) diff --git a/src/citron/game_list.cpp b/src/citron/game_list.cpp index bd13eae1a..a85c6ca93 100644 --- a/src/citron/game_list.cpp +++ b/src/citron/game_list.cpp @@ -2144,6 +2144,13 @@ void GameList::RefreshGameDirectory() { } } +void GameList::CancelPopulation() { + if (current_worker) { + current_worker->Cancel(); + } + current_worker.reset(); +} + void GameList::ToggleFavorite(u64 program_id) { if (!UISettings::values.favorited_ids.contains(program_id)) { tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(), diff --git a/src/citron/game_list.h b/src/citron/game_list.h index ec919bf25..76a0f2292 100644 --- a/src/citron/game_list.h +++ b/src/citron/game_list.h @@ -27,7 +27,6 @@ #include #include - #include "citron/compatibility_list.h" #include "citron/multiplayer/state.h" #include "citron/play_time_manager.h" @@ -35,7 +34,6 @@ #include "core/core.h" #include "uisettings.h" - class ControllerNavigation; class GameListWorker; class GameListSearchField; @@ -107,6 +105,7 @@ public: void LoadCompatibilityList(); void PopulateAsync(QVector& game_dirs); + void CancelPopulation(); void SaveInterfaceLayout(); void LoadInterfaceLayout(); diff --git a/src/citron/game_list_worker.cpp b/src/citron/game_list_worker.cpp index 0a57cbaa8..894c73508 100644 --- a/src/citron/game_list_worker.cpp +++ b/src/citron/game_list_worker.cpp @@ -21,7 +21,6 @@ #include #include - #include "citron/compatibility_list.h" #include "citron/game_list.h" #include "citron/game_list_p.h" @@ -40,7 +39,6 @@ #include "core/file_sys/submission_package.h" #include "core/loader/loader.h" - namespace { // Structure to hold cached game metadata @@ -485,6 +483,11 @@ GameListWorker::~GameListWorker() { processing_completed.Wait(); } +void GameListWorker::Cancel() { + this->disconnect(); + stop_requested.store(true); +} + void GameListWorker::ProcessEvents(GameList* game_list) { while (true) { std::function func; @@ -542,6 +545,10 @@ void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir, } for (const auto& [slot, game] : installed_games) { + if (stop_requested) { + break; + } + if (slot == ContentProviderUnionSlot::FrontendManual) { continue; } @@ -580,10 +587,16 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa const std::map>& online_stats, int& processed_files, int total_files) { const auto callback = [this, target, parent_dir, &online_stats, &processed_files, - total_files](const std::filesystem::path& path) -> bool { - if (stop_requested) + total_files](const std::filesystem::directory_entry& dir_entry) -> bool { + if (stop_requested) { return false; + } + if (dir_entry.is_directory()) { + return true; + } + + const auto& path = dir_entry.path(); const auto physical_name = Common::FS::PathToUTF8String(path); if (physical_name.find("/nand/") != std::string::npos || @@ -714,7 +727,7 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa if (deep_scan) { Common::FS::IterateDirEntriesRecursively(dir_path, callback, - Common::FS::DirEntryFilter::File); + Common::FS::DirEntryFilter::All); } else { Common::FS::IterateDirEntries(dir_path, callback, Common::FS::DirEntryFilter::File); } @@ -745,7 +758,16 @@ void GameListWorker::run() { if (game_dir.path == "SDMC" || game_dir.path == "UserNAND" || game_dir.path == "SysNAND") continue; - auto count_callback = [&](const std::filesystem::path& path) -> bool { + auto count_callback = [&](const std::filesystem::directory_entry& dir_entry) -> bool { + if (stop_requested) { + return false; + } + + if (dir_entry.is_directory()) { + return true; + } + + const auto& path = dir_entry.path(); const std::string physical_name = Common::FS::PathToUTF8String(path); if (HasSupportedFileExtension(physical_name)) { total_files++; @@ -755,7 +777,7 @@ void GameListWorker::run() { if (game_dir.deep_scan) { Common::FS::IterateDirEntriesRecursively(game_dir.path, count_callback, - Common::FS::DirEntryFilter::File); + Common::FS::DirEntryFilter::All); } else { Common::FS::IterateDirEntries(game_dir.path, count_callback, Common::FS::DirEntryFilter::File); diff --git a/src/citron/game_list_worker.h b/src/citron/game_list_worker.h index 8ad673eeb..8377f6500 100644 --- a/src/citron/game_list_worker.h +++ b/src/citron/game_list_worker.h @@ -6,24 +6,26 @@ #include #include +#include // Required for the online_stats map #include #include -#include // Required for the online_stats map #include // Required for std::pair + #include #include #include #include -#include "common/thread.h" #include "citron/compatibility_list.h" -#include "citron/play_time_manager.h" #include "citron/multiplayer/state.h" +#include "citron/play_time_manager.h" +#include "common/thread.h" #include "network/announce_multiplayer_session.h" + namespace Core { - class System; +class System; } class GameList; @@ -31,9 +33,9 @@ class GameListDir; // Forward declare GameListDir class QStandardItem; namespace FileSys { - class NCA; - class VfsFilesystem; - class ManualContentProvider; +class NCA; +class VfsFilesystem; +class ManualContentProvider; } // namespace FileSys /** @@ -62,6 +64,9 @@ public: /// Starts the processing of directory tree information. void run() override; + /// Request the worker to stop. + void Cancel(); + public: /** * Synchronously processes any events queued by the worker. diff --git a/src/citron/main.cpp b/src/citron/main.cpp index 2e6ea942e..c374f0630 100644 --- a/src/citron/main.cpp +++ b/src/citron/main.cpp @@ -123,6 +123,8 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual #ifdef ARCHITECTURE_x86_64 #include "common/x64/cpu_detect.h" #endif +#include "audio_core/audio_core.h" +#include "audio_core/sink/sink.h" #include "citron/about_dialog.h" #include "citron/bootmanager.h" #include "citron/compatdb.h" @@ -2100,6 +2102,8 @@ void GMainWindow::BootGame(const QString& filename, Service::AM::FrontendAppletP StartGameType type) { LOG_INFO(Frontend, "citron starting..."); + game_list->CancelPopulation(); + RegisterAutoloaderContents(); if (params.program_id == 0 || @@ -2342,9 +2346,15 @@ void GMainWindow::OnEmulationStopped() { LOG_INFO(Frontend, "Mirroring: Emulation stopped. Re-arming startup sync for next game list refresh."); + // This is necessary to stop the game list worker from accessing the filesystem. + game_list->CancelPopulation(); + // This is necessary to reset the in-memory state for the next launch. system->GetFileSystemController().CreateFactories(*vfs, true); + // Refresh the game list now that the filesystem is valid again. + game_list->PopulateAsync(UISettings::values.game_dirs); + discord_rpc->Update(); #ifdef __unix__ @@ -5746,6 +5756,14 @@ void GMainWindow::UpdateVolumeUI() { volume_button->setChecked(true); volume_button->setText(tr("VOLUME: %1%", "Volume percentage (e.g. 50%)").arg(volume_value)); } + float volume_scale = static_cast(volume_value) / 100.0f; + if (Settings::values.audio_muted) { + volume_scale = 0.0f; + } + + if (system && system->IsPoweredOn()) { + system->AudioCore().GetOutputSink().SetSystemVolume(volume_scale); + } } void GMainWindow::UpdateStatusButtons() { diff --git a/src/common/fs/fs.cpp b/src/common/fs/fs.cpp index 71ac8c27a..0efd0a101 100644 --- a/src/common/fs/fs.cpp +++ b/src/common/fs/fs.cpp @@ -437,18 +437,17 @@ void IterateDirEntries(const std::filesystem::path& path, const DirEntryCallable std::error_code status_ec; const auto st = entry.status(status_ec); - if (status_ec) continue; + if (status_ec) + continue; - if (True(filter & DirEntryFilter::File) && - st.type() == fs::file_type::regular) { + if (True(filter & DirEntryFilter::File) && st.type() == fs::file_type::regular) { if (!callback(entry)) { callback_error = true; break; } } - if (True(filter & DirEntryFilter::Directory) && - st.type() == fs::file_type::directory) { + if (True(filter & DirEntryFilter::Directory) && st.type() == fs::file_type::directory) { if (!callback(entry)) { callback_error = true; break; @@ -467,14 +466,16 @@ void IterateDirEntries(const std::filesystem::path& path, const DirEntryCallable PathToUTF8String(path)); } -void IterateDirEntriesRecursivelyInternal(const std::filesystem::path& path, +bool IterateDirEntriesRecursivelyInternal(const std::filesystem::path& path, const DirEntryCallable& callback, DirEntryFilter filter, int depth) { - if (depth > 12) return; + if (depth > 12) + return true; std::error_code ec; auto it = fs::directory_iterator(path, ec); - if (ec) return; + if (ec) + return true; while (it != fs::directory_iterator() && !ec) { const auto& entry = *it; @@ -483,8 +484,8 @@ void IterateDirEntriesRecursivelyInternal(const std::filesystem::path& path, const std::string filename = entry.path().filename().string(); if (filename[0] == '$' || filename == "Windows" || filename == "Program Files" || filename == "Program Files (x86)" || filename == "System Volume Information" || - filename == "ProgramData" || filename == "Application Data" || - filename == "Users" || filename == "SteamLibrary") { + filename == "ProgramData" || filename == "Application Data" || filename == "Users" || + filename == "SteamLibrary") { it.increment(ec); continue; } @@ -492,15 +493,29 @@ void IterateDirEntriesRecursivelyInternal(const std::filesystem::path& path, std::error_code status_ec; if (entry.is_directory(status_ec)) { - if (True(filter & DirEntryFilter::Directory)) { if (!callback(entry)) break; } - IterateDirEntriesRecursivelyInternal(entry.path(), callback, filter, depth + 1); + if (True(filter & DirEntryFilter::Directory)) { + if (!callback(entry)) { + return false; + } + } + if (!IterateDirEntriesRecursivelyInternal(entry.path(), callback, filter, depth + 1)) { + return false; + } } else { - if (True(filter & DirEntryFilter::File)) { if (!callback(entry)) break; } + if (True(filter & DirEntryFilter::File)) { + if (!callback(entry)) { + return false; + } + } } it.increment(ec); - if (ec) { ec.clear(); break; } + if (ec) { + ec.clear(); + break; + } } + return true; } void IterateDirEntriesRecursively(const std::filesystem::path& path, diff --git a/src/common/fs/fs.h b/src/common/fs/fs.h index 42b66ea8d..f2688302f 100644 --- a/src/common/fs/fs.h +++ b/src/common/fs/fs.h @@ -400,9 +400,9 @@ void IterateDirEntriesRecursively(const std::filesystem::path& path, const DirEntryCallable& callback, DirEntryFilter filter = DirEntryFilter::All); -void IterateDirEntriesRecursivelyInternal(const std::filesystem::path& path, - const DirEntryCallable& callback, - DirEntryFilter filter, int depth); +bool IterateDirEntriesRecursivelyInternal(const std::filesystem::path& path, + const DirEntryCallable& callback, DirEntryFilter filter, + int depth); #ifdef _WIN32 template