Merge pull request 'fs: Fix directory scanning crashes and optimize library performance & Include a Progress Bar' (#72) from fs/linux-ntfs-fix into main

Reviewed-on: https://git.citron-emu.org/Citron/Emulator/pulls/72
This commit is contained in:
Zephyron
2025-12-21 02:57:33 +00:00
10 changed files with 351 additions and 503 deletions

View File

@@ -72,6 +72,9 @@ void ConfigureFilesystem::SetConfiguration() {
ui->cache_game_list->setChecked(UISettings::values.cache_game_list.GetValue());
ui->prompt_for_autoloader->setChecked(UISettings::values.prompt_for_autoloader.GetValue());
// NCA Scanning Toggle
ui->scan_nca->setChecked(UISettings::values.scan_nca.GetValue());
#ifdef __linux__
ui->enable_backups_checkbox->setChecked(UISettings::values.updater_enable_backups.GetValue());
const std::string& backup_path = UISettings::values.updater_backup_path.GetValue();
@@ -100,6 +103,9 @@ void ConfigureFilesystem::ApplyConfiguration() {
UISettings::values.cache_game_list = ui->cache_game_list->isChecked();
UISettings::values.prompt_for_autoloader = ui->prompt_for_autoloader->isChecked();
// NCA Scanning Toggle
UISettings::values.scan_nca = ui->scan_nca->isChecked();
#ifdef __linux__
UISettings::values.updater_enable_backups = ui->enable_backups_checkbox->isChecked();
const bool new_custom_backup_enabled = ui->custom_backup_location_checkbox->isChecked();

View File

@@ -7,294 +7,90 @@
<x>0</x>
<y>0</y>
<width>453</width>
<height>561</height>
<height>650</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<property name="accessibleName">
<string>Filesystem</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Storage Directories</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>NAND</string>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QToolButton" name="nand_directory_button">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLineEdit" name="nand_directory_edit"/>
</item>
<item row="1" column="2">
<widget class="QLineEdit" name="sdmc_directory_edit"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>SD Card</string>
</property>
</widget>
</item>
<item row="1" column="3">
<widget class="QToolButton" name="sdmc_directory_button">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="0" column="1">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Maximum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>60</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Gamecard</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="2" column="1">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Path</string>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QLineEdit" name="gamecard_path_edit"/>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="gamecard_inserted">
<property name="text">
<string>Inserted</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="gamecard_current_game">
<property name="text">
<string>Current Game</string>
</property>
</widget>
</item>
<item row="2" column="3">
<widget class="QToolButton" name="gamecard_path_button">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_4">
<property name="title">
<string>Patch Manager</string>
</property>
<layout class="QGridLayout" name="gridLayout_4">
<item row="1" column="2">
<widget class="QLineEdit" name="load_path_edit"/>
</item>
<item row="0" column="2">
<widget class="QLineEdit" name="dump_path_edit"/>
</item>
<item row="0" column="3">
<widget class="QToolButton" name="dump_path_button">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="1" column="3">
<widget class="QToolButton" name="load_path_button">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="2" column="0" colspan="4">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QCheckBox" name="dump_nso">
<property name="text">
<string>Dump Decompressed NSOs</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="dump_exefs">
<property name="text">
<string>Dump ExeFS</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
<string>Mod Load Root</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Dump Root</string>
</property>
</widget>
</item>
<item row="0" column="1">
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="autoloader_group">
<property name="title">
<string>Autoloader</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_Autoloader">
<item>
<widget class="QCheckBox" name="prompt_for_autoloader">
<property name="text">
<string>Prompt to run Autoloader when a new game directory is added</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="run_autoloader_button">
<property name="text">
<string>Run Autoloader Now</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="updater_group">
<property name="title">
<string>Updater</string>
</property>
<layout class="QGridLayout" name="gridLayout_updater">
<item row="0" column="0" colspan="2">
<widget class="QCheckBox" name="enable_backups_checkbox">
<property name="text">
<string>Enable AppImage Backups</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="custom_backup_location_checkbox">
<property name="text">
<string>Use Custom Backup Location for AppImage Updates</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLineEdit" name="custom_backup_location_edit"/>
</item>
<item row="2" column="1">
<widget class="QToolButton" name="custom_backup_location_button">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_5">
<property name="title">
<string>Caching</string>
</property>
<layout class="QGridLayout" name="gridLayout_5">
<item row="0" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QCheckBox" name="cache_game_list">
<property name="text">
<string>Cache Game List Metadata</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="reset_game_list_cache">
<property name="text">
<string>Reset Metadata Cache</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Storage Directories</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0"><widget class="QLabel" name="label"><property name="text"><string>NAND</string></property></widget></item>
<item row="0" column="2"><widget class="QLineEdit" name="nand_directory_edit"/></item>
<item row="0" column="3"><widget class="QToolButton" name="nand_directory_button"><property name="text"><string>...</string></property></widget></item>
<item row="1" column="0"><widget class="QLabel" name="label_2"><property name="text"><string>SD Card</string></property></widget></item>
<item row="1" column="2"><widget class="QLineEdit" name="sdmc_directory_edit"/></item>
<item row="1" column="3"><widget class="QToolButton" name="sdmc_directory_button"><property name="text"><string>...</string></property></widget></item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
<widget class="QGroupBox" name="groupBox_2">
<property name="title"><string>Gamecard</string></property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="1"><widget class="QCheckBox" name="gamecard_inserted"><property name="text"><string>Inserted</string></property></widget></item>
<item row="1" column="1"><widget class="QCheckBox" name="gamecard_current_game"><property name="text"><string>Current Game</string></property></widget></item>
<item row="2" column="1"><widget class="QLabel" name="label_3"><property name="text"><string>Path</string></property></widget></item>
<item row="2" column="2"><widget class="QLineEdit" name="gamecard_path_edit"/></item>
<item row="2" column="3"><widget class="QToolButton" name="gamecard_path_button"><property name="text"><string>...</string></property></widget></item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_4">
<property name="title"><string>Patch Manager</string></property>
<layout class="QGridLayout" name="gridLayout_4">
<item row="0" column="0"><widget class="QLabel" name="label_8"><property name="text"><string>Dump Root</string></property></widget></item>
<item row="0" column="2"><widget class="QLineEdit" name="dump_path_edit"/></item>
<item row="0" column="3"><widget class="QToolButton" name="dump_path_button"><property name="text"><string>...</string></property></widget></item>
<item row="1" column="0"><widget class="QLabel" name="label_9"><property name="text"><string>Mod Load Root</string></property></widget></item>
<item row="1" column="2"><widget class="QLineEdit" name="load_path_edit"/></item>
<item row="1" column="3"><widget class="QToolButton" name="load_path_button"><property name="text"><string>...</string></property></widget></item>
<item row="2" column="0" colspan="4">
<layout class="QHBoxLayout" name="horizontalLayout">
<item><widget class="QCheckBox" name="dump_nso"><property name="text"><string>Dump Decompressed NSOs</string></property></widget></item>
<item><widget class="QCheckBox" name="dump_exefs"><property name="text"><string>Dump ExeFS</string></property></widget></item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="autoloader_group">
<property name="title"><string>Autoloader</string></property>
<layout class="QVBoxLayout">
<item><widget class="QCheckBox" name="prompt_for_autoloader"><property name="text"><string>Prompt to run Autoloader when a new game directory is added</string></property></widget></item>
<item><widget class="QPushButton" name="run_autoloader_button"><property name="text"><string>Run Autoloader Now</string></property></widget></item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="updater_group">
<property name="title"><string>Updater</string></property>
<layout class="QGridLayout" name="gridLayout_updater">
<item row="0" column="0" colspan="2"><widget class="QCheckBox" name="enable_backups_checkbox"><property name="text"><string>Enable AppImage Backups</string></property></widget></item>
<item row="1" column="0"><widget class="QCheckBox" name="custom_backup_location_checkbox"><property name="text"><string>Use Custom Backup Location</string></property></widget></item>
<item row="2" column="0"><widget class="QLineEdit" name="custom_backup_location_edit"/></item>
<item row="2" column="1"><widget class="QToolButton" name="custom_backup_location_button"><property name="text"><string>...</string></property></widget></item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_5">
<property name="title"><string>Caching &amp; Scanning</string></property>
<layout class="QGridLayout" name="gridLayout_5">
<item row="0" column="0"><widget class="QCheckBox" name="cache_game_list"><property name="text"><string>Cache Game List Metadata</string></property></widget></item>
<item row="0" column="1"><widget class="QPushButton" name="reset_game_list_cache"><property name="text"><string>Reset Metadata Cache</string></property></widget></item>
<item row="1" column="0" colspan="2"><widget class="QCheckBox" name="scan_nca"><property name="text"><string>Scan for .nca files (Advanced: Significantly slows down scanning)</string></property></widget></item>
</layout>
</widget>
</item>
<item><spacer name="verticalSpacer"><property name="orientation"><enum>Qt::Vertical</enum></property></spacer></item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -21,6 +21,7 @@
#include <QPainter>
#include <QPainterPath>
#include <QProgressDialog>
#include <QProgressBar>
#include <QScrollBar>
#include <QStyle>
#include <QThreadPool>
@@ -660,6 +661,16 @@ play_time_manager{play_time_manager_}, system{system_} {
));
connect(btn_sort_az, &QToolButton::clicked, this, &GameList::ToggleSortOrder);
// Create progress bar
progress_bar = new QProgressBar(this);
progress_bar->setVisible(false);
progress_bar->setFixedHeight(4);
progress_bar->setTextVisible(false);
progress_bar->setStyleSheet(QStringLiteral(
"QProgressBar { border: none; background: transparent; } "
"QProgressBar::chunk { background-color: #0078d4; }"
));
// Add widgets to toolbar
toolbar_layout->addWidget(btn_list_view);
toolbar_layout->addWidget(btn_grid_view);
@@ -671,6 +682,7 @@ play_time_manager{play_time_manager_}, system{system_} {
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
layout->addWidget(toolbar);
layout->addWidget(progress_bar);
layout->addWidget(tree_view);
layout->addWidget(list_view);
setLayout(layout);
@@ -749,16 +761,21 @@ void GameList::UpdateOnlineStatus() {
// Run the blocking network call in a background thread using QtConcurrent
QFuture<std::map<u64, std::pair<int, int>>> future = QtConcurrent::run([session]() {
std::map<u64, std::pair<int, int>> stats;
AnnounceMultiplayerRoom::RoomList room_list = session->GetRoomList();
for (const auto& room : room_list) {
u64 game_id = room.information.preferred_game.id;
if (game_id != 0) {
stats[game_id].first += room.members.size();
stats[game_id].second++;
try {
std::map<u64, std::pair<int, int>> stats;
AnnounceMultiplayerRoom::RoomList room_list = session->GetRoomList();
for (const auto& room : room_list) {
u64 game_id = room.information.preferred_game.id;
if (game_id != 0) {
stats[game_id].first += (int)room.members.size();
stats[game_id].second++;
}
}
return stats;
} catch (const std::exception& e) {
LOG_ERROR(Frontend, "Exception in Online Status thread: {}", e.what());
return std::map<u64, std::pair<int, int>>{};
}
return stats;
});
online_status_watcher->setFuture(future);
@@ -859,6 +876,9 @@ bool GameList::IsEmpty() const {
}
void GameList::DonePopulating(const QStringList& watch_list) {
if (progress_bar) {
progress_bar->setVisible(false);
}
emit ShowList(!IsEmpty());
item_model->invisibleRootItem()->appendRow(new GameListAddDir());
item_model->invisibleRootItem()->insertRow(0, new GameListFavorites());
@@ -1283,7 +1303,9 @@ QStandardItemModel* GameList::GetModel() const {
}
void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) {
UpdateProgressBarColor();
tree_view->setEnabled(false);
emit ShowList(true);
tree_view->setColumnHidden(COLUMN_ADD_ONS, !UISettings::values.show_add_ons);
tree_view->setColumnHidden(COLUMN_COMPATIBILITY, !UISettings::values.show_compat);
tree_view->setColumnHidden(COLUMN_FILE_TYPE, !UISettings::values.show_types);
@@ -1293,8 +1315,19 @@ void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) {
current_worker.reset();
item_model->removeRows(0, item_model->rowCount());
search_field->clear();
if (progress_bar) {
progress_bar->setValue(0);
progress_bar->setVisible(true);
}
current_worker = std::make_unique<GameListWorker>(vfs, provider, game_dirs, compatibility_list, play_time_manager, system, main_window->GetMultiplayerState()->GetSession());
connect(current_worker.get(), &GameListWorker::DataAvailable, this, &GameList::WorkerEvent, Qt::QueuedConnection);
if (progress_bar) {
connect(current_worker.get(), &GameListWorker::ProgressUpdated, progress_bar, &QProgressBar::setValue, Qt::QueuedConnection);
}
QThreadPool::globalInstance()->start(current_worker.get());
}
@@ -1312,15 +1345,16 @@ void GameList::LoadInterfaceLayout() {
}
const QStringList GameList::supported_file_extensions = {
QStringLiteral("nso"), QStringLiteral("nro"), QStringLiteral("nca"),
QStringLiteral("xci"), QStringLiteral("nsp"), QStringLiteral("kip")};
QStringLiteral("xci"), QStringLiteral("nsp"),
QStringLiteral("nso"), QStringLiteral("nro"), QStringLiteral("kip")
};
void GameList::RefreshGameDirectory() {
if (!UISettings::values.game_dirs.empty() && current_worker != nullptr) {
LOG_INFO(Frontend, "Change detected in the games directory. Reloading game list.");
PopulateAsync(UISettings::values.game_dirs);
}
void GameList::RefreshGameDirectory() {
if (!UISettings::values.game_dirs.empty() && current_worker != nullptr) {
LOG_INFO(Frontend, "Change detected in the games directory. Reloading game list.");
PopulateAsync(UISettings::values.game_dirs);
}
}
void GameList::ToggleFavorite(u64 program_id) {
if (!UISettings::values.favorited_ids.contains(program_id)) {
@@ -1588,3 +1622,26 @@ const QStringList GameList::supported_file_extensions = {
}
btn_sort_az->setIcon(sort_icon);
}
void GameList::UpdateProgressBarColor() {
if (!progress_bar) return;
// Convert the Hex String from settings to a QColor
QColor accent(QString::fromStdString(UISettings::values.accent_color.GetValue()));
if (UISettings::values.enable_rainbow_mode.GetValue()) {
progress_bar->setStyleSheet(QStringLiteral(
"QProgressBar { border: none; background: transparent; } "
"QProgressBar::chunk { "
"background: qlineargradient(x1:0, y1:0, x2:1, y2:0, "
"stop:0 #ff0000, stop:0.16 #ffff00, stop:0.33 #00ff00, "
"stop:0.5 #00ffff, stop:0.66 #0000ff, stop:0.83 #ff00ff, stop:1 #ff0000); "
"}"
));
} else {
progress_bar->setStyleSheet(QStringLiteral(
"QProgressBar { border: none; background: transparent; } "
"QProgressBar::chunk { background-color: %1; }"
).arg(accent.name()));
}
}

View File

@@ -14,6 +14,7 @@
#include <QListView>
#include <QPushButton>
#include <QSlider>
#include <QProgressBar>
#include <QStandardItemModel>
#include <QString>
#include <QTimer>
@@ -146,6 +147,7 @@ public slots:
void OnConfigurationChanged();
private slots:
void UpdateProgressBarColor();
void OnItemExpanded(const QModelIndex& item);
void OnTextChanged(const QString& new_text);
void OnFilterCloseClicked();
@@ -201,6 +203,7 @@ private:
QListView* list_view = nullptr;
QStandardItemModel* item_model = nullptr;
std::unique_ptr<GameListWorker> current_worker;
QProgressBar* progress_bar = nullptr;
QFileSystemWatcher* watcher = nullptr;
ControllerNavigation* controller_navigation = nullptr;
CompatibilityList compatibility_list;

View File

@@ -349,7 +349,19 @@ void GetMetadataFromControlNCA(const FileSys::PatchManager& patch_manager, const
bool HasSupportedFileExtension(const std::string& file_name) {
const QFileInfo file = QFileInfo(QString::fromStdString(file_name));
return GameList::supported_file_extensions.contains(file.suffix(), Qt::CaseInsensitive);
const QString suffix = file.suffix().toLower();
// 1. Check if it's a standard game container (.nsp, .xci, etc.)
if (GameList::supported_file_extensions.contains(suffix)) {
return true;
}
// 2. Only allow .nca if the user explicitly enabled it in the UI Settings
if (suffix == QStringLiteral("nca") && UISettings::values.scan_nca.GetValue()) {
return true;
}
return false;
}
bool IsExtractedNCAMain(const std::string& file_name) {
@@ -551,145 +563,116 @@ void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir, const std::map
}
void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_path, bool deep_scan,
GameListDir* parent_dir, const std::map<u64, std::pair<int, int>>& online_stats) {
const auto callback = [this, target, parent_dir, &online_stats](const std::filesystem::path& path) -> bool {
if (stop_requested) {
// Breaks the callback loop.
return false;
}
GameListDir* parent_dir, const std::map<u64, std::pair<int, int>>& 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) return false;
const auto physical_name = Common::FS::PathToUTF8String(path);
const auto is_dir = Common::FS::IsDir(path);
if (!is_dir &&
(HasSupportedFileExtension(physical_name) || IsExtractedNCAMain(physical_name))) {
// Try to get cached metadata first
const auto* cached_metadata = GetCachedGameMetadata(physical_name);
const auto file = vfs->OpenFile(physical_name, FileSys::OpenMode::Read);
if (!file) {
return true;
}
auto loader = Loader::GetLoader(system, file);
if (!loader) {
return true;
}
const auto file_type = loader->GetFileType();
if (file_type == Loader::FileType::Unknown || file_type == Loader::FileType::Error) {
return true;
}
u64 program_id = 0;
const auto res2 = loader->ReadProgramId(program_id);
if (target == ScanTarget::FillManualContentProvider) {
if (res2 == Loader::ResultStatus::Success && file_type == Loader::FileType::NCA) {
provider->AddEntry(FileSys::TitleType::Application,
FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()),
program_id, file);
} else if (res2 == Loader::ResultStatus::Success &&
(file_type == Loader::FileType::XCI ||
file_type == Loader::FileType::NSP)) {
const auto nsp = file_type == Loader::FileType::NSP
? std::make_shared<FileSys::NSP>(file)
: FileSys::XCI{file}.GetSecurePartitionNSP();
for (const auto& title : nsp->GetNCAs()) {
for (const auto& entry : title.second) {
provider->AddEntry(entry.first.first, entry.first.second, title.first,
entry.second->GetBaseFile());
}
}
}
} else {
std::vector<u64> program_ids;
loader->ReadProgramIds(program_ids);
if (res2 == Loader::ResultStatus::Success && program_ids.size() > 1 &&
(file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP)) {
for (const auto id : program_ids) {
loader = Loader::GetLoader(system, file, id);
if (!loader) {
continue;
}
std::vector<u8> icon;
[[maybe_unused]] const auto res1 = loader->ReadIcon(icon);
std::string name = " ";
[[maybe_unused]] const auto res3 = loader->ReadTitle(name);
const FileSys::PatchManager patch{id, system.GetFileSystemController(),
system.GetContentProvider()};
auto entry = MakeGameListEntry(
physical_name, name, Common::FS::GetSize(physical_name), icon, *loader,
id, compatibility_list, play_time_manager, patch, online_stats);
RecordEvent(
[=](GameList* game_list) { game_list->AddEntry(entry, parent_dir); });
}
} else {
// Use cached metadata if available, otherwise read from file
std::vector<u8> icon;
std::string name = " ";
std::size_t file_size = 0;
if (cached_metadata && cached_metadata->program_id == program_id) {
// Use cached data
icon = cached_metadata->icon;
name = cached_metadata->title;
file_size = cached_metadata->file_size;
} else {
// Read from file
[[maybe_unused]] const auto res1 = loader->ReadIcon(icon);
[[maybe_unused]] const auto res3 = loader->ReadTitle(name);
file_size = Common::FS::GetSize(physical_name);
// Cache it for next time
if (res2 == Loader::ResultStatus::Success) {
CacheGameMetadata(physical_name, program_id, file_type, file_size, name, icon);
}
}
const FileSys::PatchManager patch{program_id, system.GetFileSystemController(),
system.GetContentProvider()};
auto entry = MakeGameListEntry(
physical_name, name, file_size, icon, *loader,
program_id, compatibility_list, play_time_manager, patch, online_stats);
RecordEvent(
[=](GameList* game_list) { game_list->AddEntry(entry, parent_dir); });
}
}
} else if (is_dir) {
watch_list.append(QString::fromStdString(physical_name));
if (physical_name.find("/nand/") != std::string::npos ||
physical_name.find("\\nand\\") != std::string::npos ||
physical_name.find("/registered/") != std::string::npos ||
physical_name.find("\\registered\\") != std::string::npos) {
return true;
}
if (!HasSupportedFileExtension(physical_name) && !IsExtractedNCAMain(physical_name)) {
return true;
}
// Cache Check
const auto* cached = GetCachedGameMetadata(physical_name);
if (cached && cached->IsValid() && (target == ScanTarget::PopulateGameList || target == ScanTarget::Both)) {
if ((cached->program_id & 0xFFF) == 0) {
const FileSys::PatchManager patch{cached->program_id, system.GetFileSystemController(), system.GetContentProvider()};
auto file = vfs->OpenFile(physical_name, FileSys::OpenMode::Read);
if (file) {
auto loader = Loader::GetLoader(system, file);
if (loader) {
auto entry = MakeGameListEntry(physical_name, cached->title, cached->file_size, cached->icon, *loader,
cached->program_id, compatibility_list, play_time_manager, patch, online_stats);
RecordEvent([=](GameList* game_list) { game_list->AddEntry(entry, parent_dir); });
}
}
}
processed_files++;
emit ProgressUpdated(std::min(100, (processed_files * 100) / total_files));
return true;
}
// Full Scan
const auto file = vfs->OpenFile(physical_name, FileSys::OpenMode::Read);
if (!file) {
processed_files++;
return true;
}
auto loader = Loader::GetLoader(system, file);
if (!loader) {
processed_files++;
return true;
}
u64 program_id = 0;
const auto res2 = loader->ReadProgramId(program_id);
const auto file_type = loader->GetFileType();
if (res2 == Loader::ResultStatus::Success && program_id != 0) {
if (target == ScanTarget::FillManualContentProvider || target == ScanTarget::Both) {
if (file_type == Loader::FileType::NCA) {
provider->AddEntry(FileSys::TitleType::Application, FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()), program_id, file);
} else if (file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP) {
const auto nsp = file_type == Loader::FileType::NSP ? std::make_shared<FileSys::NSP>(file) : FileSys::XCI{file}.GetSecurePartitionNSP();
for (const auto& title : nsp->GetNCAs()) {
for (const auto& entry : title.second) {
provider->AddEntry(entry.first.first, entry.first.second, title.first, entry.second->GetBaseFile());
}
}
}
}
if (target == ScanTarget::PopulateGameList || target == ScanTarget::Both) {
// 3. FILTER UPDATES: Only add to UI if it's a Base Game (ID ends in 000)
if ((program_id & 0xFFF) == 0) {
std::vector<u8> icon;
std::string name = " ";
loader->ReadIcon(icon);
loader->ReadTitle(name);
std::size_t file_size = Common::FS::GetSize(physical_name);
CacheGameMetadata(physical_name, program_id, file_type, file_size, name, icon);
const FileSys::PatchManager patch{program_id, system.GetFileSystemController(), system.GetContentProvider()};
auto entry = MakeGameListEntry(physical_name, name, file_size, icon, *loader, program_id, compatibility_list, play_time_manager, patch, online_stats);
RecordEvent([=](GameList* game_list) { game_list->AddEntry(entry, parent_dir); });
}
}
}
processed_files++;
emit ProgressUpdated(std::min(100, (processed_files * 100) / total_files));
return true;
};
if (deep_scan) {
Common::FS::IterateDirEntriesRecursively(dir_path, callback,
Common::FS::DirEntryFilter::All);
Common::FS::IterateDirEntriesRecursively(dir_path, callback, Common::FS::DirEntryFilter::File);
} else {
Common::FS::IterateDirEntries(dir_path, callback, Common::FS::DirEntryFilter::File);
}
}
void GameListWorker::run() {
// Load cached game metadata at the start
LoadGameMetadataCache();
std::map<u64, std::pair<int, int>> online_stats; // Game ID -> {player_count, server_count}
std::map<u64, std::pair<int, int>> online_stats;
if (session) {
AnnounceMultiplayerRoom::RoomList room_list = session->GetRoomList();
for (const auto& room : room_list) {
u64 game_id = room.information.preferred_game.id;
if (game_id != 0) {
online_stats[game_id].first += room.members.size();
online_stats[game_id].first += (int)room.members.size();
online_stats[game_id].second++;
}
}
@@ -698,42 +681,46 @@ void GameListWorker::run() {
watch_list.clear();
provider->ClearAllEntries();
int total_files = 0;
int processed_files = 0;
for (const auto& game_dir : game_dirs) {
if (game_dir.path == "SDMC" || game_dir.path == "UserNAND" || game_dir.path == "SysNAND") continue;
auto count_callback = [&](const std::filesystem::path& path) -> bool {
const std::string physical_name = Common::FS::PathToUTF8String(path);
if (HasSupportedFileExtension(physical_name)) {
total_files++;
}
return true;
};
if (game_dir.deep_scan) {
Common::FS::IterateDirEntriesRecursively(game_dir.path, count_callback, Common::FS::DirEntryFilter::File);
} else {
Common::FS::IterateDirEntries(game_dir.path, count_callback, Common::FS::DirEntryFilter::File);
}
}
if (total_files <= 0) total_files = 1;
const auto DirEntryReady = [&](GameListDir* game_list_dir) {
RecordEvent([=](GameList* game_list) { game_list->AddDirEntry(game_list_dir); });
};
for (UISettings::GameDir& game_dir : game_dirs) {
if (stop_requested) {
break;
}
if (stop_requested) break;
if (game_dir.path == std::string("SDMC")) {
auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SdmcDir);
DirEntryReady(game_list_dir);
AddTitlesToGameList(game_list_dir, online_stats);
} else if (game_dir.path == std::string("UserNAND")) {
auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::UserNandDir);
DirEntryReady(game_list_dir);
AddTitlesToGameList(game_list_dir, online_stats);
} else if (game_dir.path == std::string("SysNAND")) {
auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SysNandDir);
DirEntryReady(game_list_dir);
AddTitlesToGameList(game_list_dir, online_stats);
} else {
watch_list.append(QString::fromStdString(game_dir.path));
auto* const game_list_dir = new GameListDir(game_dir);
DirEntryReady(game_list_dir);
ScanFileSystem(ScanTarget::FillManualContentProvider, game_dir.path, game_dir.deep_scan,
game_list_dir, online_stats);
ScanFileSystem(ScanTarget::PopulateGameList, game_dir.path, game_dir.deep_scan,
game_list_dir, online_stats);
}
if (game_dir.path == "SDMC" || game_dir.path == "UserNAND" || game_dir.path == "SysNAND") continue;
watch_list.append(QString::fromStdString(game_dir.path));
auto* const game_list_dir = new GameListDir(game_dir);
DirEntryReady(game_list_dir);
ScanFileSystem(ScanTarget::Both, game_dir.path, game_dir.deep_scan, game_list_dir, online_stats, processed_files, total_files);
}
RecordEvent([this](GameList* game_list) { game_list->DonePopulating(watch_list); });
// Save cached game metadata at the end
SaveGameMetadataCache();
processing_completed.Set();
}

View File

@@ -47,6 +47,7 @@ public:
enum class ScanTarget {
FillManualContentProvider,
PopulateGameList,
Both,
};
explicit GameListWorker(std::shared_ptr<FileSys::VfsFilesystem> vfs_,
@@ -73,6 +74,7 @@ public:
signals:
void DataAvailable();
void ProgressUpdated(int percent);
private:
template <typename F>
@@ -84,7 +86,8 @@ private:
void ScanFileSystem(ScanTarget target, const std::string& dir_path, bool deep_scan,
GameListDir* parent_dir,
const std::map<u64, std::pair<int, int>>& online_stats);
const std::map<u64, std::pair<int, int>>& online_stats,
int& processed_files, int total_files);
std::shared_ptr<FileSys::VfsFilesystem> vfs;
FileSys::ManualContentProvider* provider;

View File

@@ -2272,11 +2272,9 @@ void GMainWindow::OnEmulationStopped() {
render_window->hide();
loading_screen->hide();
loading_screen->Clear();
if (game_list->IsEmpty()) {
game_list_placeholder->show();
} else {
game_list->show();
}
game_list->show();
game_list_placeholder->hide();
game_list->SetFilterFocus();
tas_label->clear();
input_subsystem->GetTas()->Stop();

View File

@@ -219,6 +219,7 @@ namespace UISettings {
Setting<bool> game_list_grid_view{linkage, false, "game_list_grid_view", Category::UiGameList};
std::atomic_bool is_game_list_reload_pending{false};
Setting<bool> cache_game_list{linkage, true, "cache_game_list", Category::UiGameList};
Setting<bool> scan_nca{linkage, false, "scan_nca", Category::UiGameList};
Setting<bool> prompt_for_autoloader{linkage, true, "prompt_for_autoloader", Category::UiGameList};
Setting<bool> favorites_expanded{linkage, true, "favorites_expanded", Category::UiGameList};
QVector<u64> favorited_ids;

View File

@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-FileCopyrightText: 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/fs/file.h"
@@ -324,7 +325,6 @@ bool RemoveDirContentsRecursively(const fs::path& path) {
std::error_code ec;
// TODO (Morph): Replace this with recursive_directory_iterator once it's fixed in MSVC.
for (const auto& entry : fs::directory_iterator(path, ec)) {
if (ec) {
LOG_ERROR(Common_Filesystem,
@@ -342,10 +342,11 @@ bool RemoveDirContentsRecursively(const fs::path& path) {
break;
}
// TODO (Morph): Remove this when MSVC fixes recursive_directory_iterator.
// recursive_directory_iterator throws an exception despite passing in a std::error_code.
if (entry.status().type() == fs::file_type::directory) {
return RemoveDirContentsRecursively(entry.path());
std::error_code status_ec;
if (entry.status(status_ec).type() == fs::file_type::directory) {
if (!RemoveDirContentsRecursively(entry.path())) {
return false;
}
}
}
@@ -434,8 +435,12 @@ void IterateDirEntries(const std::filesystem::path& path, const DirEntryCallable
break;
}
std::error_code status_ec;
const auto st = entry.status(status_ec);
if (status_ec) continue;
if (True(filter & DirEntryFilter::File) &&
entry.status().type() == fs::file_type::regular) {
st.type() == fs::file_type::regular) {
if (!callback(entry)) {
callback_error = true;
break;
@@ -443,7 +448,7 @@ void IterateDirEntries(const std::filesystem::path& path, const DirEntryCallable
}
if (True(filter & DirEntryFilter::Directory) &&
entry.status().type() == fs::file_type::directory) {
st.type() == fs::file_type::directory) {
if (!callback(entry)) {
callback_error = true;
break;
@@ -462,6 +467,42 @@ void IterateDirEntries(const std::filesystem::path& path, const DirEntryCallable
PathToUTF8String(path));
}
void IterateDirEntriesRecursivelyInternal(const std::filesystem::path& path,
const DirEntryCallable& callback, DirEntryFilter filter,
int depth) {
if (depth > 12) return;
std::error_code ec;
auto it = fs::directory_iterator(path, ec);
if (ec) return;
while (it != fs::directory_iterator() && !ec) {
const auto& entry = *it;
#ifndef _WIN32
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") {
it.increment(ec);
continue;
}
#endif
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);
} else {
if (True(filter & DirEntryFilter::File)) { if (!callback(entry)) break; }
}
it.increment(ec);
if (ec) { ec.clear(); break; }
}
}
void IterateDirEntriesRecursively(const std::filesystem::path& path,
const DirEntryCallable& callback, DirEntryFilter filter) {
if (!ValidatePath(path)) {
@@ -469,59 +510,10 @@ void IterateDirEntriesRecursively(const std::filesystem::path& path,
return;
}
if (!Exists(path)) {
LOG_ERROR(Common_Filesystem, "Filesystem object at path={} does not exist",
PathToUTF8String(path));
return;
}
// Start the recursion at depth 0
IterateDirEntriesRecursivelyInternal(path, callback, filter, 0);
if (!IsDir(path)) {
LOG_ERROR(Common_Filesystem, "Filesystem object at path={} is not a directory",
PathToUTF8String(path));
return;
}
bool callback_error = false;
std::error_code ec;
// TODO (Morph): Replace this with recursive_directory_iterator once it's fixed in MSVC.
for (const auto& entry : fs::directory_iterator(path, ec)) {
if (ec) {
break;
}
if (True(filter & DirEntryFilter::File) &&
entry.status().type() == fs::file_type::regular) {
if (!callback(entry)) {
callback_error = true;
break;
}
}
if (True(filter & DirEntryFilter::Directory) &&
entry.status().type() == fs::file_type::directory) {
if (!callback(entry)) {
callback_error = true;
break;
}
}
// TODO (Morph): Remove this when MSVC fixes recursive_directory_iterator.
// recursive_directory_iterator throws an exception despite passing in a std::error_code.
if (entry.status().type() == fs::file_type::directory) {
IterateDirEntriesRecursively(entry.path(), callback, filter);
}
}
if (callback_error || ec) {
LOG_ERROR(Common_Filesystem,
"Failed to visit all the directory entries of path={}, ec_message={}",
PathToUTF8String(path), ec.message());
return;
}
LOG_DEBUG(Common_Filesystem, "Successfully visited all the directory entries of path={}",
LOG_DEBUG(Common_Filesystem, "Finished visiting directory entries of path={}",
PathToUTF8String(path));
}

View File

@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-FileCopyrightText: 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@@ -399,6 +400,10 @@ 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);
#ifdef _WIN32
template <typename Path>
void IterateDirEntriesRecursively(const Path& path, const DirEntryCallable& callback,