[desktop] Clean up game list code, fix external watcher crash, and fix macOS flickering (#4106)

- Remove unnecessary icon update code (the UI reloads this stuff anyways); test on Windows please
- Cleaned up a bunch of duplicated/unused code within the game list
- Fix the game list constantly reloading on macOS
  * When you reconstruct the entire directory list on the watcher the directoryChanged signal fires on macOS--seems like a behavioral change that occurred somewhere in the 6.8 release cycle--and it would enter an infinite loop very quickly
  * To fix this, only the differences between the current and old watch list are accounted for on both ends.
  * Since this bug is now fixed, macOS uses Qt 6.11.1 now. Should theoretically improve our situation.
- Fix the external content watcher crashing; the worker would attempt to read files that didn't exist without any bounds since its cache was still pointing to that file.

This supersedes and replaces #4099.

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4106
Reviewed-by: Lizzie <lizzie@eden-emu.dev>
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
This commit is contained in:
crueter 2026-06-19 22:55:29 +02:00
parent 7c0e993b5b
commit 102d254530
No known key found for this signature in database
GPG key ID: 425ACD2D4830EBC6
12 changed files with 181 additions and 277 deletions

View file

@ -239,7 +239,8 @@ add_executable(yuzu
configuration/addon/mod_select_dialog.h configuration/addon/mod_select_dialog.cpp configuration/addon/mod_select_dialog.ui
render/performance_overlay.h render/performance_overlay.cpp render/performance_overlay.ui
updater/update_dialog.h updater/update_dialog.cpp updater/update_dialog.ui)
updater/update_dialog.h updater/update_dialog.cpp updater/update_dialog.ui
game/common.h)
set_target_properties(yuzu PROPERTIES OUTPUT_NAME "eden")

36
src/yuzu/game/common.h Normal file
View file

@ -0,0 +1,36 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QStandardItem>
#include <QStringList>
#include "qt_common/game_list/game_list_p.h"
namespace Yuzu {
inline bool ContainsAllWords(const QString& haystack, const QString& userinput) {
const QStringList userinput_split = userinput.split(QLatin1Char{' '}, Qt::SkipEmptyParts);
return std::all_of(userinput_split.begin(), userinput_split.end(),
[&haystack](const QString& s) { return haystack.contains(s); });
}
inline bool FilterMatches(const QString& filter, const QStandardItem* item) {
if (filter.isEmpty())
return true;
const auto program_id = item->data(GameListItemPath::ProgramIdRole).toULongLong();
const QString file_path = item->data(GameListItemPath::FullPathRole).toString().toLower();
const QString file_title = item->data(GameListItemPath::TitleRole).toString().toLower();
const QString file_program_id = QStringLiteral("%1").arg(program_id, 16, 16, QLatin1Char{'0'});
const QString file_name =
file_path.mid(file_path.lastIndexOf(QLatin1Char{'/'}) + 1) + QLatin1Char{' '} + file_title;
return Yuzu::ContainsAllWords(file_name, filter) ||
(file_program_id.size() == 16 && file_program_id.contains(filter));
}
} // namespace Yuzu

View file

