Browse Source

Read actual games from game dirs

TODO: GameList/GameListModel can be abstracted in some way

GameListWorker should work on this abstraction. I also need to add
grouped directories just like Widgets

Signed-off-by: crueter <crueter@eden-emu.dev>
pull/3016/head
crueter 2 months ago
parent
commit
62d72b970a
No known key found for this signature in database GPG Key ID: 425ACD2D4830EBC6
  1. 17
      src/Eden/Interface/CMakeLists.txt
  2. 5
      src/Eden/Main/GameCarouselCard.qml
  3. 6
      src/Eden/Main/GameGridCard.qml
  4. 32
      src/Eden/Main/GameList.qml
  5. 5
      src/Eden/Models/CMakeLists.txt
  6. 45
      src/Eden/Models/GameIconProvider.cpp
  7. 16
      src/Eden/Models/GameIconProvider.h
  8. 143
      src/Eden/Models/GameListModel.cpp
  9. 26
      src/Eden/Models/GameListModel.h
  10. 512
      src/Eden/Models/GameListWorker.cpp
  11. 81
      src/Eden/Models/GameListWorker.h
  12. 15
      src/Eden/Native/main.cpp
  13. 2
      src/video_core/CMakeLists.txt

17
src/Eden/Interface/CMakeLists.txt

