Procházet zdrojové kódy

Merge pull request #1957 from DarkLordZach/title-provider

file_sys: Provide generic interface for accessing game data
bunnei před 7 roky
rodič
revize
61f63bb994

+ 26 - 0
src/core/core.cpp

@@ -17,6 +17,7 @@
 #include "core/core_timing.h"
 #include "core/cpu_core_manager.h"
 #include "core/file_sys/mode.h"
+#include "core/file_sys/registered_cache.h"
 #include "core/file_sys/vfs_concat.h"
 #include "core/file_sys/vfs_real.h"
 #include "core/gdbstub/gdbstub.h"
@@ -108,6 +109,8 @@ struct System::Impl {
         // Create a default fs if one doesn't already exist.
         if (virtual_filesystem == nullptr)
             virtual_filesystem = std::make_shared<FileSys::RealVfsFilesystem>();
+        if (content_provider == nullptr)
+            content_provider = std::make_unique<FileSys::ContentProviderUnion>();
 
         /// Create default implementations of applets if one is not provided.
         if (profile_selector == nullptr)
@@ -249,6 +252,8 @@ struct System::Impl {
     Kernel::KernelCore kernel;
     /// RealVfsFilesystem instance
     FileSys::VirtualFilesystem virtual_filesystem;
+    /// ContentProviderUnion instance
+    std::unique_ptr<FileSys::ContentProviderUnion> content_provider;
     /// AppLoader used to load the current executing application
     std::unique_ptr<Loader::AppLoader> app_loader;
     std::unique_ptr<VideoCore::RendererBase> renderer;
@@ -488,6 +493,27 @@ const Frontend::SoftwareKeyboardApplet& System::GetSoftwareKeyboard() const {
     return *impl->software_keyboard;
 }
 
+void System::SetContentProvider(std::unique_ptr<FileSys::ContentProviderUnion> provider) {
+    impl->content_provider = std::move(provider);
+}
+
+FileSys::ContentProvider& System::GetContentProvider() {
+    return *impl->content_provider;
+}
+
+const FileSys::ContentProvider& System::GetContentProvider() const {
+    return *impl->content_provider;
+}
+
+void System::RegisterContentProvider(FileSys::ContentProviderUnionSlot slot,
+                                     FileSys::ContentProvider* provider) {
+    impl->content_provider->SetSlot(slot, provider);
+}
+
+void System::ClearContentProvider(FileSys::ContentProviderUnionSlot slot) {
+    impl->content_provider->ClearSlot(slot);
+}
+
 void System::SetWebBrowser(std::unique_ptr<Frontend::WebBrowserApplet> applet) {
     impl->web_browser = std::move(applet);
 }

+ 14 - 0
src/core/core.h

@@ -21,6 +21,9 @@ class WebBrowserApplet;
 
 namespace FileSys {
 class CheatList;
+class ContentProvider;
+class ContentProviderUnion;
+enum class ContentProviderUnionSlot;
 class VfsFilesystem;
 } // namespace FileSys
 
@@ -270,6 +273,17 @@ public:
     Frontend::WebBrowserApplet& GetWebBrowser();
     const Frontend::WebBrowserApplet& GetWebBrowser() const;
 
+    void SetContentProvider(std::unique_ptr<FileSys::ContentProviderUnion> provider);
+
+    FileSys::ContentProvider& GetContentProvider();
+
+    const FileSys::ContentProvider& GetContentProvider() const;
+
+    void RegisterContentProvider(FileSys::ContentProviderUnionSlot slot,
+                                 FileSys::ContentProvider* provider);
+
+    void ClearContentProvider(FileSys::ContentProviderUnionSlot slot);
+
 private:
     System();
 

+ 2 - 1
src/core/crypto/key_manager.cpp

@@ -22,6 +22,7 @@
 #include "common/file_util.h"
 #include "common/hex_util.h"
 #include "common/logging/log.h"
+#include "core/core.h"
 #include "core/crypto/aes_util.h"
 #include "core/crypto/key_manager.h"
 #include "core/crypto/partition_data_manager.h"
@@ -794,7 +795,7 @@ void KeyManager::DeriveBase() {
 
 void KeyManager::DeriveETicket(PartitionDataManager& data) {
     // ETicket keys
-    const auto es = Service::FileSystem::GetUnionContents().GetEntry(
+    const auto es = Core::System::GetInstance().GetContentProvider().GetEntry(
         0x0100000000000033, FileSys::ContentRecordType::Program);
 
     if (es == nullptr)

+ 12 - 10
src/core/file_sys/patch_manager.cpp

@@ -10,6 +10,7 @@
 #include "common/file_util.h"
 #include "common/hex_util.h"
 #include "common/logging/log.h"
+#include "core/core.h"
 #include "core/file_sys/content_archive.h"
 #include "core/file_sys/control_metadata.h"
 #include "core/file_sys/ips_layer.h"
@@ -69,7 +70,7 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const {
         }
     }
 
-    const auto installed = Service::FileSystem::GetUnionContents();
+    const auto& installed = Core::System::GetInstance().GetContentProvider();
 
     const auto& disabled = Settings::values.disabled_addons[title_id];
     const auto update_disabled =
@@ -155,7 +156,7 @@ std::vector<VirtualFile> PatchManager::CollectPatches(const std::vector<VirtualD
     return out;
 }
 
-std::vector<u8> PatchManager::PatchNSO(const std::vector<u8>& nso) const {
+std::vector<u8> PatchManager::PatchNSO(const std::vector<u8>& nso, const std::string& name) const {
     if (nso.size() < sizeof(Loader::NSOHeader)) {
         return nso;
     }
@@ -171,18 +172,19 @@ std::vector<u8> PatchManager::PatchNSO(const std::vector<u8>& nso) const {
     const auto build_id = build_id_raw.substr(0, build_id_raw.find_last_not_of('0') + 1);
 
     if (Settings::values.dump_nso) {
-        LOG_INFO(Loader, "Dumping NSO for build_id={}, title_id={:016X}", build_id, title_id);
+        LOG_INFO(Loader, "Dumping NSO for name={}, build_id={}, title_id={:016X}", name, build_id,
+                 title_id);
         const auto dump_dir = Service::FileSystem::GetModificationDumpRoot(title_id);
         if (dump_dir != nullptr) {
             const auto nso_dir = GetOrCreateDirectoryRelative(dump_dir, "/nso");
-            const auto file = nso_dir->CreateFile(fmt::format("{}.nso", build_id));
+            const auto file = nso_dir->CreateFile(fmt::format("{}-{}.nso", name, build_id));
 
             file->Resize(nso.size());
             file->WriteBytes(nso);
         }
     }
 
-    LOG_INFO(Loader, "Patching NSO for build_id={}", build_id);
+    LOG_INFO(Loader, "Patching NSO for name={}, build_id={}", name, build_id);
 
     const auto load_dir = Service::FileSystem::GetModificationLoadRoot(title_id);
     auto patch_dirs = load_dir->GetSubdirectories();
@@ -345,7 +347,7 @@ VirtualFile PatchManager::PatchRomFS(VirtualFile romfs, u64 ivfc_offset, Content
     if (romfs == nullptr)
         return romfs;
 
-    const auto installed = Service::FileSystem::GetUnionContents();
+    const auto& installed = Core::System::GetInstance().GetContentProvider();
 
     // Game Updates
     const auto update_tid = GetUpdateTitleID(title_id);
@@ -392,7 +394,7 @@ static bool IsDirValidAndNonEmpty(const VirtualDir& dir) {
 std::map<std::string, std::string, std::less<>> PatchManager::GetPatchVersionNames(
     VirtualFile update_raw) const {
     std::map<std::string, std::string, std::less<>> out;
-    const auto installed = Service::FileSystem::GetUnionContents();
+    const auto& installed = Core::System::GetInstance().GetContentProvider();
     const auto& disabled = Settings::values.disabled_addons[title_id];
 
     // Game Updates
@@ -466,10 +468,10 @@ std::map<std::string, std::string, std::less<>> PatchManager::GetPatchVersionNam
 
     // DLC
     const auto dlc_entries = installed.ListEntriesFilter(TitleType::AOC, ContentRecordType::Data);
-    std::vector<RegisteredCacheEntry> dlc_match;
+    std::vector<ContentProviderEntry> dlc_match;
     dlc_match.reserve(dlc_entries.size());
     std::copy_if(dlc_entries.begin(), dlc_entries.end(), std::back_inserter(dlc_match),
-                 [this, &installed](const RegisteredCacheEntry& entry) {
+                 [this, &installed](const ContentProviderEntry& entry) {
                      return (entry.title_id & DLC_BASE_TITLE_ID_MASK) == title_id &&
                             installed.GetEntry(entry)->GetStatus() == Loader::ResultStatus::Success;
                  });
@@ -492,7 +494,7 @@ std::map<std::string, std::string, std::less<>> PatchManager::GetPatchVersionNam
 }
 
 std::pair<std::unique_ptr<NACP>, VirtualFile> PatchManager::GetControlMetadata() const {
-    const auto installed{Service::FileSystem::GetUnionContents()};
+    const auto& installed = Core::System::GetInstance().GetContentProvider();
 
     const auto base_control_nca = installed.GetEntry(title_id, ContentRecordType::Control);
     if (base_control_nca == nullptr)

+ 1 - 1
src/core/file_sys/patch_manager.h

@@ -44,7 +44,7 @@ public:
     // Currently tracked NSO patches:
     // - IPS
     // - IPSwitch
-    std::vector<u8> PatchNSO(const std::vector<u8>& nso) const;
+    std::vector<u8> PatchNSO(const std::vector<u8>& nso, const std::string& name) const;
 
     // Checks to see if PatchNSO() will have any effect given the NSO's build ID.
     // Used to prevent expensive copies in NSO loader.

+ 175 - 100
src/core/file_sys/registered_cache.cpp

@@ -23,19 +23,19 @@ namespace FileSys {
 // The size of blocks to use when vfs raw copying into nand.
 constexpr size_t VFS_RC_LARGE_COPY_BLOCK = 0x400000;
 
-std::string RegisteredCacheEntry::DebugInfo() const {
+std::string ContentProviderEntry::DebugInfo() const {
     return fmt::format("title_id={:016X}, content_type={:02X}", title_id, static_cast<u8>(type));
 }
 
-bool operator<(const RegisteredCacheEntry& lhs, const RegisteredCacheEntry& rhs) {
+bool operator<(const ContentProviderEntry& lhs, const ContentProviderEntry& rhs) {
     return (lhs.title_id < rhs.title_id) || (lhs.title_id == rhs.title_id && lhs.type < rhs.type);
 }
 
-bool operator==(const RegisteredCacheEntry& lhs, const RegisteredCacheEntry& rhs) {
+bool operator==(const ContentProviderEntry& lhs, const ContentProviderEntry& rhs) {
     return std::tie(lhs.title_id, lhs.type) == std::tie(rhs.title_id, rhs.type);
 }
 
-bool operator!=(const RegisteredCacheEntry& lhs, const RegisteredCacheEntry& rhs) {
+bool operator!=(const ContentProviderEntry& lhs, const ContentProviderEntry& rhs) {
     return !operator==(lhs, rhs);
 }
 
@@ -84,7 +84,7 @@ static std::string GetCNMTName(TitleType type, u64 title_id) {
     return fmt::format("{}_{:016x}.cnmt", TITLE_TYPE_NAMES[index], title_id);
 }
 
-static ContentRecordType GetCRTypeFromNCAType(NCAContentType type) {
+ContentRecordType GetCRTypeFromNCAType(NCAContentType type) {
     switch (type) {
     case NCAContentType::Program:
         // TODO(DarkLordZach): Differentiate between Program and Patch
@@ -104,6 +104,28 @@ static ContentRecordType GetCRTypeFromNCAType(NCAContentType type) {
     }
 }
 
+ContentProvider::~ContentProvider() = default;
+
+bool ContentProvider::HasEntry(ContentProviderEntry entry) const {
+    return HasEntry(entry.title_id, entry.type);
+}
+
+VirtualFile ContentProvider::GetEntryUnparsed(ContentProviderEntry entry) const {
+    return GetEntryUnparsed(entry.title_id, entry.type);
+}
+
+VirtualFile ContentProvider::GetEntryRaw(ContentProviderEntry entry) const {
+    return GetEntryRaw(entry.title_id, entry.type);
+}
+
+std::unique_ptr<NCA> ContentProvider::GetEntry(ContentProviderEntry entry) const {
+    return GetEntry(entry.title_id, entry.type);
+}
+
+std::vector<ContentProviderEntry> ContentProvider::ListEntries() const {
+    return ListEntriesFilter(std::nullopt, std::nullopt, std::nullopt);
+}
+
 VirtualFile RegisteredCache::OpenFileOrDirectoryConcat(const VirtualDir& dir,
                                                        std::string_view path) const {
     const auto file = dir->GetFileRelative(path);
@@ -161,8 +183,8 @@ VirtualFile RegisteredCache::GetFileAtID(NcaID id) const {
     return file;
 }
 
-static std::optional<NcaID> CheckMapForContentRecord(
-    const boost::container::flat_map<u64, CNMT>& map, u64 title_id, ContentRecordType type) {
+static std::optional<NcaID> CheckMapForContentRecord(const std::map<u64, CNMT>& map, u64 title_id,
+                                                     ContentRecordType type) {
     if (map.find(title_id) == map.end())
         return {};
 
@@ -268,7 +290,7 @@ void RegisteredCache::Refresh() {
     AccumulateYuzuMeta();
 }
 
-RegisteredCache::RegisteredCache(VirtualDir dir_, RegisteredCacheParsingFunction parsing_function)
+RegisteredCache::RegisteredCache(VirtualDir dir_, ContentProviderParsingFunction parsing_function)
     : dir(std::move(dir_)), parser(std::move(parsing_function)) {
     Refresh();
 }
@@ -279,19 +301,11 @@ bool RegisteredCache::HasEntry(u64 title_id, ContentRecordType type) const {
     return GetEntryRaw(title_id, type) != nullptr;
 }
 
-bool RegisteredCache::HasEntry(RegisteredCacheEntry entry) const {
-    return GetEntryRaw(entry) != nullptr;
-}
-
 VirtualFile RegisteredCache::GetEntryUnparsed(u64 title_id, ContentRecordType type) const {
     const auto id = GetNcaIDFromMetadata(title_id, type);
     return id ? GetFileAtID(*id) : nullptr;
 }
 
-VirtualFile RegisteredCache::GetEntryUnparsed(RegisteredCacheEntry entry) const {
-    return GetEntryUnparsed(entry.title_id, entry.type);
-}
-
 std::optional<u32> RegisteredCache::GetEntryVersion(u64 title_id) const {
     const auto meta_iter = meta.find(title_id);
     if (meta_iter != meta.end())
@@ -309,10 +323,6 @@ VirtualFile RegisteredCache::GetEntryRaw(u64 title_id, ContentRecordType type) c
     return id ? parser(GetFileAtID(*id), *id) : nullptr;
 }
 
-VirtualFile RegisteredCache::GetEntryRaw(RegisteredCacheEntry entry) const {
-    return GetEntryRaw(entry.title_id, entry.type);
-}
-
 std::unique_ptr<NCA> RegisteredCache::GetEntry(u64 title_id, ContentRecordType type) const {
     const auto raw = GetEntryRaw(title_id, type);
     if (raw == nullptr)
@@ -320,10 +330,6 @@ std::unique_ptr<NCA> RegisteredCache::GetEntry(u64 title_id, ContentRecordType t
     return std::make_unique<NCA>(raw, nullptr, 0, keys);
 }
 
-std::unique_ptr<NCA> RegisteredCache::GetEntry(RegisteredCacheEntry entry) const {
-    return GetEntry(entry.title_id, entry.type);
-}
-
 template <typename T>
 void RegisteredCache::IterateAllMetadata(
     std::vector<T>& out, std::function<T(const CNMT&, const ContentRecord&)> proc,
@@ -348,25 +354,14 @@ void RegisteredCache::IterateAllMetadata(
     }
 }
 
-std::vector<RegisteredCacheEntry> RegisteredCache::ListEntries() const {
-    std::vector<RegisteredCacheEntry> out;
-    IterateAllMetadata<RegisteredCacheEntry>(
-        out,
-        [](const CNMT& c, const ContentRecord& r) {
-            return RegisteredCacheEntry{c.GetTitleID(), r.type};
-        },
-        [](const CNMT& c, const ContentRecord& r) { return true; });
-    return out;
-}
-
-std::vector<RegisteredCacheEntry> RegisteredCache::ListEntriesFilter(
+std::vector<ContentProviderEntry> RegisteredCache::ListEntriesFilter(
     std::optional<TitleType> title_type, std::optional<ContentRecordType> record_type,
     std::optional<u64> title_id) const {
-    std::vector<RegisteredCacheEntry> out;
-    IterateAllMetadata<RegisteredCacheEntry>(
+    std::vector<ContentProviderEntry> out;
+    IterateAllMetadata<ContentProviderEntry>(
         out,
         [](const CNMT& c, const ContentRecord& r) {
-            return RegisteredCacheEntry{c.GetTitleID(), r.type};
+            return ContentProviderEntry{c.GetTitleID(), r.type};
         },
         [&title_type, &record_type, &title_id](const CNMT& c, const ContentRecord& r) {
             if (title_type && *title_type != c.GetType())
@@ -521,37 +516,56 @@ bool RegisteredCache::RawInstallYuzuMeta(const CNMT& cnmt) {
                         }) != yuzu_meta.end();
 }
 
-RegisteredCacheUnion::RegisteredCacheUnion(std::vector<RegisteredCache*> caches)
-    : caches(std::move(caches)) {}
+ContentProviderUnion::~ContentProviderUnion() = default;
 
-void RegisteredCacheUnion::Refresh() {
-    for (const auto& c : caches)
-        c->Refresh();
+void ContentProviderUnion::SetSlot(ContentProviderUnionSlot slot, ContentProvider* provider) {
+    providers[slot] = provider;
 }
 
-bool RegisteredCacheUnion::HasEntry(u64 title_id, ContentRecordType type) const {
-    return std::any_of(caches.begin(), caches.end(), [title_id, type](const auto& cache) {
-        return cache->HasEntry(title_id, type);
-    });
+void ContentProviderUnion::ClearSlot(ContentProviderUnionSlot slot) {
+    providers[slot] = nullptr;
 }
 
-bool RegisteredCacheUnion::HasEntry(RegisteredCacheEntry entry) const {
-    return HasEntry(entry.title_id, entry.type);
+void ContentProviderUnion::Refresh() {
+    for (auto& provider : providers) {
+        if (provider.second == nullptr)
+            continue;
+
+        provider.second->Refresh();
+    }
 }
 
-std::optional<u32> RegisteredCacheUnion::GetEntryVersion(u64 title_id) const {
-    for (const auto& c : caches) {
-        const auto res = c->GetEntryVersion(title_id);
-        if (res)
+bool ContentProviderUnion::HasEntry(u64 title_id, ContentRecordType type) const {
+    for (const auto& provider : providers) {
+        if (provider.second == nullptr)
+            continue;
+
+        if (provider.second->HasEntry(title_id, type))
+            return true;
+    }
+
+    return false;
+}
+
+std::optional<u32> ContentProviderUnion::GetEntryVersion(u64 title_id) const {
+    for (const auto& provider : providers) {
+        if (provider.second == nullptr)
+            continue;
+
+        const auto res = provider.second->GetEntryVersion(title_id);
+        if (res != std::nullopt)
             return res;
     }
 
-    return {};
+    return std::nullopt;
 }
 
-VirtualFile RegisteredCacheUnion::GetEntryUnparsed(u64 title_id, ContentRecordType type) const {
-    for (const auto& c : caches) {
-        const auto res = c->GetEntryUnparsed(title_id, type);
+VirtualFile ContentProviderUnion::GetEntryUnparsed(u64 title_id, ContentRecordType type) const {
+    for (const auto& provider : providers) {
+        if (provider.second == nullptr)
+            continue;
+
+        const auto res = provider.second->GetEntryUnparsed(title_id, type);
         if (res != nullptr)
             return res;
     }
@@ -559,13 +573,12 @@ VirtualFile RegisteredCacheUnion::GetEntryUnparsed(u64 title_id, ContentRecordTy
     return nullptr;
 }
 
-VirtualFile RegisteredCacheUnion::GetEntryUnparsed(RegisteredCacheEntry entry) const {
-    return GetEntryUnparsed(entry.title_id, entry.type);
-}
+VirtualFile ContentProviderUnion::GetEntryRaw(u64 title_id, ContentRecordType type) const {
+    for (const auto& provider : providers) {
+        if (provider.second == nullptr)
+            continue;
 
-VirtualFile RegisteredCacheUnion::GetEntryRaw(u64 title_id, ContentRecordType type) const {
-    for (const auto& c : caches) {
-        const auto res = c->GetEntryRaw(title_id, type);
+        const auto res = provider.second->GetEntryRaw(title_id, type);
         if (res != nullptr)
             return res;
     }
@@ -573,30 +586,56 @@ VirtualFile RegisteredCacheUnion::GetEntryRaw(u64 title_id, ContentRecordType ty
     return nullptr;
 }
 
-VirtualFile RegisteredCacheUnion::GetEntryRaw(RegisteredCacheEntry entry) const {
-    return GetEntryRaw(entry.title_id, entry.type);
-}
+std::unique_ptr<NCA> ContentProviderUnion::GetEntry(u64 title_id, ContentRecordType type) const {
+    for (const auto& provider : providers) {
+        if (provider.second == nullptr)
+            continue;
 
-std::unique_ptr<NCA> RegisteredCacheUnion::GetEntry(u64 title_id, ContentRecordType type) const {
-    const auto raw = GetEntryRaw(title_id, type);
-    if (raw == nullptr)
-        return nullptr;
-    return std::make_unique<NCA>(raw);
+        auto res = provider.second->GetEntry(title_id, type);
+        if (res != nullptr)
+            return res;
+    }
+
+    return nullptr;
 }
 
-std::unique_ptr<NCA> RegisteredCacheUnion::GetEntry(RegisteredCacheEntry entry) const {
-    return GetEntry(entry.title_id, entry.type);
+std::vector<ContentProviderEntry> ContentProviderUnion::ListEntriesFilter(
+    std::optional<TitleType> title_type, std::optional<ContentRecordType> record_type,
+    std::optional<u64> title_id) const {
+    std::vector<ContentProviderEntry> out;
+
+    for (const auto& provider : providers) {
+        if (provider.second == nullptr)
+            continue;
+
+        const auto vec = provider.second->ListEntriesFilter(title_type, record_type, title_id);
+        std::copy(vec.begin(), vec.end(), std::back_inserter(out));
+    }
+
+    std::sort(out.begin(), out.end());
+    out.erase(std::unique(out.begin(), out.end()), out.end());
+    return out;
 }
 
-std::vector<RegisteredCacheEntry> RegisteredCacheUnion::ListEntries() const {
-    std::vector<RegisteredCacheEntry> out;
-    for (const auto& c : caches) {
-        c->IterateAllMetadata<RegisteredCacheEntry>(
-            out,
-            [](const CNMT& c, const ContentRecord& r) {
-                return RegisteredCacheEntry{c.GetTitleID(), r.type};
-            },
-            [](const CNMT& c, const ContentRecord& r) { return true; });
+std::vector<std::pair<ContentProviderUnionSlot, ContentProviderEntry>>
+ContentProviderUnion::ListEntriesFilterOrigin(std::optional<ContentProviderUnionSlot> origin,
+                                              std::optional<TitleType> title_type,
+                                              std::optional<ContentRecordType> record_type,
+                                              std::optional<u64> title_id) const {
+    std::vector<std::pair<ContentProviderUnionSlot, ContentProviderEntry>> out;
+
+    for (const auto& provider : providers) {
+        if (provider.second == nullptr)
+            continue;
+
+        if (origin.has_value() && *origin != provider.first)
+            continue;
+
+        const auto vec = provider.second->ListEntriesFilter(title_type, record_type, title_id);
+        std::transform(vec.begin(), vec.end(), std::back_inserter(out),
+                       [&provider](const ContentProviderEntry& entry) {
+                           return std::make_pair(provider.first, entry);
+                       });
     }
 
     std::sort(out.begin(), out.end());
@@ -604,25 +643,61 @@ std::vector<RegisteredCacheEntry> RegisteredCacheUnion::ListEntries() const {
     return out;
 }
 
-std::vector<RegisteredCacheEntry> RegisteredCacheUnion::ListEntriesFilter(
+ManualContentProvider::~ManualContentProvider() = default;
+
+void ManualContentProvider::AddEntry(TitleType title_type, ContentRecordType content_type,
+                                     u64 title_id, VirtualFile file) {
+    entries.insert_or_assign({title_type, content_type, title_id}, file);
+}
+
+void ManualContentProvider::ClearAllEntries() {
+    entries.clear();
+}
+
+void ManualContentProvider::Refresh() {}
+
+bool ManualContentProvider::HasEntry(u64 title_id, ContentRecordType type) const {
+    return GetEntryRaw(title_id, type) != nullptr;
+}
+
+std::optional<u32> ManualContentProvider::GetEntryVersion(u64 title_id) const {
+    return std::nullopt;
+}
+
+VirtualFile ManualContentProvider::GetEntryUnparsed(u64 title_id, ContentRecordType type) const {
+    return GetEntryRaw(title_id, type);
+}
+
+VirtualFile ManualContentProvider::GetEntryRaw(u64 title_id, ContentRecordType type) const {
+    const auto iter =
+        std::find_if(entries.begin(), entries.end(), [title_id, type](const auto& entry) {
+            const auto [title_type, content_type, e_title_id] = entry.first;
+            return content_type == type && e_title_id == title_id;
+        });
+    if (iter == entries.end())
+        return nullptr;
+    return iter->second;
+}
+
+std::unique_ptr<NCA> ManualContentProvider::GetEntry(u64 title_id, ContentRecordType type) const {
+    const auto res = GetEntryRaw(title_id, type);
+    if (res == nullptr)
+        return nullptr;
+    return std::make_unique<NCA>(res, nullptr, 0, keys);
+}
+
+std::vector<ContentProviderEntry> ManualContentProvider::ListEntriesFilter(
     std::optional<TitleType> title_type, std::optional<ContentRecordType> record_type,
     std::optional<u64> title_id) const {
-    std::vector<RegisteredCacheEntry> out;
-    for (const auto& c : caches) {
-        c->IterateAllMetadata<RegisteredCacheEntry>(
-            out,
-            [](const CNMT& c, const ContentRecord& r) {
-                return RegisteredCacheEntry{c.GetTitleID(), r.type};
-            },
-            [&title_type, &record_type, &title_id](const CNMT& c, const ContentRecord& r) {
-                if (title_type && *title_type != c.GetType())
-                    return false;
-                if (record_type && *record_type != r.type)
-                    return false;
-                if (title_id && *title_id != c.GetTitleID())
-                    return false;
-                return true;
-            });
+    std::vector<ContentProviderEntry> out;
+
+    for (const auto& entry : entries) {
+        const auto [e_title_type, e_content_type, e_title_id] = entry.first;
+        if ((title_type == std::nullopt || e_title_type == *title_type) &&
+            (record_type == std::nullopt || e_content_type == *record_type) &&
+            (title_id == std::nullopt || e_title_id == *title_id)) {
+            out.emplace_back(ContentProviderEntry{e_title_id, e_content_type});
+        }
     }
 
     std::sort(out.begin(), out.end());

+ 104 - 52
src/core/file_sys/registered_cache.h

@@ -21,12 +21,13 @@ class NSP;
 class XCI;
 
 enum class ContentRecordType : u8;
+enum class NCAContentType : u8;
 enum class TitleType : u8;
 
 struct ContentRecord;
 
 using NcaID = std::array<u8, 0x10>;
-using RegisteredCacheParsingFunction = std::function<VirtualFile(const VirtualFile&, const NcaID&)>;
+using ContentProviderParsingFunction = std::function<VirtualFile(const VirtualFile&, const NcaID&)>;
 using VfsCopyFunction = std::function<bool(const VirtualFile&, const VirtualFile&, size_t)>;
 
 enum class InstallResult {
@@ -36,7 +37,7 @@ enum class InstallResult {
     ErrorMetaFailed,
 };
 
-struct RegisteredCacheEntry {
+struct ContentProviderEntry {
     u64 title_id;
     ContentRecordType type;
 
@@ -47,12 +48,46 @@ constexpr u64 GetUpdateTitleID(u64 base_title_id) {
     return base_title_id | 0x800;
 }
 
+ContentRecordType GetCRTypeFromNCAType(NCAContentType type);
+
 // boost flat_map requires operator< for O(log(n)) lookups.
-bool operator<(const RegisteredCacheEntry& lhs, const RegisteredCacheEntry& rhs);
+bool operator<(const ContentProviderEntry& lhs, const ContentProviderEntry& rhs);
 
 // std unique requires operator== to identify duplicates.
-bool operator==(const RegisteredCacheEntry& lhs, const RegisteredCacheEntry& rhs);
-bool operator!=(const RegisteredCacheEntry& lhs, const RegisteredCacheEntry& rhs);
+bool operator==(const ContentProviderEntry& lhs, const ContentProviderEntry& rhs);
+bool operator!=(const ContentProviderEntry& lhs, const ContentProviderEntry& rhs);
+
+class ContentProvider {
+public:
+    virtual ~ContentProvider();
+
+    virtual void Refresh() = 0;
+
+    virtual bool HasEntry(u64 title_id, ContentRecordType type) const = 0;
+    virtual bool HasEntry(ContentProviderEntry entry) const;
+
+    virtual std::optional<u32> GetEntryVersion(u64 title_id) const = 0;
+
+    virtual VirtualFile GetEntryUnparsed(u64 title_id, ContentRecordType type) const = 0;
+    virtual VirtualFile GetEntryUnparsed(ContentProviderEntry entry) const;
+
+    virtual VirtualFile GetEntryRaw(u64 title_id, ContentRecordType type) const = 0;
+    virtual VirtualFile GetEntryRaw(ContentProviderEntry entry) const;
+
+    virtual std::unique_ptr<NCA> GetEntry(u64 title_id, ContentRecordType type) const = 0;
+    virtual std::unique_ptr<NCA> GetEntry(ContentProviderEntry entry) const;
+
+    virtual std::vector<ContentProviderEntry> ListEntries() const;
+
+    // If a parameter is not std::nullopt, it will be filtered for from all entries.
+    virtual std::vector<ContentProviderEntry> ListEntriesFilter(
+        std::optional<TitleType> title_type = {}, std::optional<ContentRecordType> record_type = {},
+        std::optional<u64> title_id = {}) const = 0;
+
+protected:
+    // A single instance of KeyManager to be used by GetEntry()
+    Core::Crypto::KeyManager keys;
+};
 
 /*
  * A class that catalogues NCAs in the registered directory structure.
@@ -67,39 +102,32 @@ bool operator!=(const RegisteredCacheEntry& lhs, const RegisteredCacheEntry& rhs
  * (This impl also supports substituting the nca dir for an nca file, as that's more convenient
  * when 4GB splitting can be ignored.)
  */
-class RegisteredCache {
-    friend class RegisteredCacheUnion;
-
+class RegisteredCache : public ContentProvider {
 public:
     // Parsing function defines the conversion from raw file to NCA. If there are other steps
     // besides creating the NCA from the file (e.g. NAX0 on SD Card), that should go in a custom
     // parsing function.
     explicit RegisteredCache(VirtualDir dir,
-                             RegisteredCacheParsingFunction parsing_function =
+                             ContentProviderParsingFunction parsing_function =
                                  [](const VirtualFile& file, const NcaID& id) { return file; });
-    ~RegisteredCache();
+    ~RegisteredCache() override;
 
-    void Refresh();
+    void Refresh() override;
 
-    bool HasEntry(u64 title_id, ContentRecordType type) const;
-    bool HasEntry(RegisteredCacheEntry entry) const;
+    bool HasEntry(u64 title_id, ContentRecordType type) const override;
 
-    std::optional<u32> GetEntryVersion(u64 title_id) const;
+    std::optional<u32> GetEntryVersion(u64 title_id) const override;
 
-    VirtualFile GetEntryUnparsed(u64 title_id, ContentRecordType type) const;
-    VirtualFile GetEntryUnparsed(RegisteredCacheEntry entry) const;
+    VirtualFile GetEntryUnparsed(u64 title_id, ContentRecordType type) const override;
 
-    VirtualFile GetEntryRaw(u64 title_id, ContentRecordType type) const;
-    VirtualFile GetEntryRaw(RegisteredCacheEntry entry) const;
+    VirtualFile GetEntryRaw(u64 title_id, ContentRecordType type) const override;
 
-    std::unique_ptr<NCA> GetEntry(u64 title_id, ContentRecordType type) const;
-    std::unique_ptr<NCA> GetEntry(RegisteredCacheEntry entry) const;
+    std::unique_ptr<NCA> GetEntry(u64 title_id, ContentRecordType type) const override;
 
-    std::vector<RegisteredCacheEntry> ListEntries() const;
     // If a parameter is not std::nullopt, it will be filtered for from all entries.
-    std::vector<RegisteredCacheEntry> ListEntriesFilter(
+    std::vector<ContentProviderEntry> ListEntriesFilter(
         std::optional<TitleType> title_type = {}, std::optional<ContentRecordType> record_type = {},
-        std::optional<u64> title_id = {}) const;
+        std::optional<u64> title_id = {}) const override;
 
     // Raw copies all the ncas from the xci/nsp to the csache. Does some quick checks to make sure
     // there is a meta NCA and all of them are accessible.
@@ -131,46 +159,70 @@ private:
     bool RawInstallYuzuMeta(const CNMT& cnmt);
 
     VirtualDir dir;
-    RegisteredCacheParsingFunction parser;
-    Core::Crypto::KeyManager keys;
+    ContentProviderParsingFunction parser;
 
     // maps tid -> NcaID of meta
-    boost::container::flat_map<u64, NcaID> meta_id;
+    std::map<u64, NcaID> meta_id;
     // maps tid -> meta
-    boost::container::flat_map<u64, CNMT> meta;
+    std::map<u64, CNMT> meta;
     // maps tid -> meta for CNMT in yuzu_meta
-    boost::container::flat_map<u64, CNMT> yuzu_meta;
+    std::map<u64, CNMT> yuzu_meta;
 };
 
-// Combines multiple RegisteredCaches (i.e. SysNAND, UserNAND, SDMC) into one interface.
-class RegisteredCacheUnion {
-public:
-    explicit RegisteredCacheUnion(std::vector<RegisteredCache*> caches);
-
-    void Refresh();
-
-    bool HasEntry(u64 title_id, ContentRecordType type) const;
-    bool HasEntry(RegisteredCacheEntry entry) const;
-
-    std::optional<u32> GetEntryVersion(u64 title_id) const;
-
-    VirtualFile GetEntryUnparsed(u64 title_id, ContentRecordType type) const;
-    VirtualFile GetEntryUnparsed(RegisteredCacheEntry entry) const;
-
-    VirtualFile GetEntryRaw(u64 title_id, ContentRecordType type) const;
-    VirtualFile GetEntryRaw(RegisteredCacheEntry entry) const;
-
-    std::unique_ptr<NCA> GetEntry(u64 title_id, ContentRecordType type) const;
-    std::unique_ptr<NCA> GetEntry(RegisteredCacheEntry entry) const;
+enum class ContentProviderUnionSlot {
+    SysNAND,        ///< System NAND
+    UserNAND,       ///< User NAND
+    SDMC,           ///< SD Card
+    FrontendManual, ///< Frontend-defined game list or similar
+};
 
-    std::vector<RegisteredCacheEntry> ListEntries() const;
-    // If a parameter is not std::nullopt, it will be filtered for from all entries.
-    std::vector<RegisteredCacheEntry> ListEntriesFilter(
+// Combines multiple ContentProvider(s) (i.e. SysNAND, UserNAND, SDMC) into one interface.
+class ContentProviderUnion : public ContentProvider {
+public:
+    ~ContentProviderUnion() override;
+
+    void SetSlot(ContentProviderUnionSlot slot, ContentProvider* provider);
+    void ClearSlot(ContentProviderUnionSlot slot);
+
+    void Refresh() override;
+    bool HasEntry(u64 title_id, ContentRecordType type) const override;
+    std::optional<u32> GetEntryVersion(u64 title_id) const override;
+    VirtualFile GetEntryUnparsed(u64 title_id, ContentRecordType type) const override;
+    VirtualFile GetEntryRaw(u64 title_id, ContentRecordType type) const override;
+    std::unique_ptr<NCA> GetEntry(u64 title_id, ContentRecordType type) const override;
+    std::vector<ContentProviderEntry> ListEntriesFilter(
+        std::optional<TitleType> title_type, std::optional<ContentRecordType> record_type,
+        std::optional<u64> title_id) const override;
+
+    std::vector<std::pair<ContentProviderUnionSlot, ContentProviderEntry>> ListEntriesFilterOrigin(
+        std::optional<ContentProviderUnionSlot> origin = {},
         std::optional<TitleType> title_type = {}, std::optional<ContentRecordType> record_type = {},
         std::optional<u64> title_id = {}) const;
 
 private:
-    std::vector<RegisteredCache*> caches;
+    std::map<ContentProviderUnionSlot, ContentProvider*> providers;
+};
+
+class ManualContentProvider : public ContentProvider {
+public:
+    ~ManualContentProvider() override;
+
+    void AddEntry(TitleType title_type, ContentRecordType content_type, u64 title_id,
+                  VirtualFile file);
+    void ClearAllEntries();
+
+    void Refresh() override;
+    bool HasEntry(u64 title_id, ContentRecordType type) const override;
+    std::optional<u32> GetEntryVersion(u64 title_id) const override;
+    VirtualFile GetEntryUnparsed(u64 title_id, ContentRecordType type) const override;
+    VirtualFile GetEntryRaw(u64 title_id, ContentRecordType type) const override;
+    std::unique_ptr<NCA> GetEntry(u64 title_id, ContentRecordType type) const override;
+    std::vector<ContentProviderEntry> ListEntriesFilter(
+        std::optional<TitleType> title_type, std::optional<ContentRecordType> record_type,
+        std::optional<u64> title_id) const override;
+
+private:
+    std::map<std::tuple<TitleType, ContentRecordType, u64>, VirtualFile> entries;
 };
 
 } // namespace FileSys

+ 1 - 1
src/core/file_sys/romfs_factory.cpp

@@ -48,7 +48,7 @@ ResultVal<VirtualFile> RomFSFactory::Open(u64 title_id, StorageId storage, Conte
 
     switch (storage) {
     case StorageId::None:
-        res = Service::FileSystem::GetUnionContents().GetEntry(title_id, type);
+        res = Core::System::GetInstance().GetContentProvider().GetEntry(title_id, type);
         break;
     case StorageId::NandSystem:
         res = Service::FileSystem::GetSystemNANDContents()->GetEntry(title_id, type);

+ 7 - 6
src/core/file_sys/submission_package.cpp

@@ -143,11 +143,12 @@ std::multimap<u64, std::shared_ptr<NCA>> NSP::GetNCAsByTitleID() const {
     return out;
 }
 
-std::map<u64, std::map<ContentRecordType, std::shared_ptr<NCA>>> NSP::GetNCAs() const {
+std::map<u64, std::map<std::pair<TitleType, ContentRecordType>, std::shared_ptr<NCA>>>
+NSP::GetNCAs() const {
     return ncas;
 }
 
-std::shared_ptr<NCA> NSP::GetNCA(u64 title_id, ContentRecordType type) const {
+std::shared_ptr<NCA> NSP::GetNCA(u64 title_id, ContentRecordType type, TitleType title_type) const {
     if (extracted)
         LOG_WARNING(Service_FS, "called on an NSP that is of type extracted.");
 
@@ -155,14 +156,14 @@ std::shared_ptr<NCA> NSP::GetNCA(u64 title_id, ContentRecordType type) const {
     if (title_id_iter == ncas.end())
         return nullptr;
 
-    const auto type_iter = title_id_iter->second.find(type);
+    const auto type_iter = title_id_iter->second.find({title_type, type});
     if (type_iter == title_id_iter->second.end())
         return nullptr;
 
     return type_iter->second;
 }
 
-VirtualFile NSP::GetNCAFile(u64 title_id, ContentRecordType type) const {
+VirtualFile NSP::GetNCAFile(u64 title_id, ContentRecordType type, TitleType title_type) const {
     if (extracted)
         LOG_WARNING(Service_FS, "called on an NSP that is of type extracted.");
     const auto nca = GetNCA(title_id, type);
@@ -240,7 +241,7 @@ void NSP::ReadNCAs(const std::vector<VirtualFile>& files) {
             const CNMT cnmt(inner_file);
             auto& ncas_title = ncas[cnmt.GetTitleID()];
 
-            ncas_title[ContentRecordType::Meta] = nca;
+            ncas_title[{cnmt.GetType(), ContentRecordType::Meta}] = nca;
             for (const auto& rec : cnmt.GetContentRecords()) {
                 const auto id_string = Common::HexArrayToString(rec.nca_id, false);
                 const auto next_file = pfs->GetFile(fmt::format("{}.nca", id_string));
@@ -258,7 +259,7 @@ void NSP::ReadNCAs(const std::vector<VirtualFile>& files) {
                 if (next_nca->GetStatus() == Loader::ResultStatus::Success ||
                     (next_nca->GetStatus() == Loader::ResultStatus::ErrorMissingBKTRBaseRomFS &&
                      (cnmt.GetTitleID() & 0x800) != 0)) {
-                    ncas_title[rec.type] = std::move(next_nca);
+                    ncas_title[{cnmt.GetType(), rec.type}] = std::move(next_nca);
                 }
             }
 

+ 7 - 4
src/core/file_sys/submission_package.h

@@ -42,9 +42,12 @@ public:
     // Type 0 Only (Collection of NCAs + Certificate + Ticket + Meta XML)
     std::vector<std::shared_ptr<NCA>> GetNCAsCollapsed() const;
     std::multimap<u64, std::shared_ptr<NCA>> GetNCAsByTitleID() const;
-    std::map<u64, std::map<ContentRecordType, std::shared_ptr<NCA>>> GetNCAs() const;
-    std::shared_ptr<NCA> GetNCA(u64 title_id, ContentRecordType type) const;
-    VirtualFile GetNCAFile(u64 title_id, ContentRecordType type) const;
+    std::map<u64, std::map<std::pair<TitleType, ContentRecordType>, std::shared_ptr<NCA>>> GetNCAs()
+        const;
+    std::shared_ptr<NCA> GetNCA(u64 title_id, ContentRecordType type,
+                                TitleType title_type = TitleType::Application) const;
+    VirtualFile GetNCAFile(u64 title_id, ContentRecordType type,
+                           TitleType title_type = TitleType::Application) const;
     std::vector<Core::Crypto::Key128> GetTitlekey() const;
 
     std::vector<VirtualFile> GetFiles() const override;
@@ -67,7 +70,7 @@ private:
 
     std::shared_ptr<PartitionFilesystem> pfs;
     // Map title id -> {map type -> NCA}
-    std::map<u64, std::map<ContentRecordType, std::shared_ptr<NCA>>> ncas;
+    std::map<u64, std::map<std::pair<TitleType, ContentRecordType>, std::shared_ptr<NCA>>> ncas;
     std::vector<VirtualFile> ticket_files;
 
     Core::Crypto::KeyManager keys;

+ 1 - 1
src/core/hle/service/am/applets/web_browser.cpp

@@ -86,7 +86,7 @@ static FileSys::VirtualFile GetManualRomFS() {
     if (loader.ReadManualRomFS(out) == Loader::ResultStatus::Success)
         return out;
 
-    const auto& installed{FileSystem::GetUnionContents()};
+    const auto& installed{Core::System::GetInstance().GetContentProvider()};
     const auto res = installed.GetEntry(Core::System::GetInstance().CurrentProcess()->GetTitleID(),
                                         FileSys::ContentRecordType::Manual);
 

+ 2 - 2
src/core/hle/service/aoc/aoc_u.cpp

@@ -33,11 +33,11 @@ static bool CheckAOCTitleIDMatchesBase(u64 title_id, u64 base) {
 
 static std::vector<u64> AccumulateAOCTitleIDs() {
     std::vector<u64> add_on_content;
-    const auto rcu = FileSystem::GetUnionContents();
+    const auto& rcu = Core::System::GetInstance().GetContentProvider();
     const auto list =
         rcu.ListEntriesFilter(FileSys::TitleType::AOC, FileSys::ContentRecordType::Data);
     std::transform(list.begin(), list.end(), std::back_inserter(add_on_content),
-                   [](const FileSys::RegisteredCacheEntry& rce) { return rce.title_id; });
+                   [](const FileSys::ContentProviderEntry& rce) { return rce.title_id; });
     add_on_content.erase(
         std::remove_if(
             add_on_content.begin(), add_on_content.end(),

+ 6 - 5
src/core/hle/service/filesystem/filesystem.cpp

@@ -391,11 +391,6 @@ void WriteSaveDataSize(FileSys::SaveDataType type, u64 title_id, u128 user_id,
         save_data_factory->WriteSaveDataSize(type, title_id, user_id, new_value);
 }
 
-FileSys::RegisteredCacheUnion GetUnionContents() {
-    return FileSys::RegisteredCacheUnion{
-        {GetSystemNANDContents(), GetUserNANDContents(), GetSDMCContents()}};
-}
-
 FileSys::RegisteredCache* GetSystemNANDContents() {
     LOG_TRACE(Service_FS, "Opening System NAND Contents");
 
@@ -460,6 +455,10 @@ void CreateFactories(FileSys::VfsFilesystem& vfs, bool overwrite) {
     if (bis_factory == nullptr) {
         bis_factory =
             std::make_unique<FileSys::BISFactory>(nand_directory, load_directory, dump_directory);
+        Core::System::GetInstance().RegisterContentProvider(
+            FileSys::ContentProviderUnionSlot::SysNAND, bis_factory->GetSystemNANDContents());
+        Core::System::GetInstance().RegisterContentProvider(
+            FileSys::ContentProviderUnionSlot::UserNAND, bis_factory->GetUserNANDContents());
     }
 
     if (save_data_factory == nullptr) {
@@ -468,6 +467,8 @@ void CreateFactories(FileSys::VfsFilesystem& vfs, bool overwrite) {
 
     if (sdmc_factory == nullptr) {
         sdmc_factory = std::make_unique<FileSys::SDMCFactory>(std::move(sd_directory));
+        Core::System::GetInstance().RegisterContentProvider(FileSys::ContentProviderUnionSlot::SDMC,
+                                                            sdmc_factory->GetSDMCContents());
     }
 }
 

+ 0 - 2
src/core/hle/service/filesystem/filesystem.h

@@ -54,8 +54,6 @@ FileSys::SaveDataSize ReadSaveDataSize(FileSys::SaveDataType type, u64 title_id,
 void WriteSaveDataSize(FileSys::SaveDataType type, u64 title_id, u128 user_id,
                        FileSys::SaveDataSize new_value);
 
-FileSys::RegisteredCacheUnion GetUnionContents();
-
 FileSys::RegisteredCache* GetSystemNANDContents();
 FileSys::RegisteredCache* GetUserNANDContents();
 FileSys::RegisteredCache* GetSDMCContents();

+ 4 - 2
src/core/loader/nso.cpp

@@ -21,6 +21,8 @@
 #include "core/memory.h"
 #include "core/settings.h"
 
+#pragma optimize("", off)
+
 namespace Loader {
 namespace {
 struct MODHeader {
@@ -136,13 +138,13 @@ std::optional<VAddr> AppLoader_NSO::LoadModule(Kernel::Process& process,
 
     // Apply patches if necessary
     if (pm && (pm->HasNSOPatch(nso_header.build_id) || Settings::values.dump_nso)) {
-        std::vector<u8> pi_header(sizeof(NSOHeader) + program_image.size());
+        std::vector<u8> pi_header;
         pi_header.insert(pi_header.begin(), reinterpret_cast<u8*>(&nso_header),
                          reinterpret_cast<u8*>(&nso_header) + sizeof(NSOHeader));
         pi_header.insert(pi_header.begin() + sizeof(NSOHeader), program_image.begin(),
                          program_image.end());
 
-        pi_header = pm->PatchNSO(pi_header);
+        pi_header = pm->PatchNSO(pi_header, file.GetName());
 
         std::copy(pi_header.begin() + sizeof(NSOHeader), pi_header.end(), program_image.begin());
     }

+ 6 - 3
src/yuzu/game_list.cpp

@@ -18,6 +18,7 @@
 #include "common/common_types.h"
 #include "common/logging/log.h"
 #include "core/file_sys/patch_manager.h"
+#include "core/file_sys/registered_cache.h"
 #include "yuzu/compatibility_list.h"
 #include "yuzu/game_list.h"
 #include "yuzu/game_list_p.h"
@@ -193,8 +194,9 @@ void GameList::onFilterCloseClicked() {
     main_window->filterBarSetChecked(false);
 }
 
-GameList::GameList(FileSys::VirtualFilesystem vfs, GMainWindow* parent)
-    : QWidget{parent}, vfs(std::move(vfs)) {
+GameList::GameList(FileSys::VirtualFilesystem vfs, FileSys::ManualContentProvider* provider,
+                   GMainWindow* parent)
+    : QWidget{parent}, vfs(std::move(vfs)), provider(provider) {
     watcher = new QFileSystemWatcher(this);
     connect(watcher, &QFileSystemWatcher::directoryChanged, this, &GameList::RefreshGameDirectory);
 
@@ -432,7 +434,8 @@ void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) {
 
     emit ShouldCancelWorker();
 
-    GameListWorker* worker = new GameListWorker(vfs, dir_path, deep_scan, compatibility_list);
+    GameListWorker* worker =
+        new GameListWorker(vfs, provider, dir_path, deep_scan, compatibility_list);
 
     connect(worker, &GameListWorker::EntryReady, this, &GameList::AddEntry, Qt::QueuedConnection);
     connect(worker, &GameListWorker::Finished, this, &GameList::DonePopulating,

+ 5 - 2
src/yuzu/game_list.h

@@ -26,8 +26,9 @@ class GameListSearchField;
 class GMainWindow;
 
 namespace FileSys {
+class ManualContentProvider;
 class VfsFilesystem;
-}
+} // namespace FileSys
 
 enum class GameListOpenTarget {
     SaveData,
@@ -47,7 +48,8 @@ public:
         COLUMN_COUNT, // Number of columns
     };
 
-    explicit GameList(std::shared_ptr<FileSys::VfsFilesystem> vfs, GMainWindow* parent = nullptr);
+    explicit GameList(std::shared_ptr<FileSys::VfsFilesystem> vfs,
+                      FileSys::ManualContentProvider* provider, GMainWindow* parent = nullptr);
     ~GameList() override;
 
     void clearFilter();
@@ -86,6 +88,7 @@ private:
     void RefreshGameDirectory();
 
     std::shared_ptr<FileSys::VfsFilesystem> vfs;
+    FileSys::ManualContentProvider* provider;
     GameListSearchField* search_field;
     GMainWindow* main_window = nullptr;
     QVBoxLayout* layout = nullptr;

+ 57 - 68
src/yuzu/game_list_worker.cpp

@@ -12,12 +12,15 @@
 
 #include "common/common_paths.h"
 #include "common/file_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/mode.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/hle/service/filesystem/filesystem.h"
 #include "core/loader/loader.h"
 #include "yuzu/compatibility_list.h"
@@ -119,20 +122,25 @@ QList<QStandardItem*> MakeGameListEntry(const std::string& path, const std::stri
 }
 } // Anonymous namespace
 
-GameListWorker::GameListWorker(FileSys::VirtualFilesystem vfs, QString dir_path, bool deep_scan,
-                               const CompatibilityList& compatibility_list)
-    : vfs(std::move(vfs)), dir_path(std::move(dir_path)), deep_scan(deep_scan),
+GameListWorker::GameListWorker(FileSys::VirtualFilesystem vfs,
+                               FileSys::ManualContentProvider* provider, QString dir_path,
+                               bool deep_scan, const CompatibilityList& compatibility_list)
+    : vfs(std::move(vfs)), provider(provider), dir_path(std::move(dir_path)), deep_scan(deep_scan),
       compatibility_list(compatibility_list) {}
 
 GameListWorker::~GameListWorker() = default;
 
-void GameListWorker::AddInstalledTitlesToGameList() {
-    const auto cache = Service::FileSystem::GetUnionContents();
-    const auto installed_games = cache.ListEntriesFilter(FileSys::TitleType::Application,
-                                                         FileSys::ContentRecordType::Program);
+void GameListWorker::AddTitlesToGameList() {
+    const auto& cache = dynamic_cast<FileSys::ContentProviderUnion&>(
+        Core::System::GetInstance().GetContentProvider());
+    const auto installed_games = cache.ListEntriesFilterOrigin(
+        std::nullopt, FileSys::TitleType::Application, FileSys::ContentRecordType::Program);
 
-    for (const auto& game : installed_games) {
-        const auto file = cache.GetEntryUnparsed(game);
+    for (const auto& [slot, game] : installed_games) {
+        if (slot == FileSys::ContentProviderUnionSlot::FrontendManual)
+            continue;
+
+        const auto file = cache.GetEntryUnparsed(game.title_id, game.type);
         std::unique_ptr<Loader::AppLoader> loader = Loader::GetLoader(file);
         if (!loader)
             continue;
@@ -150,45 +158,13 @@ void GameListWorker::AddInstalledTitlesToGameList() {
         emit EntryReady(MakeGameListEntry(file->GetFullPath(), name, icon, *loader, program_id,
                                           compatibility_list, patch));
     }
-
-    const auto control_data = cache.ListEntriesFilter(FileSys::TitleType::Application,
-                                                      FileSys::ContentRecordType::Control);
-
-    for (const auto& entry : control_data) {
-        auto nca = cache.GetEntry(entry);
-        if (nca != nullptr) {
-            nca_control_map.insert_or_assign(entry.title_id, std::move(nca));
-        }
-    }
 }
 
-void GameListWorker::FillControlMap(const std::string& dir_path) {
-    const auto nca_control_callback = [this](u64* num_entries_out, const std::string& directory,
-                                             const std::string& virtual_name) -> bool {
-        if (stop_processing) {
-            // Breaks the callback loop
-            return false;
-        }
-
-        const std::string physical_name = directory + DIR_SEP + virtual_name;
-        const QFileInfo file_info(QString::fromStdString(physical_name));
-        if (!file_info.isDir() && file_info.suffix() == QStringLiteral("nca")) {
-            auto nca =
-                std::make_unique<FileSys::NCA>(vfs->OpenFile(physical_name, FileSys::Mode::Read));
-            if (nca->GetType() == FileSys::NCAContentType::Control) {
-                const u64 title_id = nca->GetTitleId();
-                nca_control_map.insert_or_assign(title_id, std::move(nca));
-            }
-        }
-        return true;
-    };
-
-    FileUtil::ForeachDirectoryEntry(nullptr, dir_path, nca_control_callback);
-}
-
-void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion) {
-    const auto callback = [this, recursion](u64* num_entries_out, const std::string& directory,
-                                            const std::string& virtual_name) -> bool {
+void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_path,
+                                    unsigned int recursion) {
+    const auto callback = [this, target, recursion](u64* num_entries_out,
+                                                    const std::string& directory,
+                                                    const std::string& virtual_name) -> bool {
         if (stop_processing) {
             // Breaks the callback loop.
             return false;
@@ -198,7 +174,8 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign
         const bool is_dir = FileUtil::IsDirectory(physical_name);
         if (!is_dir &&
             (HasSupportedFileExtension(physical_name) || IsExtractedNCAMain(physical_name))) {
-            auto loader = Loader::GetLoader(vfs->OpenFile(physical_name, FileSys::Mode::Read));
+            const auto file = vfs->OpenFile(physical_name, FileSys::Mode::Read);
+            auto loader = Loader::GetLoader(file);
             if (!loader) {
                 return true;
             }
@@ -209,31 +186,42 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign
                 return true;
             }
 
-            std::vector<u8> icon;
-            const auto res1 = loader->ReadIcon(icon);
-
             u64 program_id = 0;
             const auto res2 = loader->ReadProgramId(program_id);
 
-            std::string name = " ";
-            const auto res3 = loader->ReadTitle(name);
+            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<u8> icon;
+                const auto res1 = loader->ReadIcon(icon);
 
-            const FileSys::PatchManager patch{program_id};
+                std::string name = " ";
+                const auto res3 = loader->ReadTitle(name);
 
-            if (res1 != Loader::ResultStatus::Success && res3 != Loader::ResultStatus::Success &&
-                res2 == Loader::ResultStatus::Success) {
-                // Use from metadata pool.
-                if (nca_control_map.find(program_id) != nca_control_map.end()) {
-                    const auto& nca = nca_control_map[program_id];
-                    GetMetadataFromControlNCA(patch, *nca, icon, name);
-                }
-            }
+                const FileSys::PatchManager patch{program_id};
 
-            emit EntryReady(MakeGameListEntry(physical_name, name, icon, *loader, program_id,
-                                              compatibility_list, patch));
+                emit EntryReady(MakeGameListEntry(physical_name, name, icon, *loader, program_id,
+                                                  compatibility_list, patch));
+            }
         } else if (is_dir && recursion > 0) {
             watch_list.append(QString::fromStdString(physical_name));
-            AddFstEntriesToGameList(physical_name, recursion - 1);
+            ScanFileSystem(target, physical_name, recursion - 1);
         }
 
         return true;
@@ -245,10 +233,11 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign
 void GameListWorker::run() {
     stop_processing = false;
     watch_list.append(dir_path);
-    FillControlMap(dir_path.toStdString());
-    AddInstalledTitlesToGameList();
-    AddFstEntriesToGameList(dir_path.toStdString(), deep_scan ? 256 : 0);
-    nca_control_map.clear();
+    provider->ClearAllEntries();
+    ScanFileSystem(ScanTarget::FillManualContentProvider, dir_path.toStdString(),
+                   deep_scan ? 256 : 0);
+    AddTitlesToGameList();
+    ScanFileSystem(ScanTarget::PopulateGameList, dir_path.toStdString(), deep_scan ? 256 : 0);
     emit Finished(watch_list);
 }
 

+ 11 - 5
src/yuzu/game_list_worker.h

@@ -33,7 +33,8 @@ class GameListWorker : public QObject, public QRunnable {
     Q_OBJECT
 
 public:
-    GameListWorker(std::shared_ptr<FileSys::VfsFilesystem> vfs, QString dir_path, bool deep_scan,
+    GameListWorker(std::shared_ptr<FileSys::VfsFilesystem> vfs,
+                   FileSys::ManualContentProvider* provider, QString dir_path, bool deep_scan,
                    const CompatibilityList& compatibility_list);
     ~GameListWorker() override;
 
@@ -58,12 +59,17 @@ signals:
     void Finished(QStringList watch_list);
 
 private:
-    void AddInstalledTitlesToGameList();
-    void FillControlMap(const std::string& dir_path);
-    void AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion = 0);
+    void AddTitlesToGameList();
+
+    enum class ScanTarget {
+        FillManualContentProvider,
+        PopulateGameList,
+    };
+
+    void ScanFileSystem(ScanTarget target, const std::string& dir_path, unsigned int recursion = 0);
 
     std::shared_ptr<FileSys::VfsFilesystem> vfs;
-    std::map<u64, std::unique_ptr<FileSys::NCA>> nca_control_map;
+    FileSys::ManualContentProvider* provider;
     QStringList watch_list;
     QString dir_path;
     bool deep_scan;

+ 14 - 9
src/yuzu/main.cpp

@@ -171,7 +171,8 @@ static void InitializeLogging() {
 
 GMainWindow::GMainWindow()
     : config(new Config()), emu_thread(nullptr),
-      vfs(std::make_shared<FileSys::RealVfsFilesystem>()) {
+      vfs(std::make_shared<FileSys::RealVfsFilesystem>()),
+      provider(std::make_unique<FileSys::ManualContentProvider>()) {
     InitializeLogging();
 
     debug_context = Tegra::DebugContext::Construct();
@@ -203,11 +204,15 @@ GMainWindow::GMainWindow()
                        .arg(Common::g_build_fullname, Common::g_scm_branch, Common::g_scm_desc));
     show();
 
+    Core::System::GetInstance().SetContentProvider(
+        std::make_unique<FileSys::ContentProviderUnion>());
+    Core::System::GetInstance().RegisterContentProvider(
+        FileSys::ContentProviderUnionSlot::FrontendManual, provider.get());
+    Service::FileSystem::CreateFactories(*vfs);
+
     // Gen keys if necessary
     OnReinitializeKeys(ReinitializeKeyBehavior::NoWarning);
 
-    // Necessary to load titles from nand in gamelist.
-    Service::FileSystem::CreateFactories(*vfs);
     game_list->LoadCompatibilityList();
     game_list->PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan);
 
@@ -419,7 +424,7 @@ void GMainWindow::InitializeWidgets() {
     render_window = new GRenderWindow(this, emu_thread.get());
     render_window->hide();
 
-    game_list = new GameList(vfs, this);
+    game_list = new GameList(vfs, provider.get(), this);
     ui.horizontalLayout->addWidget(game_list);
 
     loading_screen = new LoadingScreen(this);
@@ -1179,7 +1184,7 @@ void GMainWindow::OnGameListDumpRomFS(u64 program_id, const std::string& game_pa
         return;
     }
 
-    const auto installed = Service::FileSystem::GetUnionContents();
+    const auto& installed = Core::System::GetInstance().GetContentProvider();
     const auto romfs_title_id = SelectRomFSDumpTarget(installed, program_id);
 
     if (!romfs_title_id) {
@@ -1925,14 +1930,14 @@ void GMainWindow::OnReinitializeKeys(ReinitializeKeyBehavior behavior) {
     }
 }
 
-std::optional<u64> GMainWindow::SelectRomFSDumpTarget(
-    const FileSys::RegisteredCacheUnion& installed, u64 program_id) {
+std::optional<u64> GMainWindow::SelectRomFSDumpTarget(const FileSys::ContentProvider& installed,
+                                                      u64 program_id) {
     const auto dlc_entries =
         installed.ListEntriesFilter(FileSys::TitleType::AOC, FileSys::ContentRecordType::Data);
-    std::vector<FileSys::RegisteredCacheEntry> dlc_match;
+    std::vector<FileSys::ContentProviderEntry> dlc_match;
     dlc_match.reserve(dlc_entries.size());
     std::copy_if(dlc_entries.begin(), dlc_entries.end(), std::back_inserter(dlc_match),
-                 [&program_id, &installed](const FileSys::RegisteredCacheEntry& entry) {
+                 [&program_id, &installed](const FileSys::ContentProviderEntry& entry) {
                      return (entry.title_id & DLC_BASE_TITLE_ID_MASK) == program_id &&
                             installed.GetEntry(entry)->GetStatus() == Loader::ResultStatus::Success;
                  });

+ 4 - 2
src/yuzu/main.h

@@ -37,7 +37,8 @@ struct SoftwareKeyboardParameters;
 } // namespace Core::Frontend
 
 namespace FileSys {
-class RegisteredCacheUnion;
+class ContentProvider;
+class ManualContentProvider;
 class VfsFilesystem;
 } // namespace FileSys
 
@@ -205,7 +206,7 @@ private slots:
     void OnReinitializeKeys(ReinitializeKeyBehavior behavior);
 
 private:
-    std::optional<u64> SelectRomFSDumpTarget(const FileSys::RegisteredCacheUnion&, u64 program_id);
+    std::optional<u64> SelectRomFSDumpTarget(const FileSys::ContentProvider&, u64 program_id);
     void UpdateStatusBar();
 
     Ui::MainWindow ui;
@@ -233,6 +234,7 @@ private:
 
     // FS
     std::shared_ptr<FileSys::VfsFilesystem> vfs;
+    std::unique_ptr<FileSys::ManualContentProvider> provider;
 
     // Debugger panes
     ProfilerWidget* profilerWidget;

+ 2 - 0
src/yuzu_cmd/yuzu.cpp

@@ -33,6 +33,7 @@
 #include "yuzu_cmd/emu_window/emu_window_sdl2.h"
 
 #include <getopt.h>
+#include "core/file_sys/registered_cache.h"
 #ifndef _MSC_VER
 #include <unistd.h>
 #endif
@@ -178,6 +179,7 @@ int main(int argc, char** argv) {
     }
 
     Core::System& system{Core::System::GetInstance()};
+    system.SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>());
     system.SetFilesystem(std::make_shared<FileSys::RealVfsFilesystem>());
     Service::FileSystem::CreateFactories(*system.GetFilesystem());