diff --git a/src/core/file_sys/registered_cache.cpp b/src/core/file_sys/registered_cache.cpp
index cf6f774010..39c0710e1b 100644
--- a/src/core/file_sys/registered_cache.cpp
+++ b/src/core/file_sys/registered_cache.cpp
@@ -280,6 +280,14 @@ VirtualFile RegisteredCache::GetEntryUnparsed(RegisteredCacheEntry entry) const
     return GetEntryUnparsed(entry.title_id, entry.type);
 }
 
+boost::optional<u32> RegisteredCache::GetEntryVersion(u64 title_id) const {
+    if (meta.find(title_id) != meta.end())
+        return meta.at(title_id).GetTitleVersion();
+    if (yuzu_meta.find(title_id) != yuzu_meta.end())
+        return yuzu_meta.at(title_id).GetTitleVersion();
+    return boost::none;
+}
+
 VirtualFile RegisteredCache::GetEntryRaw(u64 title_id, ContentRecordType type) const {
     const auto id = GetNcaIDFromMetadata(title_id, type);
     if (id == boost::none)
@@ -498,4 +506,110 @@ bool RegisteredCache::RawInstallYuzuMeta(const CNMT& cnmt) {
                                    kv.second.GetTitleID() == cnmt.GetTitleID();
                         }) != yuzu_meta.end();
 }
+
+RegisteredCacheUnion::RegisteredCacheUnion(std::vector<std::shared_ptr<RegisteredCache>> caches)
+    : caches(std::move(caches)) {}
+
+void RegisteredCacheUnion::Refresh() {
+    for (const auto& c : caches)
+        c->Refresh();
+}
+
+bool RegisteredCacheUnion::HasEntry(u64 title_id, ContentRecordType type) const {
+    for (const auto& c : caches) {
+        if (c->HasEntry(title_id, type))
+            return true;
+    }
+
+    return false;
+}
+
+bool RegisteredCacheUnion::HasEntry(RegisteredCacheEntry entry) const {
+    return HasEntry(entry.title_id, entry.type);
+}
+
+boost::optional<u32> RegisteredCacheUnion::GetEntryVersion(u64 title_id) const {
+    for (const auto& c : caches) {
+        const auto res = c->GetEntryVersion(title_id);
+        if (res != boost::none)
+            return res;
+    }
+
+    return boost::none;
+}
+
+VirtualFile RegisteredCacheUnion::GetEntryUnparsed(u64 title_id, ContentRecordType type) const {
+    for (const auto& c : caches) {
+        const auto res = c->GetEntryUnparsed(title_id, type);
+        if (res != nullptr)
+            return res;
+    }
+
+    return nullptr;
+}
+
+VirtualFile RegisteredCacheUnion::GetEntryUnparsed(RegisteredCacheEntry entry) const {
+    return GetEntryUnparsed(entry.title_id, entry.type);
+}
+
+VirtualFile RegisteredCacheUnion::GetEntryRaw(u64 title_id, ContentRecordType type) const {
+    for (const auto& c : caches) {
+        const auto res = c->GetEntryRaw(title_id, type);
+        if (res != nullptr)
+            return res;
+    }
+
+    return nullptr;
+}
+
+VirtualFile RegisteredCacheUnion::GetEntryRaw(RegisteredCacheEntry entry) const {
+    return GetEntryRaw(entry.title_id, entry.type);
+}
+
+std::shared_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_shared<NCA>(raw);
+}
+
+std::shared_ptr<NCA> RegisteredCacheUnion::GetEntry(RegisteredCacheEntry entry) const {
+    return GetEntry(entry.title_id, entry.type);
+}
+
+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; });
+    }
+    return out;
+}
+
+std::vector<RegisteredCacheEntry> RegisteredCacheUnion::ListEntriesFilter(
+    boost::optional<TitleType> title_type, boost::optional<ContentRecordType> record_type,
+    boost::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 != boost::none && title_type.get() != c.GetType())
+                    return false;
+                if (record_type != boost::none && record_type.get() != r.type)
+                    return false;
+                if (title_id != boost::none && title_id.get() != c.GetTitleID())
+                    return false;
+                return true;
+            });
+    }
+    return out;
+}
 } // namespace FileSys
diff --git a/src/core/file_sys/registered_cache.h b/src/core/file_sys/registered_cache.h
index 467ceeef13..dcce3fd169 100644
--- a/src/core/file_sys/registered_cache.h
+++ b/src/core/file_sys/registered_cache.h
@@ -43,6 +43,10 @@ struct RegisteredCacheEntry {
     std::string DebugInfo() const;
 };
 