@ -4,21 +4,16 @@
# SPDX-FileCopyrightText: Copyright 2025 crueter
# SPDX-License-Identifier: GPL-3.0-or-later
find_package(Qt6 REQUIRED COMPONENTS Core)
qt_add_library(EdenInterface STATIC)
qt_add_qml_module(EdenInterface
EdenModule(
NAME Interface
URI Eden.Interface
NO_PLUGIN
SOURCES
SettingsInterface.h SettingsInterface.cpp
QMLSetting.h QMLSetting.cpp
MetaObjectHelper.h
QMLConfig.h
SOURCES TitleManager.h TitleManager.cpp
TitleManager.h TitleManager.cpp
LIBRARIES
Qt6::Quick
Qt6::Core
)
target_link_libraries(EdenInterface PUBLIC Qt6::Quick)
target_link_libraries(EdenInterface PRIVATE Qt6::Core)
add_library(Eden::Interface ALIAS EdenInterface)

5
src/Eden/Main/GameCarouselCard.qml

@ -25,7 +25,10 @@ Item {
id: image
fillMode: Image.PreserveAspectFit
source: "file://" + model.path
source: "image://games/" + model.name
sourceSize.width: width
sourceSize.height: height
clip: true

6
src/Eden/Main/GameGridCard.qml

@ -14,8 +14,7 @@ Rectangle {
Image {
id: image
fillMode: Image.PreserveAspectFit
source: "file://" + model.path
source: "image://games/" + model.name
clip: true
@ -28,6 +27,9 @@ Rectangle {
margins: 10
}
sourceSize.width: width
sourceSize.height: height
height: parent.height
MouseArea {

32
src/Eden/Main/GameList.qml

@ -61,16 +61,6 @@ Rectangle {
// repeat: true
// onTriggered: gamepad.pollEvents()
// }
FolderDialog {
id: openDir
folder: StandardPaths.writableLocation(StandardPaths.HomeLocation)
onAccepted: {
button.visible = false
view.anchors.bottom = root.bottom
EdenGameList.addDir(folder)
}
}
Item {
id: view
@ -102,26 +92,4 @@ Rectangle {
// }
// }
}
Button {
id: button
font.pixelSize: 25
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
margins: 8
}
text: "Add Directory"
onClicked: openDir.open()
background: Rectangle {
color: button.pressed ? Constants.accentPressed : Constants.accent
radius: 5
}
}
}

5
src/Eden/Models/CMakeLists.txt

@ -4,14 +4,19 @@
# SPDX-FileCopyrightText: Copyright 2025 crueter
# SPDX-License-Identifier: GPL-3.0-or-later
find_package(Qt6 REQUIRED COMPONENTS Core)
qt_add_library(EdenModels STATIC
GameListModel.h GameListModel.cpp
SettingsModel.h SettingsModel.cpp
GameListWorker.h GameListWorker.cpp
GameIconProvider.h GameIconProvider.cpp
)
target_link_libraries(EdenModels
PRIVATE
Qt6::Gui
)
target_link_libraries(EdenModels PRIVATE Qt6::Core Qt6::Quick)
add_library(Eden::Models ALIAS EdenModels)

45
src/Eden/Models/GameIconProvider.cpp

@ -0,0 +1,45 @@
#include <qnamespace.h>
#include "GameIconProvider.h"
#include "qt_common/uisettings.h"
/**
* Gets the default icon (for games without valid title metadata)
* @param size The desired width and height of the default icon.
* @return QPixmap default icon
*/
static QPixmap GetDefaultIcon(const QSize &size)
{
QPixmap icon(size.width(), size.height());
icon.fill(Qt::transparent);
return icon;
}
GameIconProvider::GameIconProvider()
: QQuickImageProvider(QQuickImageProvider::Pixmap)
{}
QPixmap GameIconProvider::requestPixmap(const QString &id, QSize *size, const QSize &requestedSize)
{
const u32 default_size = UISettings::values.game_icon_size.GetValue();
QSize trueSize = QSize(default_size, default_size);
if (requestedSize.isValid()) {
trueSize = requestedSize;
}
QPixmap pixmap = m_pixmaps.value(id, GetDefaultIcon(trueSize));
if (size)
*size = QSize(trueSize.width(), trueSize.height());
return pixmap;
}
void GameIconProvider::addPixmap(const QString &key, const QPixmap pixmap)
{
m_pixmaps.insert(key, pixmap);
}
void GameIconProvider::clear()
{
m_pixmaps.clear();
}

16
src/Eden/Models/GameIconProvider.h

@ -0,0 +1,16 @@
#pragma once
#include <QQuickImageProvider>
class GameIconProvider : public QQuickImageProvider
{
public:
GameIconProvider();
QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) override;
void addPixmap(const QString &key, const QPixmap pixmap);
void clear();
private:
QMap<QString, QPixmap> m_pixmaps;
};

143
src/Eden/Models/GameListModel.cpp

@ -1,16 +1,40 @@
#include "GameListModel.h"
#include <QDirIterator>
#include <QGuiApplication>
#include <QThreadPool>
const QStringList GameListModel::ValidSuffixes{"jpg", "png", "webp", "jpeg"};
#include "GameIconProvider.h"
#include "GameListWorker.h"
#include "common/logging/filter.h"
#include "core/hle/service/filesystem/filesystem.h"
#include "hid_core/hid_core.h"
#include "qt_common/qt_common.h"
#include "qt_common/qt_meta.h"
GameListModel::GameListModel(QObject *parent) {
GameListModel::GameListModel(QObject *parent, QQmlEngine *engine) {
QHash<int, QByteArray> rez = QStandardItemModel::roleNames();
rez.insert(GLMRoleTypes::NAME, "name");
rez.insert(GLMRoleTypes::PATH, "path");
rez.insert(GLMRoleTypes::FILESIZE, "size");
rez.insert(GLMRoleTypes::ICON, "icon");
QStandardItemModel::setItemRoleNames(rez);
QtCommon::Meta::RegisterMetaTypes();
QtCommon::system->HIDCore().ReloadInputDevices();
QtCommon::system->SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>());
QtCommon::system->RegisterContentProvider(FileSys::ContentProviderUnionSlot::FrontendManual,
QtCommon::provider.get());
QtCommon::system->GetFileSystemController().CreateFactories(*QtCommon::vfs);
m_provider = new GameIconProvider;
engine->addImageProvider(QStringLiteral("games"), m_provider);
watcher = new QFileSystemWatcher(this);
connect(watcher, &QFileSystemWatcher::directoryChanged, this, &GameListModel::RefreshGameDirectory);
populateAsync(UISettings::values.game_dirs);
}
QVariant GameListModel::data(const QModelIndex &index, int role) const
@ -25,43 +49,92 @@ QVariant GameListModel::data(const QModelIndex &index, int role) const
return QStandardItemModel::data(index, role);
}
void GameListModel::addDir(const QString &toAdd)
// void GameListModel::addDir(const QString &toAdd)
// {
// QString name = toAdd;
// #ifdef Q_OS_WINDOWS
// name.replace("file:///", "");
// #else
// name.replace("file://", "");
// #endif
// UISettings::GameDir game_dir{name.toStdString(), false, true};
// if (!UISettings::values.game_dirs.contains(game_dir)) {
// UISettings::values.game_dirs.append(game_dir);
// populateAsync(UISettings::values.game_dirs);
// } else {
// LOG_WARNING(Frontend, "Selected directory is already in the game list");
// }
// QtCommon::system->ApplySettings();
// // TODO
// // config->SaveAllValues();
// }
void GameListModel::RefreshGameDirectory()
{
QString name = toAdd;
#ifdef Q_OS_WINDOWS
name.replace("file:///", "");
#else
name.replace("file://", "");
#endif
m_dirs << name;
reload();
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 GameListModel::removeDir(const QString &toRemove)
{
m_dirs.removeAll(toRemove);
reload();
void GameListModel::addEntry(QStandardItem *entry, const UISettings::GameDir &parent_dir) {
// TODO: Directory grouping
QString text = entry->data(GLMRoleTypes::NAME).toString();
QPixmap pixmap = entry->data(GLMRoleTypes::ICON).value<QPixmap>();
qDebug() << "Adding pixmap" << text;
m_provider->addPixmap(text, pixmap);
invisibleRootItem()->appendRow(entry);
}
void GameListModel::reload()
{
clear();
for (const QString &dir : std::as_const(m_dirs)) {
qDebug() << dir;
for (const auto &entry : QDirListing(dir, QDirListing::IteratorFlag::FilesOnly)) {
if (ValidSuffixes.contains(entry.completeSuffix().toLower())) {
QString path = entry.absoluteFilePath();
QString name = entry.baseName();
qreal size = entry.size();
QString sizeString = QLocale::system().formattedDataSize(size);
QStandardItem *game = new QStandardItem(name);
game->setData(path, GLMRoleTypes::PATH);
game->setData(sizeString, GLMRoleTypes::FILESIZE);
invisibleRootItem()->appendRow(game);
}
}
// TODO
void GameListModel::addDirEntry(const UISettings::GameDir &dir) {}
// TODO
void GameListModel::donePopulating(QStringList watch_list) {
// emit ShowList(!empt());
// Clear out the old directories to watch for changes and add the new ones
auto watch_dirs = watcher->directories();
if (!watch_dirs.isEmpty()) {
watcher->removePaths(watch_dirs);
}
// Workaround: Add the watch paths in chunks to allow the gui to refresh
// This prevents the UI from stalling when a large number of watch paths are added
// Also artificially caps the watcher to a certain number of directories
constexpr int LIMIT_WATCH_DIRECTORIES = 5000;
constexpr int SLICE_SIZE = 25;
int len = (std::min)(static_cast<int>(watch_list.size()), LIMIT_WATCH_DIRECTORIES);
for (int i = 0; i < len; i += SLICE_SIZE) {
watcher->addPaths(watch_list.mid(i, i + SLICE_SIZE));
QGuiApplication::processEvents();
}
}
// TODO: Disable view
void GameListModel::populateAsync(QVector<UISettings::GameDir> &game_dirs) {
// Cancel any existing worker.
current_worker.reset();
/// clear image provider
m_provider->clear();
// Delete any rows that might already exist if we're repopulating
removeRows(0, rowCount());
current_worker = std::make_unique<GameListWorker>(game_dirs);
// Get events from the worker as data becomes available
connect(current_worker.get(), &GameListWorker::DataAvailable, this, &GameListModel::WorkerEvent,
Qt::QueuedConnection);
QThreadPool::globalInstance()->start(current_worker.get());
}
// Worker-related slots
void GameListModel::WorkerEvent() {
current_worker->ProcessEvents(this);
}

26
src/Eden/Models/GameListModel.h

@ -1,8 +1,11 @@
#ifndef GAMELISTMODEL_H
#define GAMELISTMODEL_H
#include <QFileSystemWatcher>
#include <QObject>
#include <QQmlEngine>
#include <QStandardItemModel>
#include "qt_common/uisettings.h"
typedef struct Game {
QString absPath;
@ -10,6 +13,9 @@ typedef struct Game {
QString fileSize;
} Game;
class GameListWorker;
class GameIconProvider;
class GameListModel : public QStandardItemModel
{
Q_OBJECT
@ -17,23 +23,31 @@ public:
enum GLMRoleTypes {
NAME = Qt::UserRole + 1,
PATH,
FILESIZE
FILESIZE,
ICON
};
GameListModel(QObject *parent = nullptr);
GameListModel(QObject *parent, QQmlEngine *engine);
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
Q_INVOKABLE void addDir(const QString &toAdd);
Q_INVOKABLE void removeDir(const QString &toRemove);
void addEntry(QStandardItem *entry, const UISettings::GameDir &parent_dir);
void addDirEntry(const UISettings::GameDir &dir);
void donePopulating(QStringList watch_list);
void populateAsync(QVector<UISettings::GameDir> &game_dirs);
void RefreshGameDirectory();
static const QStringList ValidSuffixes;
private slots:
void WorkerEvent();
private:
QStringList m_dirs;
QList<Game> m_data;
QFileSystemWatcher *watcher = nullptr;
std::unique_ptr<GameListWorker> current_worker;
void reload();
GameIconProvider *m_provider;
};
#endif // GAMELISTMODEL_H

512
src/Eden/Models/GameListWorker.cpp

@ -0,0 +1,512 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "GameListModel.h"
#include "common/logging/types.h"
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QSettings>
#include <QStandardItem>
#include "GameListWorker.h"
#include "common/fs/fs.h"
#include "common/fs/path_util.h"
#include "core/core.h"
#include "core/file_sys/card_image.h"
#include "core/file_sys/content_archive.h"
#include "core/file_sys/control_metadata.h"
#include "core/file_sys/fs_filesystem.h"
#include "core/file_sys/nca_metadata.h"
#include "core/file_sys/patch_manager.h"
#include "core/file_sys/registered_cache.h"
#include "core/file_sys/submission_package.h"
#include "core/loader/loader.h"
#include "qt_common/qt_common.h"
#include "qt_common/uisettings.h"
namespace {
/**
* Gets the default icon (for games without valid title metadata)
* @param size The desired width and height of the default icon.
* @return QPixmap default icon
*/
static QPixmap GetDefaultIcon(u32 size) {
QPixmap icon(size, size);
icon.fill(Qt::transparent);
return icon;
}
QString GetGameListCachedObject(const std::string& filename,
const std::string& ext,
const std::function<QString()>& generator)
{
if (!UISettings::values.cache_game_list || filename == "0000000000000000") {
return generator();
}
const auto path = Common::FS::PathToUTF8String(
Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir) / "game_list"
/ fmt::format("{}.{}", filename, ext));
void(Common::FS::CreateParentDirs(path));
if (!Common::FS::Exists(path)) {
const auto str = generator();
QFile file{QString::fromStdString(path)};
if (file.open(QFile::WriteOnly)) {
file.write(str.toUtf8());
}
return str;
}
QFile file{QString::fromStdString(path)};
if (file.open(QFile::ReadOnly)) {
return QString::fromUtf8(file.readAll());
}
return generator();
}
std::pair<std::vector<u8>, std::string> GetGameListCachedObject(
const std::string& filename,
const std::string& ext,
const std::function<std::pair<std::vector<u8>, std::string>()>& generator)
{
if (!UISettings::values.cache_game_list || filename == "0000000000000000") {
return generator();
}
const auto game_list_dir = Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir)
/ "game_list";
const auto jpeg_name = fmt::format("{}.jpeg", filename);
const auto app_name = fmt::format("{}.appname.txt", filename);
const auto path1 = Common::FS::PathToUTF8String(game_list_dir / jpeg_name);
const auto path2 = Common::FS::PathToUTF8String(game_list_dir / app_name);
void(Common::FS::CreateParentDirs(path1));
if (!Common::FS::Exists(path1) || !Common::FS::Exists(path2)) {
const auto [icon, nacp] = generator();
QFile file1{QString::fromStdString(path1)};
if (!file1.open(QFile::WriteOnly)) {
LOG_ERROR(Frontend, "Failed to open cache file.");
return generator();
}
if (!file1.resize(icon.size())) {
LOG_ERROR(Frontend, "Failed to resize cache file to necessary size.");
return generator();
}
if (file1.write(reinterpret_cast<const char*>(icon.data()), icon.size())
!= s64(icon.size())) {
LOG_ERROR(Frontend, "Failed to write data to cache file.");
return generator();
}
QFile file2{QString::fromStdString(path2)};
if (file2.open(QFile::WriteOnly)) {
file2.write(nacp.data(), nacp.size());
}
return std::make_pair(icon, nacp);
}
QFile file1(QString::fromStdString(path1));
QFile file2(QString::fromStdString(path2));
if (!file1.open(QFile::ReadOnly)) {
LOG_ERROR(Frontend, "Failed to open cache file for reading.");
return generator();
}
if (!file2.open(QFile::ReadOnly)) {
LOG_ERROR(Frontend, "Failed to open cache file for reading.");
return generator();
}
std::vector<u8> vec(file1.size());
if (file1.read(reinterpret_cast<char*>(vec.data()), vec.size())
!= static_cast<s64>(vec.size())) {
return generator();
}
const auto data = file2.readAll();
return std::make_pair(vec, data.toStdString());
}
void GetMetadataFromControlNCA(const FileSys::PatchManager& patch_manager,
const FileSys::NCA& nca,
std::vector<u8>& icon,
std::string& name)
{
std::tie(icon, name) = GetGameListCachedObject(
fmt::format("{:016X}", patch_manager.GetTitleID()), {}, [&patch_manager, &nca] {
const auto [nacp, icon_f] = patch_manager.ParseControlNCA(nca);
return std::make_pair(icon_f->ReadAllBytes(), nacp->GetApplicationName());
});
}
bool HasSupportedFileExtension(const std::string& file_name)
{
const QFileInfo file = QFileInfo(QString::fromStdString(file_name));
return QtCommon::supported_file_extensions.contains(file.suffix(), Qt::CaseInsensitive);
}
bool IsExtractedNCAMain(const std::string& file_name)
{
return QFileInfo(QString::fromStdString(file_name)).fileName() == QStringLiteral("main");
}
// QString FormatGameName(const std::string& physical_name)
// {
// const QString physical_name_as_qstring = QString::fromStdString(physical_name);
// const QFileInfo file_info(physical_name_as_qstring);
// if (IsExtractedNCAMain(physical_name)) {
// return file_info.dir().path();
// }
// return physical_name_as_qstring;
// }
QString FormatPatchNameVersions(const FileSys::PatchManager& patch_manager,
Loader::AppLoader& loader,
bool updatable = true)
{
QString out;
FileSys::VirtualFile update_raw;
loader.ReadUpdateRaw(update_raw);
for (const auto& patch : patch_manager.GetPatches(update_raw)) {
const bool is_update = patch.name == "Update";
if (!updatable && is_update) {
continue;
}
const QString type = QString::fromStdString(patch.enabled ? patch.name
: "[D] " + patch.name);
if (patch.version.empty()) {
out.append(QStringLiteral("%1\n").arg(type));
} else {
auto ver = patch.version;
// Display container name for packed updates
if (is_update && ver == "PACKED") {
ver = Loader::GetFileTypeString(loader.GetFileType());
}
out.append(QStringLiteral("%1 (%2)\n").arg(type, QString::fromStdString(ver)));
}
}
out.chop(1);
return out;
}
QStandardItem* MakeGameListEntry(const std::string& path,
const std::string& name,
const std::size_t size,
const std::vector<u8>& icon,
Loader::AppLoader& loader,
u64 program_id,
const FileSys::PatchManager& patch)
{
auto const file_type = loader.GetFileType();
auto const file_type_string = QString::fromStdString(Loader::GetFileTypeString(file_type));
QString patch_versions = GetGameListCachedObject(
fmt::format("{:016X}", patch.GetTitleID()), "pv.txt", [&patch, &loader] {
return FormatPatchNameVersions(patch, loader, loader.IsRomFSUpdatable());
});
QStandardItem* item = new QStandardItem(QString::fromStdString(name));
enum GLMRoleTypes { NAME = Qt::UserRole + 1, PATH, FILESIZE };
item->setData(QString::fromStdString(name), GameListModel::NAME);
item->setData(QString::fromStdString(path), GameListModel::PATH);
const u32 pic_size = UISettings::values.game_icon_size.GetValue();
QPixmap picture;
if (!picture.loadFromData(icon.data(), static_cast<u32>(icon.size()))) {
picture = GetDefaultIcon(pic_size);
}
picture = picture.scaled(pic_size, pic_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
item->setData(picture, GameListModel::ICON);
// item->setData(QVariant::fromValue(size), GameListModel::FILESIZE);
return item;
}
} // Anonymous namespace
GameListWorker::GameListWorker(QVector<UISettings::GameDir>& game_dirs_)
: game_dirs{game_dirs_}
{
// We want the game list to manage our lifetime.
setAutoDelete(false);
}
GameListWorker::~GameListWorker()
{
this->disconnect();
stop_requested.store(true);
processing_completed.Wait();
}
void GameListWorker::ProcessEvents(GameListModel* game_list)
{
while (true) {
std::function<void(GameListModel*)> func;
{
// Lock queue to protect concurrent modification.
std::scoped_lock lk(lock);
// If we can't pop a function, return.
if (queued_events.empty()) {
return;
}
// Pop a function.
func = std::move(queued_events.back());
queued_events.pop_back();
}
// Run the function.
func(game_list);
}
}
template<typename F>
void GameListWorker::RecordEvent(F&& func)
{
{
// Lock queue to protect concurrent modification.
std::scoped_lock lk(lock);
// Add the function into the front of the queue.
queued_events.emplace_front(std::move(func));
}
// Data now available.
emit DataAvailable();
}
void GameListWorker::AddTitlesToGameList(UISettings::GameDir& parent_dir)
{
using namespace FileSys;
const auto& cache = QtCommon::system->GetContentProviderUnion();
auto installed_games = cache.ListEntriesFilterOrigin(std::nullopt,
TitleType::Application,
ContentRecordType::Program);
for (const auto& [slot, game] : installed_games) {
if (slot == ContentProviderUnionSlot::FrontendManual) {
continue;
}
const auto file = cache.GetEntryUnparsed(game.title_id, game.type);
std::unique_ptr<Loader::AppLoader> loader = Loader::GetLoader(*QtCommon::system, file);
if (!loader) {
continue;
}
std::vector<u8> icon;
std::string name;
u64 program_id = 0;
const auto result = loader->ReadProgramId(program_id);
if (result != Loader::ResultStatus::Success) {
continue;
}
const PatchManager patch{program_id,
QtCommon::system->GetFileSystemController(),
QtCommon::system->GetContentProvider()};
LOG_INFO(Frontend, "PatchManager initiated for id {:X}", program_id);
const auto control = cache.GetEntry(game.title_id, ContentRecordType::Control);
if (control != nullptr) {
GetMetadataFromControlNCA(patch, *control, icon, name);
}
auto entry = MakeGameListEntry(file->GetFullPath(),
name,
file->GetSize(),
icon,
*loader,
program_id,
patch);
RecordEvent([=](GameListModel* game_list) { game_list->addEntry(entry, parent_dir); });
}
}
void GameListWorker::ScanFileSystem(ScanTarget target,
const std::string& dir_path,
bool deep_scan,
UISettings::GameDir& parent_dir)
{
const auto callback = [this, target, parent_dir](const std::filesystem::path& path) -> bool {
if (stop_requested) {
// Breaks the callback loop.
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))) {
const auto file = QtCommon::vfs->OpenFile(physical_name, FileSys::OpenMode::Read);
if (!file) {
return true;
}
auto loader = Loader::GetLoader(*QtCommon::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) {
QtCommon::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) {
QtCommon::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(*QtCommon::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,
QtCommon::system->GetFileSystemController(),
QtCommon::system->GetContentProvider()};
auto entry = MakeGameListEntry(physical_name,
name,
Common::FS::GetSize(physical_name),
icon,
*loader,
id,
patch);
RecordEvent([=](GameListModel* game_list) {
game_list->addEntry(entry, parent_dir);
});
}
} else {
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{program_id,
QtCommon::system->GetFileSystemController(),
QtCommon::system->GetContentProvider()};
auto entry = MakeGameListEntry(physical_name,
name,
Common::FS::GetSize(physical_name),
icon,
*loader,
program_id,
patch);
RecordEvent(
[=](GameListModel* game_list) { game_list->addEntry(entry, parent_dir); });
}
}
} else if (is_dir) {
watch_list.append(QString::fromStdString(physical_name));
}
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::File);
}
}
void GameListWorker::run()
{
watch_list.clear();
QtCommon::provider->ClearAllEntries();
const auto DirEntryReady = [&](UISettings::GameDir& game_list_dir) {
RecordEvent([=](GameListModel* game_list) { game_list->addDirEntry(game_list_dir); });
};
for (UISettings::GameDir& game_dir : game_dirs) {
if (stop_requested) {
break;
}
watch_list.append(QString::fromStdString(game_dir.path));
DirEntryReady(game_dir);
ScanFileSystem(ScanTarget::FillManualContentProvider, game_dir.path, game_dir.deep_scan,
game_dir);
ScanFileSystem(ScanTarget::PopulateGameList, game_dir.path, game_dir.deep_scan,
game_dir);
}
RecordEvent([this](GameListModel* game_list) { game_list->donePopulating(watch_list); });
processing_completed.Set();
}

81
src/Eden/Models/GameListWorker.h

@ -0,0 +1,81 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <atomic>
#include <deque>
#include <memory>
#include <string>
#include <QList>
#include <QObject>
#include <QRunnable>
#include <QString>
#include "common/thread.h"
#include "core/file_sys/registered_cache.h"
#include "qt_common/uisettings.h"
namespace Core { class System; }
class GameListModel;
class QStandardItem;
namespace FileSys {
class NCA;
class VfsFilesystem;
} // namespace FileSys
/**
* Asynchronous worker object for populating the game list.
* Communicates with other threads through Qt's signal/slot system.
*/
class GameListWorker : public QObject, public QRunnable {
Q_OBJECT
public:
explicit GameListWorker(QVector<UISettings::GameDir>& game_dirs_);
~GameListWorker() override;
/// Starts the processing of directory tree information.
void run() override;
public:
/**
* Synchronously processes any events queued by the worker.
*
* AddDirEntry is called on the game list for every discovered directory.
* AddEntry is called on the game list for every discovered program.
* DonePopulating is called on the game list when processing completes.
*/
void ProcessEvents(GameListModel* game_list);
signals:
void DataAvailable();
private:
template <typename F>
void RecordEvent(F&& func);
private:
void AddTitlesToGameList(UISettings::GameDir& parent_dir);
enum class ScanTarget {
FillManualContentProvider,
PopulateGameList,
};
void ScanFileSystem(ScanTarget target, const std::string& dir_path, bool deep_scan,
UISettings::GameDir& parent_dir);
QVector<UISettings::GameDir>& game_dirs;
QStringList watch_list;
std::mutex lock;
std::condition_variable cv;
std::deque<std::function<void(GameListModel*)>> queued_events;
std::atomic_bool stop_requested = false;
Common::Event processing_completed;
};

15
src/Eden/Native/main.cpp

@ -6,8 +6,10 @@
#include "Interface/TitleManager.h"
#include "Models/GameListModel.h"
#include "core/core.h"
#include "qt_common/qt_common.h"
#include <QQuickStyle>
#include <qwidget.h>
int main(int argc, char *argv[])
{
@ -20,14 +22,17 @@ int main(int argc, char *argv[])
QApplication::setDesktopFileName(QStringLiteral("org.eden-emu.eden"));
QGuiApplication::setWindowIcon(QIcon(":/icons/eden.svg"));
/// QtCommon
QtCommon::Init(new QWidget);
/// Settings, etc
Settings::SetConfiguringGlobal(true);
QMLConfig *config = new QMLConfig;
// // TODO: Save all values on launch and per game etc
// app.connect(&app, &QCoreApplication::aboutToQuit, &app, [config]() {
// config->save();
// });
// TODO: Save all values on launch and per game etc
app.connect(&app, &QCoreApplication::aboutToQuit, &app, [config]() {
config->save();
});
/// Expose Enums
@ -44,7 +49,7 @@ int main(int argc, char *argv[])
qmlRegisterUncreatableMetaObject(SettingsCategories::staticMetaObject, "Eden.Interface", 1, 0, "SettingsCategories", QString());
// Directory List
GameListModel *gameListModel = new GameListModel(&app);
GameListModel *gameListModel = new GameListModel(&app, &engine);
ctx->setContextProperty(QStringLiteral("EdenGameList"), gameListModel);
// Settings Interface

2
src/video_core/CMakeLists.txt

@ -331,8 +331,6 @@ target_link_options(video_core PRIVATE ${FFmpeg_LDFLAGS})
add_dependencies(video_core host_shaders)
target_include_directories(video_core PRIVATE ${HOST_SHADERS_INCLUDE})
target_link_libraries(video_core PRIVATE sirit::sirit)
# Header-only stuff needed by all dependent targets
target_link_libraries(video_core PUBLIC Vulkan::UtilityHeaders GPUOpen::VulkanMemoryAllocator)

Loading…
Cancel
Save