From 3c38afad74c751a949de0bcca9a51cfc11797f68 Mon Sep 17 00:00:00 2001 From: xbzk Date: Fri, 18 Sep 2026 23:48:35 +0200 Subject: [PATCH] [fs] share parent resources with bundled programs (#4319) - [x] I have read and followed the [Contribution Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/CONTRIBUTING.md#code-contributions). - [x] I have read and followed the [AI Policy](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/AI.md) - [x] I have read and followed the [Coding Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/Coding.md) to the best of my ability. ------------------- Normal games are Applications; Bundled games are Application + Programs; We've been treating both as the same thing, calling both programs, and making programs unable to see settings and saves from application. References: https://switchbrew.org/wiki/Filesystem_services https://switchbrew.org/wiki/NCM_services#ApplicationId https://switchbrew.org/wiki/Super_Mario_3D_All-Stars Ryujinx.HLE/HOS/ApplicationLoader.cs:GetMultiProgramInfo UPDATE: The first version was a big rework to make AppId/ProgId concepts proper, and it was successful in most tests, but it caused a regression with applets, which are programs that breed straight from no application. In order to simplify things i've reverted all the rework, added a helper to identify whether a program has a parent Application and bounded it to it's proper stores. Although this is no big deal coz it doesn't have impact on emulation, it permeates to several routines, so it needs to be tested broadly. Applets, Normal games, Bundled games, Blobbed NSPs/XCIs, Mods, Updates, Saves, DLCs,. Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4319 Reviewed-by: MaranBr Reviewed-by: lizzie --- src/core/file_sys/patch_manager.cpp | 68 +++++++++++++++++++------- src/core/file_sys/patch_manager.h | 4 ++ src/core/file_sys/registered_cache.cpp | 17 +++++++ src/core/file_sys/registered_cache.h | 2 + src/core/file_sys/savedata_factory.cpp | 22 ++++++--- src/core/file_sys/savedata_factory.h | 4 ++ 6 files changed, 91 insertions(+), 26 deletions(-) diff --git a/src/core/file_sys/patch_manager.cpp b/src/core/file_sys/patch_manager.cpp index ada8c44e78..4fd7dc6c71 100644 --- a/src/core/file_sys/patch_manager.cpp +++ b/src/core/file_sys/patch_manager.cpp @@ -9,6 +9,7 @@ #include #include +#include "common/assert.h" #include "common/hex_util.h" #include "common/logging.h" #include "common/settings.h" @@ -156,12 +157,15 @@ std::string GetUpdateVersionStringFromSlot(const ContentProvider* provider, u64 NACP nacp{nacp_file}; return nacp.GetVersionString(); } +YUZU_NO_INLINE std::optional GetParentApplicationId(const ContentProvider* provider, u64 title_id) { + return provider == nullptr ? std::nullopt : provider->GetParentApplicationId(title_id); +} } // Anonymous namespace PatchManager::PatchManager(u64 title_id_, const Service::FileSystem::FileSystemController& fs_controller_, const ContentProvider& content_provider_) - : title_id{title_id_}, fs_controller{fs_controller_}, content_provider{content_provider_} {} + : title_id{title_id_}, parent_title_id{GetParentApplicationId(std::addressof(content_provider_), title_id_)}, fs_controller{fs_controller_}, content_provider{content_provider_} {} PatchManager::~PatchManager() = default; @@ -169,13 +173,35 @@ u64 PatchManager::GetTitleID() const { return title_id; } +VirtualDir PatchManager::GetModificationLoadRoot(bool sdmc) const { + const auto get_root = [&](u64 id) { + return sdmc ? fs_controller.GetSDMCModificationLoadRoot(id) : fs_controller.GetModificationLoadRoot(id); + }; + auto root = get_root(title_id); + if (!parent_title_id) { + return root; + } + std::vector roots{std::move(root), get_root(*parent_title_id)}; + std::erase(roots, nullptr); + return LayeredVfsDirectory::MakeLayeredDirectory(std::move(roots)); +} + +std::vector PatchManager::GetDisabledAddons() const { + auto disabled = Settings::values.disabled_addons[title_id]; + if (parent_title_id) { + const auto& shared = Settings::values.disabled_addons[*parent_title_id]; + disabled.insert(disabled.end(), shared.begin(), shared.end()); + } + return disabled; +} + VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const { LOG_INFO(Loader, "Patching ExeFS for title_id={:016X}", title_id); if (exefs == nullptr) return exefs; - const auto& disabled = Settings::values.disabled_addons[title_id]; + const auto disabled = GetDisabledAddons(); bool update_disabled = true; std::optional enabled_version; @@ -303,8 +329,8 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const { } // LayeredExeFS - const auto load_dir = fs_controller.GetModificationLoadRoot(title_id); - const auto sdmc_load_dir = fs_controller.GetSDMCModificationLoadRoot(title_id); + const auto load_dir = GetModificationLoadRoot(); + const auto sdmc_load_dir = GetModificationLoadRoot(true); std::vector patch_dirs = {sdmc_load_dir}; if (load_dir != nullptr) { @@ -346,7 +372,7 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const { } std::vector PatchManager::CollectPatches(const std::vector& patch_dirs, const std::string& build_id) const { - const auto& disabled = Settings::values.disabled_addons[title_id]; + const auto disabled = GetDisabledAddons(); const auto nso_build_id = fmt::format("{:0<64}", build_id); std::vector out; @@ -405,7 +431,7 @@ std::vector PatchManager::PatchNSO(const std::vector& nso, const std::st LOG_INFO(Loader, "Patching NSO for name={}, build_id={}", name, build_id); - const auto load_dir = fs_controller.GetModificationLoadRoot(title_id); + const auto load_dir = GetModificationLoadRoot(); if (load_dir == nullptr) { LOG_ERROR(Loader, "Cannot load mods for invalid title_id={:016X}", title_id); return nso; @@ -448,7 +474,7 @@ bool PatchManager::HasNSOPatch(const BuildID& build_id_, std::string_view name) LOG_INFO(Loader, "Querying NSO patch existence for build_id={}, name={}", build_id, name); - const auto load_dir = fs_controller.GetModificationLoadRoot(title_id); + const auto load_dir = GetModificationLoadRoot(); if (load_dir == nullptr) { LOG_ERROR(Loader, "Cannot load mods for invalid title_id={:016X}", title_id); return false; @@ -462,13 +488,13 @@ bool PatchManager::HasNSOPatch(const BuildID& build_id_, std::string_view name) } std::vector PatchManager::CreateCheatList(const BuildID& build_id_) const { - const auto load_dir = fs_controller.GetModificationLoadRoot(title_id); + const auto load_dir = GetModificationLoadRoot(); if (load_dir == nullptr) { LOG_ERROR(Loader, "Cannot load mods for invalid title_id={:016X}", title_id); return {}; } - const auto& disabled = Settings::values.disabled_addons[title_id]; + const auto disabled = GetDisabledAddons(); auto patch_dirs = load_dir->GetSubdirectories(); std::sort(patch_dirs.begin(), patch_dirs.end(), [](auto const& l, auto const& r) { return l->GetName() < r->GetName(); }); @@ -502,17 +528,16 @@ std::vector PatchManager::CreateCheatList(const BuildI return out; } -static void ApplyLayeredFS(VirtualFile& romfs, u64 title_id, ContentRecordType type, - const Service::FileSystem::FileSystemController& fs_controller) { - const auto load_dir = fs_controller.GetModificationLoadRoot(title_id); - const auto sdmc_load_dir = fs_controller.GetSDMCModificationLoadRoot(title_id); +void PatchManager::ApplyLayeredFS(VirtualFile& romfs, ContentRecordType type) const { + const auto load_dir = GetModificationLoadRoot(); + const auto sdmc_load_dir = GetModificationLoadRoot(true); if ((type != ContentRecordType::Program && type != ContentRecordType::Data && type != ContentRecordType::HtmlDocument) || (load_dir == nullptr && sdmc_load_dir == nullptr)) { return; } - const auto& disabled = Settings::values.disabled_addons[title_id]; + const auto disabled = GetDisabledAddons(); std::vector patch_dirs = load_dir->GetSubdirectories(); if (std::find(disabled.cbegin(), disabled.cend(), "SDMC") == disabled.cend()) { patch_dirs.push_back(sdmc_load_dir); @@ -591,7 +616,7 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs // Game Updates const auto update_tid = GetUpdateTitleID(title_id); - const auto& disabled = Settings::values.disabled_addons[title_id]; + const auto disabled = GetDisabledAddons(); bool update_disabled = true; std::optional enabled_version; @@ -698,7 +723,7 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs // LayeredFS if (apply_layeredfs) { - ApplyLayeredFS(romfs, title_id, type, fs_controller); + ApplyLayeredFS(romfs, type); } return romfs; @@ -1072,15 +1097,22 @@ std::vector PatchManager::GetPatches(VirtualFile update_raw) const { std::optional PatchManager::GetGameVersion() const { const auto update_tid = GetUpdateTitleID(title_id); if (content_provider.HasEntry(update_tid, ContentRecordType::Program)) { - return content_provider.GetEntryVersion(update_tid); + const auto version = content_provider.GetEntryVersion(update_tid); + return version || !parent_title_id ? version : content_provider.GetEntryVersion(GetUpdateTitleID(*parent_title_id)); } - return content_provider.GetEntryVersion(title_id); + const auto version = content_provider.GetEntryVersion(title_id); + return version || !parent_title_id ? version : content_provider.GetEntryVersion(*parent_title_id); } PatchManager::Metadata PatchManager::GetControlMetadata() const { const auto base_control_nca = content_provider.GetEntry(title_id, ContentRecordType::Control); if (base_control_nca == nullptr) { + if (parent_title_id) { + const auto control_id = content_provider.HasEntry(*parent_title_id, ContentRecordType::Control) ? *parent_title_id : GetUpdateTitleID(*parent_title_id); + const PatchManager parent{control_id, fs_controller, content_provider}; + return parent.GetControlMetadata(); + } return {}; } diff --git a/src/core/file_sys/patch_manager.h b/src/core/file_sys/patch_manager.h index 5d71dbcf20..6819a1a3e7 100644 --- a/src/core/file_sys/patch_manager.h +++ b/src/core/file_sys/patch_manager.h @@ -109,10 +109,14 @@ public: [[nodiscard]] static PatchManager::Metadata GetMetadataFromBaseOrUpdate(Core::System& system, u64 application_id) noexcept; private: + [[nodiscard]] VirtualDir GetModificationLoadRoot(bool sdmc = false) const; + [[nodiscard]] std::vector GetDisabledAddons() const; + void ApplyLayeredFS(VirtualFile& romfs, ContentRecordType type) const; [[nodiscard]] std::vector CollectPatches(const std::vector& patch_dirs, const std::string& build_id) const; u64 title_id; + std::optional parent_title_id; const Service::FileSystem::FileSystemController& fs_controller; const ContentProvider& content_provider; }; diff --git a/src/core/file_sys/registered_cache.cpp b/src/core/file_sys/registered_cache.cpp index ce851e4d38..1f430434f7 100644 --- a/src/core/file_sys/registered_cache.cpp +++ b/src/core/file_sys/registered_cache.cpp @@ -5,6 +5,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include +#include #include #include #include @@ -348,6 +349,22 @@ std::vector ContentProvider::ListEntries() const { return ListEntriesFilter(std::nullopt, std::nullopt, std::nullopt); } +std::optional ContentProvider::GetParentApplicationId(u64 program_id) const { + const auto application_id = GetBaseTitleID(program_id); + const auto program_index = program_id - application_id; + if (program_index == 0 || program_index > std::numeric_limits::max()) { + return std::nullopt; + } + if (!ListEntriesFilter(TitleType::Application, ContentRecordType::Meta, program_id).empty()) { + return std::nullopt; + } + if ((!ListEntriesFilter(TitleType::Application, ContentRecordType::Meta, application_id).empty() && HasEntry(program_id, ContentRecordType::Program)) + || (!ListEntriesFilter(TitleType::Update, ContentRecordType::Meta, GetUpdateTitleID(application_id)).empty() && HasEntry(GetUpdateTitleID(program_id), ContentRecordType::Program))) { + return application_id; + } + return std::nullopt; +} + PlaceholderCache::PlaceholderCache(VirtualDir dir_) : dir(std::move(dir_)) {} bool PlaceholderCache::Create(const NcaID& id, u64 size) const { diff --git a/src/core/file_sys/registered_cache.h b/src/core/file_sys/registered_cache.h index a8e60ad470..89140a2e95 100644 --- a/src/core/file_sys/registered_cache.h +++ b/src/core/file_sys/registered_cache.h @@ -100,6 +100,8 @@ public: std::optional title_type = {}, std::optional record_type = {}, std::optional title_id = {}) const = 0; + [[nodiscard]] std::optional GetParentApplicationId(u64 program_id) const; + protected: // A single instance of KeyManager to be used by GetEntry() Core::Crypto::KeyManager& keys = Core::Crypto::KeyManager::Instance(); diff --git a/src/core/file_sys/savedata_factory.cpp b/src/core/file_sys/savedata_factory.cpp index c756734a43..72f022cb41 100644 --- a/src/core/file_sys/savedata_factory.cpp +++ b/src/core/file_sys/savedata_factory.cpp @@ -9,6 +9,7 @@ #include "common/logging.h" #include "common/uuid.h" #include "core/core.h" +#include "core/file_sys/registered_cache.h" #include "core/file_sys/savedata_factory.h" #include "core/file_sys/vfs/vfs.h" @@ -61,17 +62,24 @@ SaveDataFactory::SaveDataFactory(Core::System& system_, ProgramId program_id_, SaveDataFactory::~SaveDataFactory() = default; +std::string SaveDataFactory::GetSaveDataPath(SaveDataSpaceId space, SaveDataType type, u64 title_id, u128 user_id, u64 save_id) const { + if (type == SaveDataType::Account || type == SaveDataType::Device) { + const auto requested_id = title_id != 0 ? title_id : program_id; + const auto parent_id = system.GetContentProvider().GetParentApplicationId(requested_id); + title_id = parent_id.value_or(requested_id); + } + return GetFullPath(program_id, dir, space, type, title_id, user_id, save_id); +} + VirtualDir SaveDataFactory::Create(SaveDataSpaceId space, const SaveDataAttribute& meta) const { - const auto save_directory = GetFullPath(program_id, dir, space, meta.type, meta.program_id, - meta.user_id, meta.system_save_data_id); + const auto save_directory = GetSaveDataPath(space, meta.type, meta.program_id, meta.user_id, meta.system_save_data_id); return dir->CreateDirectoryRelative(save_directory); } VirtualDir SaveDataFactory::Open(SaveDataSpaceId space, const SaveDataAttribute& meta) const { - const auto save_directory = GetFullPath(program_id, dir, space, meta.type, meta.program_id, - meta.user_id, meta.system_save_data_id); + const auto save_directory = GetSaveDataPath(space, meta.type, meta.program_id, meta.user_id, meta.system_save_data_id); auto out = dir->GetDirectoryRelative(save_directory); @@ -154,8 +162,7 @@ std::string SaveDataFactory::GetUserGameSaveDataRoot(u128 user_id, bool future) SaveDataSize SaveDataFactory::ReadSaveDataSize(SaveDataType type, u64 title_id, u128 user_id) const { - const auto path = - GetFullPath(program_id, dir, SaveDataSpaceId::User, type, title_id, user_id, 0); + const auto path = GetSaveDataPath(SaveDataSpaceId::User, type, title_id, user_id, 0); const auto relative_dir = GetOrCreateDirectoryRelative(dir, path); const auto size_file = relative_dir->GetFile(GetSaveDataSizeFileName()); @@ -173,8 +180,7 @@ SaveDataSize SaveDataFactory::ReadSaveDataSize(SaveDataType type, u64 title_id, void SaveDataFactory::WriteSaveDataSize(SaveDataType type, u64 title_id, u128 user_id, SaveDataSize new_value) const { - const auto path = - GetFullPath(program_id, dir, SaveDataSpaceId::User, type, title_id, user_id, 0); + const auto path = GetSaveDataPath(SaveDataSpaceId::User, type, title_id, user_id, 0); const auto relative_dir = GetOrCreateDirectoryRelative(dir, path); const auto size_file = relative_dir->CreateFile(GetSaveDataSizeFileName()); diff --git a/src/core/file_sys/savedata_factory.h b/src/core/file_sys/savedata_factory.h index 15dd4ec7de..36c09ce2ac 100644 --- a/src/core/file_sys/savedata_factory.h +++ b/src/core/file_sys/savedata_factory.h @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -47,6 +50,7 @@ public: void SetAutoCreate(bool state); private: + std::string GetSaveDataPath(SaveDataSpaceId space, SaveDataType type, u64 title_id, u128 user_id, u64 save_id) const; Core::System& system; ProgramId program_id; VirtualDir dir;