+constexpr inline u64 GetUpdateTitleID(u64 base_title_id) {
+    return base_title_id | 0x800;
+}
+
 // boost flat_map requires operator< for O(log(n)) lookups.
 bool operator<(const RegisteredCacheEntry& lhs, const RegisteredCacheEntry& rhs);
 
@@ -60,6 +64,8 @@ bool operator<(const RegisteredCacheEntry& lhs, const RegisteredCacheEntry& rhs)
  * 4GB splitting can be ignored.)
  */
 class RegisteredCache {
+    friend class RegisteredCacheUnion;
+
 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
@@ -74,6 +80,8 @@ public:
     bool HasEntry(u64 title_id, ContentRecordType type) const;
     bool HasEntry(RegisteredCacheEntry entry) const;
 
+    boost::optional<u32> GetEntryVersion(u64 title_id) const;
+
     VirtualFile GetEntryUnparsed(u64 title_id, ContentRecordType type) const;
     VirtualFile GetEntryUnparsed(RegisteredCacheEntry entry) const;
 
@@ -131,4 +139,36 @@ private:
     boost::container::flat_map<u64, CNMT> yuzu_meta;
 };
 
+// Combines multiple RegisteredCaches (i.e. SysNAND, UserNAND, SDMC) into one interface.
+class RegisteredCacheUnion {
+public:
+    explicit RegisteredCacheUnion(std::vector<std::shared_ptr<RegisteredCache>> caches);
+
+    void Refresh();
+
+    bool HasEntry(u64 title_id, ContentRecordType type) const;
+    bool HasEntry(RegisteredCacheEntry entry) const;
+
+    boost::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::shared_ptr<NCA> GetEntry(u64 title_id, ContentRecordType type) const;
+    std::shared_ptr<NCA> GetEntry(RegisteredCacheEntry entry) const;
+
+    std::vector<RegisteredCacheEntry> ListEntries() const;
+    // If a parameter is not boost::none, it will be filtered for from all entries.
+    std::vector<RegisteredCacheEntry> ListEntriesFilter(
+        boost::optional<TitleType> title_type = boost::none,
+        boost::optional<ContentRecordType> record_type = boost::none,
+        boost::optional<u64> title_id = boost::none) const;
+
+private:
+    std::vector<std::shared_ptr<RegisteredCache>> caches;
+};
+
 } // namespace FileSys
diff --git a/src/core/hle/service/filesystem/filesystem.cpp b/src/core/hle/service/filesystem/filesystem.cpp
index a4426af968..e9d5bd7748 100644
--- a/src/core/hle/service/filesystem/filesystem.cpp
+++ b/src/core/hle/service/filesystem/filesystem.cpp
@@ -19,6 +19,7 @@
 #include "core/hle/service/filesystem/fsp_ldr.h"
 #include "core/hle/service/filesystem/fsp_pr.h"
 #include "core/hle/service/filesystem/fsp_srv.h"
+#include "filesystem.h"
 
 namespace Service::FileSystem {
 
@@ -307,6 +308,12 @@ ResultVal<FileSys::VirtualDir> OpenSDMC() {
     return sdmc_factory->Open();
 }
 
+std::shared_ptr<FileSys::RegisteredCacheUnion> GetUnionContents() {
+    return std::make_shared<FileSys::RegisteredCacheUnion>(
+        std::vector<std::shared_ptr<FileSys::RegisteredCache>>{
+            GetSystemNANDContents(), GetUserNANDContents(), GetSDMCContents()});
+}
+
 std::shared_ptr<FileSys::RegisteredCache> GetSystemNANDContents() {
     LOG_TRACE(Service_FS, "Opening System NAND Contents");
 
diff --git a/src/core/hle/service/filesystem/filesystem.h b/src/core/hle/service/filesystem/filesystem.h
index 9ba0e2eab4..793a7b06fc 100644
--- a/src/core/hle/service/filesystem/filesystem.h
+++ b/src/core/hle/service/filesystem/filesystem.h
@@ -13,6 +13,7 @@
 namespace FileSys {
 class BISFactory;
 class RegisteredCache;
+class RegisteredCacheUnion;
 class RomFSFactory;
 class SaveDataFactory;
 class SDMCFactory;
@@ -45,6 +46,8 @@ ResultVal<FileSys::VirtualDir> OpenSaveData(FileSys::SaveDataSpaceId space,
                                             FileSys::SaveDataDescriptor save_struct);
 ResultVal<FileSys::VirtualDir> OpenSDMC();
 
+std::shared_ptr<FileSys::RegisteredCacheUnion> GetUnionContents();
+
 std::shared_ptr<FileSys::RegisteredCache> GetSystemNANDContents();
 std::shared_ptr<FileSys::RegisteredCache> GetUserNANDContents();
 std::shared_ptr<FileSys::RegisteredCache> GetSDMCContents();