@ -5,10 +5,10 @@
#include <QScrollerProperties>
#include "qt_common/config/uisettings.h"
#include "qt_common/game_list/model.h"
#include "yuzu/game/common.h"
#include "yuzu/game/game_card.h"
#include "yuzu/game/game_grid.h"
#include "qt_common/game_list/game_list_p.h"
#include "qt_common/game_list/model.h"
GameGrid::GameGrid(QWidget* parent) : QListView{parent} {
m_gameCard = new GameCard(this);
@ -43,26 +43,12 @@ void GameGrid::SetModel(GameListModel* model) {
void GameGrid::ApplyFilter(const QString& edit_filter_text, GameListModel* model) {
int row_count = model->rowCount();
auto ContainsAllWords = [](const QString& haystack, const QString& userinput) {
const QStringList userinput_split =
userinput.split(QLatin1Char{' '}, Qt::SkipEmptyParts);
return std::all_of(userinput_split.begin(), userinput_split.end(),
[&haystack](const QString& s) { return haystack.contains(s); });
};
for (int i = 0; i < row_count; ++i) {
QStandardItem* item = model->item(i, 0);
if (!item)
continue;
const QString file_path =
item->data(GameListItemPath::FullPathRole).toString().toLower();
const QString file_title =
item->data(GameListItemPath::TitleRole).toString().toLower();
const QString file_name = file_path.mid(file_path.lastIndexOf(QLatin1Char{'/'}) + 1) +
QLatin1Char{' '} + file_title;
if (edit_filter_text.isEmpty() || ContainsAllWords(file_name, edit_filter_text)) {
if (Yuzu::FilterMatches(edit_filter_text, item)) {
setRowHidden(i, false);
} else {
setRowHidden(i, true);

View file

@ -22,22 +22,21 @@
#include <QVariantAnimation>
#include "common/common_types.h"
#include "common/logging.h"
#include "core/core.h"
#include "core/file_sys/patch_manager.h"
#include "core/file_sys/registered_cache.h"
#include "qt_common/config/uisettings.h"
#include "qt_common/game_list/game_list_p.h"
#include "qt_common/game_list/model.h"
#include "qt_common/qt_common.h"
#include "qt_common/util/game.h"
#include "yuzu/compatibility_list.h"
#include "yuzu/game/game_list.h"
#include "qt_common/game_list/game_list_p.h"
#include "yuzu/game/game_grid.h"
#include "yuzu/game/game_list.h"
#include "yuzu/game/game_tree.h"
#include "qt_common/game_list/model.h"
#include "yuzu/game/search_field.h"
#include "yuzu/main_window.h"
#include "yuzu/util/controller_navigation.h"
#include "yuzu/game/search_field.h"
GameList::GameList(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvider* provider_,
PlayTime::PlayTimeManager& play_time_manager_, Core::System& system_,
@ -62,8 +61,6 @@ GameList::GameList(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvid
SetupScrollAnimation();
connect(main_window, &MainWindow::UpdateThemedIcons, this, &GameList::OnUpdateThemedIcons);
connect(tree_view, &QTreeView::activated, this, &GameList::ValidateEntry);
connect(tree_view, &QTreeView::customContextMenuRequested, this, &GameList::PopupContextMenu);
@ -87,6 +84,7 @@ GameList::GameList(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvid
connect(item_model, &GameListModel::ShowList, this, &GameList::ShowList);
connect(item_model, &GameListModel::SaveConfig, this, &GameList::SaveConfig);
connect(item_model, &GameListModel::PopulatingStarted, this, &GameList::OnPopulate);
connect(tree_view, &GameTree::FilterResultReady, search_field,
[this](int visible, int total) { search_field->setFilterResult(visible, total); });
@ -137,15 +135,17 @@ void GameList::LoadCompatibilityList() {
item_model->LoadCompatibilityList();
}
void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) {
void GameList::OnPopulate() {
m_currentView->setEnabled(false);
tree_view->UpdateColumnVisibility(item_model);
if (!m_isTreeMode) {
if (m_isTreeMode) {
grid_view->UpdateIconSize();
} else {
tree_view->UpdateColumnVisibility(item_model);
}
}
void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) {
item_model->PopulateAsync(game_dirs);
}
@ -196,8 +196,10 @@ void GameList::ResetViewMode() {
auto scroller = QScroller::scroller(view);
QScrollerProperties props;
props.setScrollMetric(QScrollerProperties::HorizontalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff);
props.setScrollMetric(QScrollerProperties::VerticalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff);
props.setScrollMetric(QScrollerProperties::HorizontalOvershootPolicy,
QScrollerProperties::OvershootAlwaysOff);
props.setScrollMetric(QScrollerProperties::VerticalOvershootPolicy,
QScrollerProperties::OvershootAlwaysOff);
scroller->setScrollerProperties(props);
if (m_isTreeMode != newTreeMode) {
@ -223,10 +225,6 @@ void GameList::OnFilterCloseClicked() {
main_window->filterBarSetChecked(false);
}
void GameList::OnUpdateThemedIcons() {
item_model->OnUpdateThemedIcons();
}
void GameList::OnPopulatingCompleted(const QStringList& watch_list) {
emit ShowList(!item_model->IsEmpty());
@ -255,50 +253,44 @@ void GameList::OnPopulatingCompleted(const QStringList& watch_list) {
}
}
// Clear out the old directories to watch for changes and add the new ones
// Watcher updates
auto* watcher = item_model->GetWatcher();
const QStringList current_watch = watcher->directories();
auto current_watch_list = watcher->directories();
constexpr int LIMIT_WATCH_DIRECTORIES = 5000;
constexpr qsizetype LIMIT_WATCH_DIRECTORIES = 5000;
constexpr int SLICE_SIZE = 25;
QStringList desired_watch = watch_list;
if (desired_watch.size() > LIMIT_WATCH_DIRECTORIES) {
desired_watch = desired_watch.mid(0, LIMIT_WATCH_DIRECTORIES);
}
QStringList to_remove, to_add;
// Only re-arm the watcher when the set of directories actually changed. Re-adding the same
// paths makes the macOS QFileSystemWatcher re-emit directoryChanged (the FSEvent arrives
// asynchronously, so the blockSignals guard below does not catch it), which retriggers a full
// refresh and loops forever, making the game list visibly flash. Comparing the sets breaks
// that loop: at steady state we leave the watcher untouched and no spurious event is produced.
QStringList sorted_current = current_watch;
QStringList sorted_desired = desired_watch;
sorted_current.sort();
sorted_desired.sort();
if (sorted_current != sorted_desired) {
if (!current_watch.isEmpty()) {
watcher->removePaths(current_watch);
}
#ifdef __APPLE__
const bool old_signals_blocked = watcher->blockSignals(true);
#endif
for (int i = 0; i < desired_watch.size(); i += SLICE_SIZE) {
auto chunk = desired_watch.mid(i, SLICE_SIZE);
const auto slice = [&](const QStringList& list, std::function<void(const QStringList&)> callback) {
const int len = (std::min)(list.size(), LIMIT_WATCH_DIRECTORIES);
for (int i = 0; i < len; i += SLICE_SIZE) {
auto chunk = list.mid(i, SLICE_SIZE);
if (!chunk.isEmpty()) {
watcher->addPaths(chunk);
callback(chunk);
}
QCoreApplication::processEvents();
}
};
#ifdef __APPLE__
watcher->blockSignals(old_signals_blocked);
#endif
// remove any paths not in the new watch list
for (const auto& path : std::as_const(current_watch_list)) {
if (!watch_list.contains(path)) {
to_remove.emplaceBack(path);
}
}
slice(to_remove, [watcher](const QStringList& chunk) { watcher->removePaths(chunk); });
// add any paths not in the old watch list
for (const auto& path : std::as_const(watch_list)) {
if (!current_watch_list.contains(path)) {
to_add.emplaceBack(path);
}
}
slice(to_add, [watcher](const QStringList& chunk) { watcher->addPaths(chunk); });
m_currentView->setEnabled(true);
int children_total = 0;
@ -317,23 +309,16 @@ void GameList::OnPopulatingCompleted(const QStringList& watch_list) {
}
void GameList::RefreshGameDirectory() {
item_model->ResetExternalWatcher();
if (!UISettings::values.game_dirs.empty()) {
LOG_INFO(Frontend, "Change detected in the games directory. Reloading game list.");
item_model->StopWorker();
QtCommon::system->GetFileSystemController().CreateFactories(*QtCommon::vfs);
PopulateAsync(UISettings::values.game_dirs);
}
item_model->RefreshGameDirectory();
}
void GameList::RefreshExternalContent() {
if (!UISettings::values.game_dirs.empty()) {
LOG_INFO(Frontend, "External content directory changed. Clearing metadata cache.");
item_model->StopWorker();
QtCommon::Game::ResetMetadata(false);
QtCommon::system->GetFileSystemController().CreateFactories(*QtCommon::vfs);
PopulateAsync(UISettings::values.game_dirs);
item_model->RefreshExternalContent();
}
void GameList::UpdateIconSizes() {
if (!m_isTreeMode) {
grid_view->UpdateIconSize();
}
}
@ -491,9 +476,8 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
});
connect(start_game, &QAction::triggered, this,
[this, path]() { emit BootGame(QString::fromStdString(path), StartGameType::Normal); });
connect(start_game_global, &QAction::triggered, this, [this, path]() {
emit BootGame(QString::fromStdString(path), StartGameType::Global);
});
connect(start_game_global, &QAction::triggered, this,
[this, path]() { emit BootGame(QString::fromStdString(path), StartGameType::Global); });
connect(open_mod_location, &QAction::triggered, this, [this, program_id, path]() {
emit OpenFolderRequested(program_id, GameListOpenTarget::ModData, path);
});
@ -632,7 +616,8 @@ void GameList::AddFavoritesPopup(QMenu& context_menu) {
connect(clear, &QAction::triggered, this, [this] {
UISettings::values.favorited_ids.clear();
item_model->invisibleRootItem()->child(0)->removeRows(0, item_model->invisibleRootItem()->child(0)->rowCount());
item_model->invisibleRootItem()->child(0)->removeRows(
0, item_model->invisibleRootItem()->child(0)->rowCount());
tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(), true);
});
}
@ -731,9 +716,6 @@ bool GameList::eventFilter(QObject* obj, QEvent* event) {
}
GameListPlaceholder::GameListPlaceholder(MainWindow* parent) : QWidget{parent} {
connect(parent, &MainWindow::UpdateThemedIcons, this,
&GameListPlaceholder::onUpdateThemedIcons);
layout = new QVBoxLayout;
image = new QLabel;
text = new QLabel;
@ -754,10 +736,6 @@ GameListPlaceholder::GameListPlaceholder(MainWindow* parent) : QWidget{parent} {
GameListPlaceholder::~GameListPlaceholder() = default;
void GameListPlaceholder::onUpdateThemedIcons() {
image->setPixmap(QIcon::fromTheme(QStringLiteral("plus_folder")).pixmap(200));
}
void GameListPlaceholder::mouseDoubleClickEvent(QMouseEvent* event) {
emit GameListPlaceholder::AddDirectory();
}

View file

@ -89,6 +89,8 @@ public:
public slots:
void RefreshGameDirectory();
void RefreshExternalContent();
void UpdateIconSizes();
void OnPopulate();
signals:
void BootGame(const QString& game_path, StartGameType type);
@ -119,7 +121,6 @@ signals:
private slots:
void OnTextChanged(const QString& new_text);
void OnFilterCloseClicked();
void OnUpdateThemedIcons();
void OnPopulatingCompleted(const QStringList& watch_list);
private:
@ -175,9 +176,6 @@ public:
signals:
void AddDirectory();
private slots:
void onUpdateThemedIcons();
protected:
void mouseDoubleClickEvent(QMouseEvent* event) override;

View file

@ -8,8 +8,9 @@
#include "qt_common/config/uisettings.h"
#include "qt_common/game_list/game_list_p.h"
#include "yuzu/game/game_tree.h"
#include "qt_common/game_list/model.h"
#include "yuzu/game/common.h"
#include "yuzu/game/game_tree.h"
GameTree::GameTree(QWidget* parent) : QTreeView{parent} {
setAlternatingRowColors(true);
@ -139,28 +140,7 @@ void GameTree::ApplyFilter(const QString& edit_filter_text, GameListModel* model
const QStandardItem* child = folder->child(j, 0);
const auto program_id = child->data(GameListItemPath::ProgramIdRole).toULongLong();
const QString file_path =
child->data(GameListItemPath::FullPathRole).toString().toLower();
const QString file_title =
child->data(GameListItemPath::TitleRole).toString().toLower();
const QString file_program_id =
QStringLiteral("%1").arg(program_id, 16, 16, QLatin1Char{'0'});
const QString file_name =
file_path.mid(file_path.lastIndexOf(QLatin1Char{'/'}) + 1) + QLatin1Char{' '} +
file_title;
auto ContainsAllWords = [](const QString& haystack, const QString& userinput) {
const QStringList userinput_split =
userinput.split(QLatin1Char{' '}, Qt::SkipEmptyParts);
return std::all_of(userinput_split.begin(), userinput_split.end(),
[&haystack](const QString& s) { return haystack.contains(s); });
};
if (ContainsAllWords(file_name, edit_filter_text) ||
(file_program_id.size() == 16 && file_program_id.contains(edit_filter_text))) {
if (Yuzu::FilterMatches(edit_filter_text, child)) {
setRowHidden(j, folder_index, false);
++result_count;
} else {

View file

@ -579,9 +579,12 @@ MainWindow::MainWindow(bool has_broken_vulkan)
} else if (should_launch_qlaunch) {
LaunchFirmwareApplet(u64(Service::AM::AppletProgramId::QLaunch), std::nullopt);
} else if (should_launch_hlaunch) {
std::filesystem::path const sd_dir = Common::FS::GetEdenPathString(Common::FS::EdenPath::SDMCDir);
std::filesystem::path const sd_dir =
Common::FS::GetEdenPathString(Common::FS::EdenPath::SDMCDir);
auto const hbl_path = (sd_dir / "atmosphere" / "hbl.nsp").string();
BootGame(QString::fromStdString(hbl_path), LibraryAppletParameters(0x010000000000100Dull, Service::AM::AppletId::QLaunch));
BootGame(
QString::fromStdString(hbl_path),
LibraryAppletParameters(0x010000000000100Dull, Service::AM::AppletId::QLaunch));
}
}
}
@ -1545,6 +1548,8 @@ void MainWindow::ConnectMenuEvents() {
if (checked) {
UISettings::values.game_icon_size.SetValue(size);
CheckIconSize();
game_list->UpdateIconSizes();
game_list->RefreshGameDirectory();
}
});
@ -3363,6 +3368,7 @@ void MainWindow::ToggleShowGameName() {
CheckIconSize();
game_list->UpdateIconSizes();
game_list->RefreshGameDirectory();
}
@ -3868,6 +3874,7 @@ void MainWindow::OnGameListRefresh() {
// Resets metadata cache and reloads
QtCommon::Game::ResetMetadata(false);
game_list->RefreshGameDirectory();
game_list->RefreshExternalContent();
SetFirmwareVersion();
}