diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index 54afa6a87d1b21b73ce16a279acf7255805984ba..7ddc8753976ef18a8612cca58f46a0ea065d0aec 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -35,8 +35,12 @@ add_library(core STATIC
     file_sys/mode.h
     file_sys/nca_metadata.cpp
     file_sys/nca_metadata.h
+    file_sys/nca_patch.cpp
+    file_sys/nca_patch.h
     file_sys/partition_filesystem.cpp
     file_sys/partition_filesystem.h
+    file_sys/patch_manager.cpp
+    file_sys/patch_manager.h
     file_sys/program_metadata.cpp
     file_sys/program_metadata.h
     file_sys/registered_cache.cpp
diff --git a/src/core/crypto/aes_util.cpp b/src/core/crypto/aes_util.cpp
index 72e4bed67fe818dca00abd69d08b9a934739a6ed..89ade5000db5a5aa2c07c47f591fa6e134245474 100644
--- a/src/core/crypto/aes_util.cpp
+++ b/src/core/crypto/aes_util.cpp
@@ -82,11 +82,25 @@ void AESCipher<Key, KeySize>::Transcode(const u8* src, size_t size, u8* dest, Op
         }
     } else {
         const auto block_size = mbedtls_cipher_get_block_size(context);
+        if (size < block_size) {
+            std::vector<u8> block(block_size);
+            std::memcpy(block.data(), src, size);
+            Transcode(block.data(), block.size(), block.data(), op);
+            std::memcpy(dest, block.data(), size);
+            return;
+        }
 
         for (size_t offset = 0; offset < size; offset += block_size) {
             auto length = std::min<size_t>(block_size, size - offset);
             mbedtls_cipher_update(context, src + offset, length, dest + offset, &written);
             if (written != length) {
+                if (length < block_size) {
+                    std::vector<u8> block(block_size);
+                    std::memcpy(block.data(), src + offset, length);
+                    Transcode(block.data(), block.size(), block.data(), op);
+                    std::memcpy(dest + offset, block.data(), length);
+                    return;
+                }
                 LOG_WARNING(Crypto, "Not all data was decrypted requested={:016X}, actual={:016X}.",
                             length, written);
             }
diff --git a/src/core/crypto/ctr_encryption_layer.cpp b/src/core/crypto/ctr_encryption_layer.cpp
index 3ea60dbd0da9b501e655fe892094390243478740..296fad419eab3bb23c44b2c665db4343f02f8f12 100644
--- a/src/core/crypto/ctr_encryption_layer.cpp
+++ b/src/core/crypto/ctr_encryption_layer.cpp
@@ -21,7 +21,7 @@ size_t CTREncryptionLayer::Read(u8* data, size_t length, size_t offset) const {
         UpdateIV(base_offset + offset);
         std::vector<u8> raw = base->ReadBytes(length, offset);
         cipher.Transcode(raw.data(), raw.size(), data, Op::Decrypt);
-        return raw.size();
+        return length;
     }
 
     // offset does not fall on block boundary (0x10)
diff --git a/src/core/file_sys/card_image.cpp b/src/core/file_sys/card_image.cpp
index 1bd3353e40a0837a2dbf8e2463594f556f54ffc5..8218893b2cdbfc91d8e4b2a7b8de50ada53497d4 100644
--- a/src/core/file_sys/card_image.cpp
+++ b/src/core/file_sys/card_image.cpp
@@ -52,11 +52,11 @@ XCI::XCI(VirtualFile file_) : file(std::move(file_)), partitions(0x4) {
     const auto secure_ncas = secure_partition->GetNCAsCollapsed();
     std::copy(secure_ncas.begin(), secure_ncas.end(), std::back_inserter(ncas));
 
-    program_nca_status = Loader::ResultStatus::ErrorXCIMissingProgramNCA;
     program =
         secure_partition->GetNCA(secure_partition->GetProgramTitleID(), ContentRecordType::Program);
-    if (program != nullptr)
-        program_nca_status = program->GetStatus();
+    program_nca_status = secure_partition->GetProgramStatus(secure_partition->GetProgramTitleID());
+    if (program_nca_status == Loader::ResultStatus::ErrorNSPMissingProgramNCA)
+        program_nca_status = Loader::ResultStatus::ErrorXCIMissingProgramNCA;
 
     auto result = AddNCAFromPartition(XCIPartition::Update);
     if (result != Loader::ResultStatus::Success) {
diff --git a/src/core/file_sys/content_archive.cpp b/src/core/file_sys/content_archive.cpp
index 7cfb6f36b6e7192ed28552d40738f54e5a228b31..79bfb6fecf22dc55ce70b8146741070f04be2914 100644
--- a/src/core/file_sys/content_archive.cpp
+++ b/src/core/file_sys/content_archive.cpp
@@ -12,6 +12,7 @@
 #include "core/crypto/aes_util.h"
 #include "core/crypto/ctr_encryption_layer.h"
 #include "core/file_sys/content_archive.h"
+#include "core/file_sys/nca_patch.h"
 #include "core/file_sys/partition_filesystem.h"
 #include "core/file_sys/romfs.h"
 #include "core/file_sys/vfs_offset.h"
@@ -68,10 +69,31 @@ struct RomFSSuperblock {
 };
 static_assert(sizeof(RomFSSuperblock) == 0x200, "RomFSSuperblock has incorrect size.");
 
+struct BKTRHeader {
+    u64_le offset;
+    u64_le size;
+    u32_le magic;
+    INSERT_PADDING_BYTES(0x4);
+    u32_le number_entries;
+    INSERT_PADDING_BYTES(0x4);
+};
+static_assert(sizeof(BKTRHeader) == 0x20, "BKTRHeader has incorrect size.");
+
+struct BKTRSuperblock {
+    NCASectionHeaderBlock header_block;
+    IVFCHeader ivfc;
+    INSERT_PADDING_BYTES(0x18);
+    BKTRHeader relocation;
+    BKTRHeader subsection;
+    INSERT_PADDING_BYTES(0xC0);
+};
+static_assert(sizeof(BKTRSuperblock) == 0x200, "BKTRSuperblock has incorrect size.");
+
 union NCASectionHeader {
     NCASectionRaw raw;
     PFS0Superblock pfs0;
     RomFSSuperblock romfs;
+    BKTRSuperblock bktr;
 };
 static_assert(sizeof(NCASectionHeader) == 0x200, "NCASectionHeader has incorrect size.");
 
@@ -104,7 +126,7 @@ boost::optional<Core::Crypto::Key128> NCA::GetKeyAreaKey(NCASectionCryptoType ty
     Core::Crypto::Key128 out;
     if (type == NCASectionCryptoType::XTS)
         std::copy(key_area.begin(), key_area.begin() + 0x10, out.begin());
-    else if (type == NCASectionCryptoType::CTR)
+    else if (type == NCASectionCryptoType::CTR || type == NCASectionCryptoType::BKTR)
         std::copy(key_area.begin() + 0x20, key_area.begin() + 0x30, out.begin());
     else
         LOG_CRITICAL(Crypto, "Called GetKeyAreaKey on invalid NCASectionCryptoType type={:02X}",
@@ -154,6 +176,9 @@ VirtualFile NCA::Decrypt(NCASectionHeader s_header, VirtualFile in, u64 starting
         LOG_DEBUG(Crypto, "called with mode=NONE");
         return in;
     case NCASectionCryptoType::CTR:
+    // During normal BKTR decryption, this entire function is skipped. This is for the metadata,
+    // which uses the same CTR as usual.
+    case NCASectionCryptoType::BKTR:
         LOG_DEBUG(Crypto, "called with mode=CTR, starting_offset={:016X}", starting_offset);
         {
             boost::optional<Core::Crypto::Key128> key = boost::none;
@@ -190,7 +215,9 @@ VirtualFile NCA::Decrypt(NCASectionHeader s_header, VirtualFile in, u64 starting
     }
 }
 
-NCA::NCA(VirtualFile file_) : file(std::move(file_)) {
+NCA::NCA(VirtualFile file_, VirtualFile bktr_base_romfs_, u64 bktr_base_ivfc_offset)
+    : file(std::move(file_)),
+      bktr_base_romfs(bktr_base_romfs_ ? std::move(bktr_base_romfs_) : nullptr) {
     status = Loader::ResultStatus::Success;
 
     if (file == nullptr) {
@@ -265,22 +292,21 @@ NCA::NCA(VirtualFile file_) : file(std::move(file_)) {
     is_update = std::find_if(sections.begin(), sections.end(), [](const NCASectionHeader& header) {
                     return header.raw.header.crypto_type == NCASectionCryptoType::BKTR;
                 }) != sections.end();
+    ivfc_offset = 0;
 
     for (std::ptrdiff_t i = 0; i < number_sections; ++i) {
         auto section = sections[i];
 
         if (section.raw.header.filesystem_type == NCASectionFilesystemType::ROMFS) {
-            const size_t romfs_offset =
-                header.section_tables[i].media_offset * MEDIA_OFFSET_MULTIPLIER +
-                section.romfs.ivfc.levels[IVFC_MAX_LEVEL - 1].offset;
+            const size_t base_offset =
+                header.section_tables[i].media_offset * MEDIA_OFFSET_MULTIPLIER;
+            ivfc_offset = section.romfs.ivfc.levels[IVFC_MAX_LEVEL - 1].offset;
+            const size_t romfs_offset = base_offset + ivfc_offset;
             const size_t romfs_size = section.romfs.ivfc.levels[IVFC_MAX_LEVEL - 1].size;
-            auto dec =
-                Decrypt(section, std::make_shared<OffsetVfsFile>(file, romfs_size, romfs_offset),
-                        romfs_offset);
-            if (dec != nullptr) {
-                files.push_back(std::move(dec));
-                romfs = files.back();
-            } else {
+            auto raw = std::make_shared<OffsetVfsFile>(file, romfs_size, romfs_offset);
+            auto dec = Decrypt(section, raw, romfs_offset);
+
+            if (dec == nullptr) {
                 if (status != Loader::ResultStatus::Success)
                     return;
                 if (has_rights_id)
@@ -289,6 +315,117 @@ NCA::NCA(VirtualFile file_) : file(std::move(file_)) {
                     status = Loader::ResultStatus::ErrorIncorrectKeyAreaKey;
                 return;
             }
+
+            if (section.raw.header.crypto_type == NCASectionCryptoType::BKTR) {
+                if (section.bktr.relocation.magic != Common::MakeMagic('B', 'K', 'T', 'R') ||
+                    section.bktr.subsection.magic != Common::MakeMagic('B', 'K', 'T', 'R')) {
+                    status = Loader::ResultStatus::ErrorBadBKTRHeader;
+                    return;
+                }
+
+                if (section.bktr.relocation.offset + section.bktr.relocation.size !=
+                    section.bktr.subsection.offset) {
+                    status = Loader::ResultStatus::ErrorBKTRSubsectionNotAfterRelocation;
+                    return;
+                }
+
+                const u64 size =
+                    MEDIA_OFFSET_MULTIPLIER * (header.section_tables[i].media_end_offset -
+                                               header.section_tables[i].media_offset);
+                if (section.bktr.subsection.offset + section.bktr.subsection.size != size) {
+                    status = Loader::ResultStatus::ErrorBKTRSubsectionNotAtEnd;
+                    return;
+                }
+
+                const u64 offset = section.romfs.ivfc.levels[IVFC_MAX_LEVEL - 1].offset;
+                RelocationBlock relocation_block{};
+                if (dec->ReadObject(&relocation_block, section.bktr.relocation.offset - offset) !=
+                    sizeof(RelocationBlock)) {
+                    status = Loader::ResultStatus::ErrorBadRelocationBlock;
+                    return;
+                }
+                SubsectionBlock subsection_block{};
+                if (dec->ReadObject(&subsection_block, section.bktr.subsection.offset - offset) !=
+                    sizeof(RelocationBlock)) {
+                    status = Loader::ResultStatus::ErrorBadSubsectionBlock;
+                    return;
+                }
+
+                std::vector<RelocationBucketRaw> relocation_buckets_raw(
+                    (section.bktr.relocation.size - sizeof(RelocationBlock)) /
+                    sizeof(RelocationBucketRaw));
+                if (dec->ReadBytes(relocation_buckets_raw.data(),
+                                   section.bktr.relocation.size - sizeof(RelocationBlock),
+                                   section.bktr.relocation.offset + sizeof(RelocationBlock) -
+                                       offset) !=
+                    section.bktr.relocation.size - sizeof(RelocationBlock)) {
+                    status = Loader::ResultStatus::ErrorBadRelocationBuckets;
+                    return;
+                }
+
+                std::vector<SubsectionBucketRaw> subsection_buckets_raw(
+                    (section.bktr.subsection.size - sizeof(SubsectionBlock)) /
+                    sizeof(SubsectionBucketRaw));
+                if (dec->ReadBytes(subsection_buckets_raw.data(),
+                                   section.bktr.subsection.size - sizeof(SubsectionBlock),
+                                   section.bktr.subsection.offset + sizeof(SubsectionBlock) -
+                                       offset) !=
+                    section.bktr.subsection.size - sizeof(SubsectionBlock)) {
+                    status = Loader::ResultStatus::ErrorBadSubsectionBuckets;
+                    return;
+                }
+
+                std::vector<RelocationBucket> relocation_buckets(relocation_buckets_raw.size());
+                std::transform(relocation_buckets_raw.begin(), relocation_buckets_raw.end(),
+                               relocation_buckets.begin(), &ConvertRelocationBucketRaw);
+                std::vector<SubsectionBucket> subsection_buckets(subsection_buckets_raw.size());
+                std::transform(subsection_buckets_raw.begin(), subsection_buckets_raw.end(),
+                               subsection_buckets.begin(), &ConvertSubsectionBucketRaw);
+
+                u32 ctr_low;
+                std::memcpy(&ctr_low, section.raw.section_ctr.data(), sizeof(ctr_low));
+                subsection_buckets.back().entries.push_back(
+                    {section.bktr.relocation.offset, {0}, ctr_low});
+                subsection_buckets.back().entries.push_back({size, {0}, 0});
+
+                boost::optional<Core::Crypto::Key128> key = boost::none;
+                if (encrypted) {
+                    if (has_rights_id) {
+                        status = Loader::ResultStatus::Success;
+                        key = GetTitlekey();
+                        if (key == boost::none) {
+                            status = Loader::ResultStatus::ErrorMissingTitlekey;
+                            return;
+                        }
+                    } else {
+                        key = GetKeyAreaKey(NCASectionCryptoType::BKTR);
+                        if (key == boost::none) {
+                            status = Loader::ResultStatus::ErrorMissingKeyAreaKey;
+                            return;
+                        }
+                    }
+                }
+
+                if (bktr_base_romfs == nullptr) {
+                    status = Loader::ResultStatus::ErrorMissingBKTRBaseRomFS;
+                    return;
+                }
+
+                auto bktr = std::make_shared<BKTR>(
+                    bktr_base_romfs, std::make_shared<OffsetVfsFile>(file, romfs_size, base_offset),
+                    relocation_block, relocation_buckets, subsection_block, subsection_buckets,
+                    encrypted, encrypted ? key.get() : Core::Crypto::Key128{}, base_offset,
+                    bktr_base_ivfc_offset, section.raw.section_ctr);
+
+                // BKTR applies to entire IVFC, so make an offset version to level 6
+
+                files.push_back(std::make_shared<OffsetVfsFile>(
+                    bktr, romfs_size, section.romfs.ivfc.levels[IVFC_MAX_LEVEL - 1].offset));
+                romfs = files.back();
+            } else {
+                files.push_back(std::move(dec));
+                romfs = files.back();
+            }
         } else if (section.raw.header.filesystem_type == NCASectionFilesystemType::PFS0) {
             u64 offset = (static_cast<u64>(header.section_tables[i].media_offset) *
                           MEDIA_OFFSET_MULTIPLIER) +
@@ -304,6 +441,12 @@ NCA::NCA(VirtualFile file_) : file(std::move(file_)) {
                     dirs.push_back(std::move(npfs));
                     if (IsDirectoryExeFS(dirs.back()))
                         exefs = dirs.back();
+                } else {
+                    if (has_rights_id)
+                        status = Loader::ResultStatus::ErrorIncorrectTitlekeyOrTitlekek;
+                    else
+                        status = Loader::ResultStatus::ErrorIncorrectKeyAreaKey;
+                    return;
                 }
             } else {
                 if (status != Loader::ResultStatus::Success)
@@ -349,11 +492,15 @@ NCAContentType NCA::GetType() const {
 }
 
 u64 NCA::GetTitleId() const {
-    if (status != Loader::ResultStatus::Success)
-        return {};
+    if (is_update || status == Loader::ResultStatus::ErrorMissingBKTRBaseRomFS)
+        return header.title_id | 0x800;
     return header.title_id;
 }
 
+bool NCA::IsUpdate() const {
+    return is_update;
+}
+
 VirtualFile NCA::GetRomFS() const {
     return romfs;
 }
@@ -366,8 +513,8 @@ VirtualFile NCA::GetBaseFile() const {
     return file;
 }
 
-bool NCA::IsUpdate() const {
-    return is_update;
+u64 NCA::GetBaseIVFCOffset() const {
+    return ivfc_offset;
 }
 
 bool NCA::ReplaceFileWithSubdirectory(VirtualFile file, VirtualDir dir) {
diff --git a/src/core/file_sys/content_archive.h b/src/core/file_sys/content_archive.h
index 0ea666cac4da1900c358a80a1a93d1ac1e352251..00eca52da5107cd4581c41967bab0f79ad71783c 100644
--- a/src/core/file_sys/content_archive.h
+++ b/src/core/file_sys/content_archive.h
@@ -79,7 +79,8 @@ bool IsValidNCA(const NCAHeader& header);
 // After construction, use GetStatus to determine if the file is valid and ready to be used.
 class NCA : public ReadOnlyVfsDirectory {
 public:
-    explicit NCA(VirtualFile file);
+    explicit NCA(VirtualFile file, VirtualFile bktr_base_romfs = nullptr,
+                 u64 bktr_base_ivfc_offset = 0);
     Loader::ResultStatus GetStatus() const;
 
     std::vector<std::shared_ptr<VfsFile>> GetFiles() const override;
@@ -89,13 +90,15 @@ public:
 
     NCAContentType GetType() const;
     u64 GetTitleId() const;
+    bool IsUpdate() const;
 
     VirtualFile GetRomFS() const;
     VirtualDir GetExeFS() const;
 
     VirtualFile GetBaseFile() const;
 
-    bool IsUpdate() const;
+    // Returns the base ivfc offset used in BKTR patching.
+    u64 GetBaseIVFCOffset() const;
 
 protected:
     bool ReplaceFileWithSubdirectory(VirtualFile file, VirtualDir dir) override;
@@ -112,14 +115,16 @@ private:
     VirtualFile romfs = nullptr;
     VirtualDir exefs = nullptr;
     VirtualFile file;
+    VirtualFile bktr_base_romfs;
+    u64 ivfc_offset;
 
     NCAHeader header{};
     bool has_rights_id{};
-    bool is_update{};
 
     Loader::ResultStatus status{};
 
     bool encrypted;
+    bool is_update;
 
     Core::Crypto::KeyManager keys;
 };
diff --git a/src/core/file_sys/nca_patch.cpp b/src/core/file_sys/nca_patch.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e0111bffc4cc5ba6ee842b7967adc64a4f3fb5df
--- /dev/null
+++ b/src/core/file_sys/nca_patch.cpp
@@ -0,0 +1,206 @@
+// Copyright 2018 yuzu emulator team
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include "common/assert.h"
+#include "core/crypto/aes_util.h"
+#include "core/file_sys/nca_patch.h"
+
+namespace FileSys {
+
+BKTR::BKTR(VirtualFile base_romfs_, VirtualFile bktr_romfs_, RelocationBlock relocation_,
+           std::vector<RelocationBucket> relocation_buckets_, SubsectionBlock subsection_,
+           std::vector<SubsectionBucket> subsection_buckets_, bool is_encrypted_,
+           Core::Crypto::Key128 key_, u64 base_offset_, u64 ivfc_offset_,
+           std::array<u8, 8> section_ctr_)
+    : base_romfs(std::move(base_romfs_)), bktr_romfs(std::move(bktr_romfs_)),
+      relocation(relocation_), relocation_buckets(std::move(relocation_buckets_)),
+      subsection(subsection_), subsection_buckets(std::move(subsection_buckets_)),
+      encrypted(is_encrypted_), key(key_), base_offset(base_offset_), ivfc_offset(ivfc_offset_),
+      section_ctr(section_ctr_) {
+    for (size_t i = 0; i < relocation.number_buckets - 1; ++i) {
+        relocation_buckets[i].entries.push_back({relocation.base_offsets[i + 1], 0, 0});
+    }
+
+    for (size_t i = 0; i < subsection.number_buckets - 1; ++i) {
+        subsection_buckets[i].entries.push_back({subsection_buckets[i + 1].entries[0].address_patch,
+                                                 {0},
+                                                 subsection_buckets[i + 1].entries[0].ctr});
+    }
+
+    relocation_buckets.back().entries.push_back({relocation.size, 0, 0});
+}
+
+BKTR::~BKTR() = default;
+
+size_t BKTR::Read(u8* data, size_t length, size_t offset) const {
+    // Read out of bounds.
+    if (offset >= relocation.size)
+        return 0;
+    const auto relocation = GetRelocationEntry(offset);
+    const auto section_offset = offset - relocation.address_patch + relocation.address_source;
+    const auto bktr_read = relocation.from_patch;
+
+    const auto next_relocation = GetNextRelocationEntry(offset);
+
+    if (offset + length > next_relocation.address_patch) {
+        const u64 partition = next_relocation.address_patch - offset;
+        return Read(data, partition, offset) +
+               Read(data + partition, length - partition, offset + partition);
+    }
+
+    if (!bktr_read) {
+        ASSERT_MSG(section_offset >= ivfc_offset, "Offset calculation negative.");
+        return base_romfs->Read(data, length, section_offset - ivfc_offset);
+    }
+
+    if (!encrypted) {
+        return bktr_romfs->Read(data, length, section_offset);
+    }
+
+    const auto subsection = GetSubsectionEntry(section_offset);
+    Core::Crypto::AESCipher<Core::Crypto::Key128> cipher(key, Core::Crypto::Mode::CTR);
+
+    // Calculate AES IV
+    std::vector<u8> iv(16);
+    auto subsection_ctr = subsection.ctr;
+    auto offset_iv = section_offset + base_offset;
+    for (size_t i = 0; i < section_ctr.size(); ++i)
+        iv[i] = section_ctr[0x8 - i - 1];
+    offset_iv >>= 4;
+    for (size_t i = 0; i < sizeof(u64); ++i) {
+        iv[0xF - i] = static_cast<u8>(offset_iv & 0xFF);
+        offset_iv >>= 8;
+    }
+    for (size_t i = 0; i < sizeof(u32); ++i) {
+        iv[0x7 - i] = static_cast<u8>(subsection_ctr & 0xFF);
+        subsection_ctr >>= 8;
+    }
+    cipher.SetIV(iv);
+
+    const auto next_subsection = GetNextSubsectionEntry(section_offset);
+
+    if (section_offset + length > next_subsection.address_patch) {
+        const u64 partition = next_subsection.address_patch - section_offset;
+        return Read(data, partition, offset) +
+               Read(data + partition, length - partition, offset + partition);
+    }
+
+    const auto block_offset = section_offset & 0xF;
+    if (block_offset != 0) {
+        auto block = bktr_romfs->ReadBytes(0x10, section_offset & ~0xF);
+        cipher.Transcode(block.data(), block.size(), block.data(), Core::Crypto::Op::Decrypt);
+        if (length + block_offset < 0x10) {
+            std::memcpy(data, block.data() + block_offset, std::min(length, block.size()));
+            return std::min(length, block.size());
+        }
+
+        const auto read = 0x10 - block_offset;
+        std::memcpy(data, block.data() + block_offset, read);
+        return read + Read(data + read, length - read, offset + read);
+    }
+
+    const auto raw_read = bktr_romfs->Read(data, length, section_offset);
+    cipher.Transcode(data, raw_read, data, Core::Crypto::Op::Decrypt);
+    return raw_read;
+}
+
+template <bool Subsection, typename BlockType, typename BucketType>
+std::pair<size_t, size_t> BKTR::SearchBucketEntry(u64 offset, BlockType block,
+                                                  BucketType buckets) const {
+    if constexpr (Subsection) {
+        const auto last_bucket = buckets[block.number_buckets - 1];
+        if (offset >= last_bucket.entries[last_bucket.number_entries].address_patch)
+            return {block.number_buckets - 1, last_bucket.number_entries};
+    } else {
+        ASSERT_MSG(offset <= block.size, "Offset is out of bounds in BKTR relocation block.");
+    }
+
+    size_t bucket_id = std::count_if(block.base_offsets.begin() + 1,
+                                     block.base_offsets.begin() + block.number_buckets,
+                                     [&offset](u64 base_offset) { return base_offset <= offset; });
+
+    const auto bucket = buckets[bucket_id];
+
+    if (bucket.number_entries == 1)
+        return {bucket_id, 0};
+
+    size_t low = 0;
+    size_t mid = 0;
+    size_t high = bucket.number_entries - 1;
+    while (low <= high) {
+        mid = (low + high) / 2;
+        if (bucket.entries[mid].address_patch > offset) {
+            high = mid - 1;
+        } else {
+            if (mid == bucket.number_entries - 1 ||
+                bucket.entries[mid + 1].address_patch > offset) {
+                return {bucket_id, mid};
+            }
+
+            low = mid + 1;
+        }
+    }
+
+    UNREACHABLE_MSG("Offset could not be found in BKTR block.");
+}
+
+RelocationEntry BKTR::GetRelocationEntry(u64 offset) const {
+    const auto res = SearchBucketEntry<false>(offset, relocation, relocation_buckets);
+    return relocation_buckets[res.first].entries[res.second];
+}
+
+RelocationEntry BKTR::GetNextRelocationEntry(u64 offset) const {
+    const auto res = SearchBucketEntry<false>(offset, relocation, relocation_buckets);
+    const auto bucket = relocation_buckets[res.first];
+    if (res.second + 1 < bucket.entries.size())
+        return bucket.entries[res.second + 1];
+    return relocation_buckets[res.first + 1].entries[0];
+}
+
+SubsectionEntry BKTR::GetSubsectionEntry(u64 offset) const {
+    const auto res = SearchBucketEntry<true>(offset, subsection, subsection_buckets);
+    return subsection_buckets[res.first].entries[res.second];
+}
+
+SubsectionEntry BKTR::GetNextSubsectionEntry(u64 offset) const {
+    const auto res = SearchBucketEntry<true>(offset, subsection, subsection_buckets);
+    const auto bucket = subsection_buckets[res.first];
+    if (res.second + 1 < bucket.entries.size())
+        return bucket.entries[res.second + 1];
+    return subsection_buckets[res.first + 1].entries[0];
+}
+
+std::string BKTR::GetName() const {
+    return base_romfs->GetName();
+}
+
+size_t BKTR::GetSize() const {
+    return relocation.size;
+}
+
+bool BKTR::Resize(size_t new_size) {
+    return false;
+}
+
+std::shared_ptr<VfsDirectory> BKTR::GetContainingDirectory() const {
+    return base_romfs->GetContainingDirectory();
+}
+
+bool BKTR::IsWritable() const {
+    return false;
+}
+
+bool BKTR::IsReadable() const {
+    return true;
+}
+
+size_t BKTR::Write(const u8* data, size_t length, size_t offset) {
+    return 0;
+}
+
+bool BKTR::Rename(std::string_view name) {
+    return base_romfs->Rename(name);
+}
+
+} // namespace FileSys
diff --git a/src/core/file_sys/nca_patch.h b/src/core/file_sys/nca_patch.h
new file mode 100644
index 0000000000000000000000000000000000000000..0d9ad95f53e66b619c494d20daeaeac12d761327
--- /dev/null
+++ b/src/core/file_sys/nca_patch.h
@@ -0,0 +1,147 @@
+// Copyright 2018 yuzu emulator team
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <array>
+#include <vector>
+#include <common/common_funcs.h>
+#include "core/crypto/key_manager.h"
+#include "core/file_sys/romfs.h"
+
+namespace FileSys {
+
+#pragma pack(push, 1)
+struct RelocationEntry {
+    u64_le address_patch;
+    u64_le address_source;
+    u32 from_patch;
+};
+#pragma pack(pop)
+static_assert(sizeof(RelocationEntry) == 0x14, "RelocationEntry has incorrect size.");
+
+struct RelocationBucketRaw {
+    INSERT_PADDING_BYTES(4);
+    u32_le number_entries;
+    u64_le end_offset;
+    std::array<RelocationEntry, 0x332> relocation_entries;
+    INSERT_PADDING_BYTES(8);
+};
+static_assert(sizeof(RelocationBucketRaw) == 0x4000, "RelocationBucketRaw has incorrect size.");
+
+// Vector version of RelocationBucketRaw
+struct RelocationBucket {
+    u32 number_entries;
+    u64 end_offset;
+    std::vector<RelocationEntry> entries;
+};
+
+struct RelocationBlock {
+    INSERT_PADDING_BYTES(4);
+    u32_le number_buckets;
+    u64_le size;
+    std::array<u64, 0x7FE> base_offsets;
+};
+static_assert(sizeof(RelocationBlock) == 0x4000, "RelocationBlock has incorrect size.");
+
+struct SubsectionEntry {
+    u64_le address_patch;
+    INSERT_PADDING_BYTES(0x4);
+    u32_le ctr;
+};
+static_assert(sizeof(SubsectionEntry) == 0x10, "SubsectionEntry has incorrect size.");
+
+struct SubsectionBucketRaw {
+    INSERT_PADDING_BYTES(4);
+    u32_le number_entries;
+    u64_le end_offset;
+    std::array<SubsectionEntry, 0x3FF> subsection_entries;
+};
+static_assert(sizeof(SubsectionBucketRaw) == 0x4000, "SubsectionBucketRaw has incorrect size.");
+
+// Vector version of SubsectionBucketRaw
+struct SubsectionBucket {
+    u32 number_entries;
+    u64 end_offset;
+    std::vector<SubsectionEntry> entries;
+};
+
+struct SubsectionBlock {
+    INSERT_PADDING_BYTES(4);
+    u32_le number_buckets;
+    u64_le size;
+    std::array<u64, 0x7FE> base_offsets;
+};
+static_assert(sizeof(SubsectionBlock) == 0x4000, "SubsectionBlock has incorrect size.");
+
+inline RelocationBucket ConvertRelocationBucketRaw(RelocationBucketRaw raw) {
+    return {raw.number_entries,
+            raw.end_offset,
+            {raw.relocation_entries.begin(), raw.relocation_entries.begin() + raw.number_entries}};
+}
+
+inline SubsectionBucket ConvertSubsectionBucketRaw(SubsectionBucketRaw raw) {
+    return {raw.number_entries,
+            raw.end_offset,
+            {raw.subsection_entries.begin(), raw.subsection_entries.begin() + raw.number_entries}};
+}
+
+class BKTR : public VfsFile {
+public:
+    BKTR(VirtualFile base_romfs, VirtualFile bktr_romfs, RelocationBlock relocation,
+         std::vector<RelocationBucket> relocation_buckets, SubsectionBlock subsection,
+         std::vector<SubsectionBucket> subsection_buckets, bool is_encrypted,
+         Core::Crypto::Key128 key, u64 base_offset, u64 ivfc_offset, std::array<u8, 8> section_ctr);
+    ~BKTR() override;
+
+    size_t Read(u8* data, size_t length, size_t offset) const override;
+
+    std::string GetName() const override;
+
+    size_t GetSize() const override;
+
+    bool Resize(size_t new_size) override;
+
+    std::shared_ptr<VfsDirectory> GetContainingDirectory() const override;
+
+    bool IsWritable() const override;
+
+    bool IsReadable() const override;
+
+    size_t Write(const u8* data, size_t length, size_t offset) override;
+
+    bool Rename(std::string_view name) override;
+
+private:
+    template <bool Subsection, typename BlockType, typename BucketType>
+    std::pair<size_t, size_t> SearchBucketEntry(u64 offset, BlockType block,
+                                                BucketType buckets) const;
+
+    RelocationEntry GetRelocationEntry(u64 offset) const;
+    RelocationEntry GetNextRelocationEntry(u64 offset) const;
+
+    SubsectionEntry GetSubsectionEntry(u64 offset) const;
+    SubsectionEntry GetNextSubsectionEntry(u64 offset) const;
+
+    RelocationBlock relocation;
+    std::vector<RelocationBucket> relocation_buckets;
+    SubsectionBlock subsection;
+    std::vector<SubsectionBucket> subsection_buckets;
+
+    // Should be the raw base romfs, decrypted.
+    VirtualFile base_romfs;
+    // Should be the raw BKTR romfs, (located at media_offset with size media_size).
+    VirtualFile bktr_romfs;
+
+    bool encrypted;
+    Core::Crypto::Key128 key;
+
+    // Base offset into NCA, used for IV calculation.
+    u64 base_offset;
+    // Distance between IVFC start and RomFS start, used for base reads
+    u64 ivfc_offset;
+    std::array<u8, 8> section_ctr;
+};
+
+} // namespace FileSys
diff --git a/src/core/file_sys/patch_manager.cpp b/src/core/file_sys/patch_manager.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..40675de35bd0a15b7eafe6faa029f5aaeb1c3c43
--- /dev/null
+++ b/src/core/file_sys/patch_manager.cpp
@@ -0,0 +1,153 @@
+// Copyright 2018 yuzu emulator team
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include "core/file_sys/content_archive.h"
+#include "core/file_sys/control_metadata.h"
+#include "core/file_sys/patch_manager.h"
+#include "core/file_sys/registered_cache.h"
+#include "core/file_sys/romfs.h"
+#include "core/hle/service/filesystem/filesystem.h"
+#include "core/loader/loader.h"
+
+namespace FileSys {
+
+constexpr u64 SINGLE_BYTE_MODULUS = 0x100;
+
+std::string FormatTitleVersion(u32 version, TitleVersionFormat format) {
+    std::array<u8, sizeof(u32)> bytes{};
+    bytes[0] = version % SINGLE_BYTE_MODULUS;
+    for (size_t i = 1; i < bytes.size(); ++i) {
+        version /= SINGLE_BYTE_MODULUS;
+        bytes[i] = version % SINGLE_BYTE_MODULUS;
+    }
+
+    if (format == TitleVersionFormat::FourElements)
+        return fmt::format("v{}.{}.{}.{}", bytes[3], bytes[2], bytes[1], bytes[0]);
+    return fmt::format("v{}.{}.{}", bytes[3], bytes[2], bytes[1]);
+}
+
+constexpr std::array<const char*, 1> PATCH_TYPE_NAMES{
+    "Update",
+};
+
+std::string FormatPatchTypeName(PatchType type) {
+    return PATCH_TYPE_NAMES.at(static_cast<size_t>(type));
+}
+
+PatchManager::PatchManager(u64 title_id) : title_id(title_id) {}
+
+VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const {
+    LOG_INFO(Loader, "Patching ExeFS for title_id={:016X}", title_id);
+
+    if (exefs == nullptr)
+        return exefs;
+
+    const auto installed = Service::FileSystem::GetUnionContents();
+
+    // Game Updates
+    const auto update_tid = GetUpdateTitleID(title_id);
+    const auto update = installed->GetEntry(update_tid, ContentRecordType::Program);
+    if (update != nullptr) {
+        if (update->GetStatus() == Loader::ResultStatus::ErrorMissingBKTRBaseRomFS &&
+            update->GetExeFS() != nullptr) {
+            LOG_INFO(Loader, "    ExeFS: Update ({}) applied successfully",
+                     FormatTitleVersion(installed->GetEntryVersion(update_tid).get_value_or(0)));
+            exefs = update->GetExeFS();
+        }
+    }
+
+    return exefs;
+}
+
+VirtualFile PatchManager::PatchRomFS(VirtualFile romfs, u64 ivfc_offset,
+                                     ContentRecordType type) const {
+    LOG_INFO(Loader, "Patching RomFS for title_id={:016X}, type={:02X}", title_id,
+             static_cast<u8>(type));
+
+    if (romfs == nullptr)
+        return romfs;
+
+    const auto installed = Service::FileSystem::GetUnionContents();
+
+    // Game Updates
+    const auto update_tid = GetUpdateTitleID(title_id);
+    const auto update = installed->GetEntryRaw(update_tid, type);
+    if (update != nullptr) {
+        const auto new_nca = std::make_shared<NCA>(update, romfs, ivfc_offset);
+        if (new_nca->GetStatus() == Loader::ResultStatus::Success &&
+            new_nca->GetRomFS() != nullptr) {
+            LOG_INFO(Loader, "    RomFS: Update ({}) applied successfully",
+                     FormatTitleVersion(installed->GetEntryVersion(update_tid).get_value_or(0)));
+            romfs = new_nca->GetRomFS();
+        }
+    }
+
+    return romfs;
+}
+
+std::map<PatchType, std::string> PatchManager::GetPatchVersionNames() const {
+    std::map<PatchType, std::string> out;
+    const auto installed = Service::FileSystem::GetUnionContents();
+
+    const auto update_tid = GetUpdateTitleID(title_id);
+    PatchManager update{update_tid};
+    auto [nacp, discard_icon_file] = update.GetControlMetadata();
+
+    if (nacp != nullptr) {
+        out[PatchType::Update] = nacp->GetVersionString();
+    } else {
+        if (installed->HasEntry(update_tid, ContentRecordType::Program)) {
+            const auto meta_ver = installed->GetEntryVersion(update_tid);
+            if (meta_ver == boost::none || meta_ver.get() == 0) {
+                out[PatchType::Update] = "";
+            } else {
+                out[PatchType::Update] =
+                    FormatTitleVersion(meta_ver.get(), TitleVersionFormat::ThreeElements);
+            }
+        }
+    }
+
+    return out;
+}
+
+std::pair<std::shared_ptr<NACP>, VirtualFile> PatchManager::GetControlMetadata() const {
+    const auto& installed{Service::FileSystem::GetUnionContents()};
+
+    const auto base_control_nca = installed->GetEntry(title_id, ContentRecordType::Control);
+    if (base_control_nca == nullptr)
+        return {};
+
+    return ParseControlNCA(base_control_nca);
+}
+
+std::pair<std::shared_ptr<NACP>, VirtualFile> PatchManager::ParseControlNCA(
+    const std::shared_ptr<NCA>& nca) const {
+    const auto base_romfs = nca->GetRomFS();
+    if (base_romfs == nullptr)
+        return {};
+
+    const auto romfs = PatchRomFS(base_romfs, nca->GetBaseIVFCOffset(), ContentRecordType::Control);
+    if (romfs == nullptr)
+        return {};
+
+    const auto extracted = ExtractRomFS(romfs);
+    if (extracted == nullptr)
+        return {};
+
+    auto nacp_file = extracted->GetFile("control.nacp");
+    if (nacp_file == nullptr)
+        nacp_file = extracted->GetFile("Control.nacp");
+
+    const auto nacp = nacp_file == nullptr ? nullptr : std::make_shared<NACP>(nacp_file);
+
+    VirtualFile icon_file;
+    for (const auto& language : FileSys::LANGUAGE_NAMES) {
+        icon_file = extracted->GetFile("icon_" + std::string(language) + ".dat");
+        if (icon_file != nullptr)
+            break;
+    }
+
+    return {nacp, icon_file};
+}
+} // namespace FileSys
diff --git a/src/core/file_sys/patch_manager.h b/src/core/file_sys/patch_manager.h
new file mode 100644
index 0000000000000000000000000000000000000000..28c7ae1363460abdaee8c718cefc2de1f7478a52
--- /dev/null
+++ b/src/core/file_sys/patch_manager.h
@@ -0,0 +1,62 @@
+// Copyright 2018 yuzu emulator team
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <map>
+#include <string>
+#include "common/common_types.h"
+#include "core/file_sys/nca_metadata.h"
+#include "core/file_sys/vfs.h"
+
+namespace FileSys {
+
+class NCA;
+class NACP;
+
+enum class TitleVersionFormat : u8 {
+    ThreeElements, ///< vX.Y.Z
+    FourElements,  ///< vX.Y.Z.W
+};
+
+std::string FormatTitleVersion(u32 version,
+                               TitleVersionFormat format = TitleVersionFormat::ThreeElements);
+
+enum class PatchType {
+    Update,
+};
+
+std::string FormatPatchTypeName(PatchType type);
+
+// A centralized class to manage patches to games.
+class PatchManager {
+public:
+    explicit PatchManager(u64 title_id);
+
+    // Currently tracked ExeFS patches:
+    // - Game Updates
+    VirtualDir PatchExeFS(VirtualDir exefs) const;
+
+    // Currently tracked RomFS patches:
+    // - Game Updates
+    VirtualFile PatchRomFS(VirtualFile base, u64 ivfc_offset,
+                           ContentRecordType type = ContentRecordType::Program) const;
+
+    // Returns a vector of pairs between patch names and patch versions.
+    // i.e. Update v80 will return {Update, 80}
+    std::map<PatchType, std::string> GetPatchVersionNames() const;
+
+    // Given title_id of the program, attempts to get the control data of the update and parse it,
+    // falling back to the base control data.
+    std::pair<std::shared_ptr<NACP>, VirtualFile> GetControlMetadata() const;
+
+    // Version of GetControlMetadata that takes an arbitrary NCA
+    std::pair<std::shared_ptr<NACP>, VirtualFile> ParseControlNCA(
+        const std::shared_ptr<NCA>& nca) const;
+
+private:
+    u64 title_id;
+};
+
+} // namespace FileSys
diff --git a/src/core/file_sys/registered_cache.cpp b/src/core/file_sys/registered_cache.cpp
index cf6f774010f60e406a73b141baeb6996e9e63354..7361a67be69a3f963841274a6d08d290805130e7 100644
--- a/src/core/file_sys/registered_cache.cpp
+++ b/src/core/file_sys/registered_cache.cpp
@@ -280,6 +280,18 @@ VirtualFile RegisteredCache::GetEntryUnparsed(RegisteredCacheEntry entry) const
     return GetEntryUnparsed(entry.title_id, entry.type);
 }
 
+boost::optional<u32> RegisteredCache::GetEntryVersion(u64 title_id) const {
+    const auto meta_iter = meta.find(title_id);
+    if (meta_iter != meta.end())
+        return meta_iter->second.GetTitleVersion();
+
+    const auto yuzu_meta_iter = yuzu_meta.find(title_id);
+    if (yuzu_meta_iter != yuzu_meta.end())
+        return yuzu_meta_iter->second.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 +510,107 @@ 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 {
+    return std::any_of(caches.begin(), caches.end(), [title_id, type](const auto& cache) {
+        return cache->HasEntry(title_id, type);
+    });
+}
+
+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 467ceeef133b5b0c84d944d6efe6707fca3a746c..f487b0cf0a91a87debfe950bef547321da86f9b8 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 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/file_sys/romfs_factory.cpp b/src/core/file_sys/romfs_factory.cpp
index 66f9786e016f0f990fab06ee2931575b5fc162be..d9d90939eb81bc04e0520bbf71e571a61c43f391 100644
--- a/src/core/file_sys/romfs_factory.cpp
+++ b/src/core/file_sys/romfs_factory.cpp
@@ -6,9 +6,13 @@
 #include "common/assert.h"
 #include "common/common_types.h"
 #include "common/logging/log.h"
+#include "core/core.h"
 #include "core/file_sys/content_archive.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/romfs_factory.h"
+#include "core/hle/kernel/process.h"
 #include "core/hle/service/filesystem/filesystem.h"
 #include "core/loader/loader.h"
 
@@ -19,10 +23,17 @@ RomFSFactory::RomFSFactory(Loader::AppLoader& app_loader) {
     if (app_loader.ReadRomFS(file) != Loader::ResultStatus::Success) {
         LOG_ERROR(Service_FS, "Unable to read RomFS!");
     }
+
+    updatable = app_loader.IsRomFSUpdatable();
+    ivfc_offset = app_loader.ReadRomFSIVFCOffset();
 }
 
 ResultVal<VirtualFile> RomFSFactory::OpenCurrentProcess() {
-    return MakeResult<VirtualFile>(file);
+    if (!updatable)
+        return MakeResult<VirtualFile>(file);
+
+    const PatchManager patch_manager(Core::CurrentProcess()->program_id);
+    return MakeResult<VirtualFile>(patch_manager.PatchRomFS(file, ivfc_offset));
 }
 
 ResultVal<VirtualFile> RomFSFactory::Open(u64 title_id, StorageId storage, ContentRecordType type) {
diff --git a/src/core/file_sys/romfs_factory.h b/src/core/file_sys/romfs_factory.h
index f38ddc4f7c5398d8214b925d45a3e92c788368cc..26b8f46cc2565525074ab5a9318d94e12257535b 100644
--- a/src/core/file_sys/romfs_factory.h
+++ b/src/core/file_sys/romfs_factory.h
@@ -36,6 +36,8 @@ public:
 
 private:
     VirtualFile file;
+    bool updatable;
+    u64 ivfc_offset;
 };
 
 } // namespace FileSys
diff --git a/src/core/file_sys/submission_package.cpp b/src/core/file_sys/submission_package.cpp
index bde8798616c57685aeaef47358c7fa9300dcdd5d..182b4069879ed57c7734c71c6081aa7a92fbc2fe 100644
--- a/src/core/file_sys/submission_package.cpp
+++ b/src/core/file_sys/submission_package.cpp
@@ -60,8 +60,11 @@ NSP::NSP(VirtualFile file_)
     for (const auto& outer_file : files) {
         if (outer_file->GetName().substr(outer_file->GetName().size() - 9) == ".cnmt.nca") {
             const auto nca = std::make_shared<NCA>(outer_file);
-            if (nca->GetStatus() != Loader::ResultStatus::Success)
+            if (nca->GetStatus() != Loader::ResultStatus::Success) {
+                program_status[nca->GetTitleId()] = nca->GetStatus();
                 continue;
+            }
+
             const auto section0 = nca->GetSubdirectories()[0];
 
             for (const auto& inner_file : section0->GetFiles()) {
diff --git a/src/core/hle/service/filesystem/filesystem.cpp b/src/core/hle/service/filesystem/filesystem.cpp
index a4426af968b4df602336155f5a0d303c7280acf9..04c9d750fd455d0941ce4a66d7e5bd580b5153c0 100644
--- a/src/core/hle/service/filesystem/filesystem.cpp
+++ b/src/core/hle/service/filesystem/filesystem.cpp
@@ -10,6 +10,7 @@
 #include "core/file_sys/bis_factory.h"
 #include "core/file_sys/errors.h"
 #include "core/file_sys/mode.h"
+#include "core/file_sys/registered_cache.h"
 #include "core/file_sys/romfs_factory.h"
 #include "core/file_sys/savedata_factory.h"
 #include "core/file_sys/sdmc_factory.h"
@@ -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 9ba0e2eab419df0439d705b38b3e5dbcc2699b9a..793a7b06fcc6e8c4f97bcc1a14b18a7985fd435f 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();
diff --git a/src/core/loader/deconstructed_rom_directory.cpp b/src/core/loader/deconstructed_rom_directory.cpp
index 1ae4bb656f1dadaa5ea723842d444477484eebf7..2b8f781360ab4394547686a4502b0848e170adf4 100644
--- a/src/core/loader/deconstructed_rom_directory.cpp
+++ b/src/core/loader/deconstructed_rom_directory.cpp
@@ -9,6 +9,7 @@
 #include "core/core.h"
 #include "core/file_sys/content_archive.h"
 #include "core/file_sys/control_metadata.h"
+#include "core/file_sys/patch_manager.h"
 #include "core/file_sys/romfs_factory.h"
 #include "core/gdbstub/gdbstub.h"
 #include "core/hle/kernel/kernel.h"
@@ -21,10 +22,19 @@
 
 namespace Loader {
 
-AppLoader_DeconstructedRomDirectory::AppLoader_DeconstructedRomDirectory(FileSys::VirtualFile file_)
-    : AppLoader(std::move(file_)) {
+AppLoader_DeconstructedRomDirectory::AppLoader_DeconstructedRomDirectory(FileSys::VirtualFile file_,
+                                                                         bool override_update)
+    : AppLoader(std::move(file_)), override_update(override_update) {
     const auto dir = file->GetContainingDirectory();
 
+    // Title ID
+    const auto npdm = dir->GetFile("main.npdm");
+    if (npdm != nullptr) {
+        const auto res = metadata.Load(npdm);
+        if (res == ResultStatus::Success)
+            title_id = metadata.GetTitleID();
+    }
+
     // Icon
     FileSys::VirtualFile icon_file = nullptr;
     for (const auto& language : FileSys::LANGUAGE_NAMES) {
@@ -66,8 +76,9 @@ AppLoader_DeconstructedRomDirectory::AppLoader_DeconstructedRomDirectory(FileSys
 }
 
 AppLoader_DeconstructedRomDirectory::AppLoader_DeconstructedRomDirectory(
-    FileSys::VirtualDir directory)
-    : AppLoader(directory->GetFile("main")), dir(std::move(directory)) {}
+    FileSys::VirtualDir directory, bool override_update)
+    : AppLoader(directory->GetFile("main")), dir(std::move(directory)),
+      override_update(override_update) {}
 
 FileType AppLoader_DeconstructedRomDirectory::IdentifyType(const FileSys::VirtualFile& file) {
     if (FileSys::IsDirectoryExeFS(file->GetContainingDirectory())) {
@@ -89,7 +100,8 @@ ResultStatus AppLoader_DeconstructedRomDirectory::Load(
         dir = file->GetContainingDirectory();
     }
 
-    const FileSys::VirtualFile npdm = dir->GetFile("main.npdm");
+    // Read meta to determine title ID
+    FileSys::VirtualFile npdm = dir->GetFile("main.npdm");
     if (npdm == nullptr)
         return ResultStatus::ErrorMissingNPDM;
 
@@ -97,6 +109,21 @@ ResultStatus AppLoader_DeconstructedRomDirectory::Load(
     if (result != ResultStatus::Success) {
         return result;
     }
+
+    if (override_update) {
+        const FileSys::PatchManager patch_manager(metadata.GetTitleID());
+        dir = patch_manager.PatchExeFS(dir);
+    }
+
+    // Reread in case PatchExeFS affected the main.npdm
+    npdm = dir->GetFile("main.npdm");
+    if (npdm == nullptr)
+        return ResultStatus::ErrorMissingNPDM;
+
+    ResultStatus result2 = metadata.Load(npdm);
+    if (result2 != ResultStatus::Success) {
+        return result2;
+    }
     metadata.Print();
 
     const FileSys::ProgramAddressSpaceType arch_bits{metadata.GetAddressSpaceType()};
@@ -119,7 +146,6 @@ ResultStatus AppLoader_DeconstructedRomDirectory::Load(
     }
 
     auto& kernel = Core::System::GetInstance().Kernel();
-    title_id = metadata.GetTitleID();
     process->program_id = metadata.GetTitleID();
     process->svc_access_mask.set();
     process->resource_limit =
@@ -170,4 +196,8 @@ ResultStatus AppLoader_DeconstructedRomDirectory::ReadTitle(std::string& title)
     return ResultStatus::Success;
 }
 
+bool AppLoader_DeconstructedRomDirectory::IsRomFSUpdatable() const {
+    return false;
+}
+
 } // namespace Loader
diff --git a/src/core/loader/deconstructed_rom_directory.h b/src/core/loader/deconstructed_rom_directory.h
index b20804f75d8472bd8c3d54673c5182450d1b90bf..8a0dc1b1eb1da21c2de425b693fe2adf03b267bb 100644
--- a/src/core/loader/deconstructed_rom_directory.h
+++ b/src/core/loader/deconstructed_rom_directory.h
@@ -20,10 +20,12 @@ namespace Loader {
  */
 class AppLoader_DeconstructedRomDirectory final : public AppLoader {
 public:
-    explicit AppLoader_DeconstructedRomDirectory(FileSys::VirtualFile main_file);
+    explicit AppLoader_DeconstructedRomDirectory(FileSys::VirtualFile main_file,
+                                                 bool override_update = false);
 
     // Overload to accept exefs directory. Must contain 'main' and 'main.npdm'
-    explicit AppLoader_DeconstructedRomDirectory(FileSys::VirtualDir directory);
+    explicit AppLoader_DeconstructedRomDirectory(FileSys::VirtualDir directory,
+                                                 bool override_update = false);
 
     /**
      * Returns the type of the file
@@ -42,6 +44,7 @@ public:
     ResultStatus ReadIcon(std::vector<u8>& buffer) override;
     ResultStatus ReadProgramId(u64& out_program_id) override;
     ResultStatus ReadTitle(std::string& title) override;
+    bool IsRomFSUpdatable() const override;
 
 private:
     FileSys::ProgramMetadata metadata;
@@ -51,6 +54,7 @@ private:
     std::vector<u8> icon_data;
     std::string name;
     u64 title_id{};
+    bool override_update;
 };
 
 } // namespace Loader
diff --git a/src/core/loader/loader.cpp b/src/core/loader/loader.cpp
index 446adf5578f3d95785ec5b1e0cae05c0c3cd4b4c..fa43a26506ae702cc16e973ca9364a2135ce800b 100644
--- a/src/core/loader/loader.cpp
+++ b/src/core/loader/loader.cpp
@@ -93,7 +93,7 @@ std::string GetFileTypeString(FileType type) {
     return "unknown";
 }
 
-constexpr std::array<const char*, 50> RESULT_MESSAGES{
+constexpr std::array<const char*, 58> RESULT_MESSAGES{
     "The operation completed successfully.",
     "The loader requested to load is already loaded.",
     "The operation is not implemented.",
@@ -143,7 +143,16 @@ constexpr std::array<const char*, 50> RESULT_MESSAGES{
     "The AES Key Generation Source could not be found.",
     "The SD Save Key Source could not be found.",
     "The SD NCA Key Source could not be found.",
-    "The NSP file is missing a Program-type NCA."};
+    "The NSP file is missing a Program-type NCA.",
+    "The BKTR-type NCA has a bad BKTR header.",
+    "The BKTR Subsection entry is not located immediately after the Relocation entry.",
+    "The BKTR Subsection entry is not at the end of the media block.",
+    "The BKTR-type NCA has a bad Relocation block.",
+    "The BKTR-type NCA has a bad Subsection block.",
+    "The BKTR-type NCA has a bad Relocation bucket.",
+    "The BKTR-type NCA has a bad Subsection bucket.",
+    "The BKTR-type NCA is missing the base RomFS.",
+};
 
 std::ostream& operator<<(std::ostream& os, ResultStatus status) {
     os << RESULT_MESSAGES.at(static_cast<size_t>(status));
diff --git a/src/core/loader/loader.h b/src/core/loader/loader.h
index be66b2257027a3c65ea5c6cf080e894d739d68e6..843c4bb912ae3e4d26de0064e872a82c2fd7ee9e 100644
--- a/src/core/loader/loader.h
+++ b/src/core/loader/loader.h
@@ -107,6 +107,14 @@ enum class ResultStatus : u16 {
     ErrorMissingSDSaveKeySource,
     ErrorMissingSDNCAKeySource,
     ErrorNSPMissingProgramNCA,
+    ErrorBadBKTRHeader,
+    ErrorBKTRSubsectionNotAfterRelocation,
+    ErrorBKTRSubsectionNotAtEnd,
+    ErrorBadRelocationBlock,
+    ErrorBadSubsectionBlock,
+    ErrorBadRelocationBuckets,
+    ErrorBadSubsectionBuckets,
+    ErrorMissingBKTRBaseRomFS,
 };
 
 std::ostream& operator<<(std::ostream& os, ResultStatus status);
@@ -197,13 +205,22 @@ public:
     }
 
     /**
-     * Get the update RomFS of the application
-     * Since the RomFS can be huge, we return a file reference instead of copying to a buffer
-     * @param file The file containing the RomFS
-     * @return ResultStatus result of function
+     * Get whether or not updates can be applied to the RomFS.
+     * By default, this is true, however for formats where it cannot be guaranteed that the RomFS is
+     * the base game it should be set to false.
+     * @return bool whether or not updatable.
      */
-    virtual ResultStatus ReadUpdateRomFS(FileSys::VirtualFile& file) {
-        return ResultStatus::ErrorNotImplemented;
+    virtual bool IsRomFSUpdatable() const {
+        return true;
+    }
+
+    /**
+     * Gets the difference between the start of the IVFC header and the start of level 6 (RomFS)
+     * data. Needed for bktr patching.
+     * @return IVFC offset for romfs.
+     */
+    virtual u64 ReadRomFSIVFCOffset() const {
+        return 0;
     }
 
     /**
diff --git a/src/core/loader/nca.cpp b/src/core/loader/nca.cpp
index c036a8a1cd452d7744441ac635764f85c35fea41..6aaffae5951831b0c3e5ad5f4bd7aaf3c280b959 100644
--- a/src/core/loader/nca.cpp
+++ b/src/core/loader/nca.cpp
@@ -48,7 +48,7 @@ ResultStatus AppLoader_NCA::Load(Kernel::SharedPtr<Kernel::Process>& process) {
     if (exefs == nullptr)
         return ResultStatus::ErrorNoExeFS;
 
-    directory_loader = std::make_unique<AppLoader_DeconstructedRomDirectory>(exefs);
+    directory_loader = std::make_unique<AppLoader_DeconstructedRomDirectory>(exefs, true);
 
     const auto load_result = directory_loader->Load(process);
     if (load_result != ResultStatus::Success)
@@ -71,6 +71,12 @@ ResultStatus AppLoader_NCA::ReadRomFS(FileSys::VirtualFile& dir) {
     return ResultStatus::Success;
 }
 
+u64 AppLoader_NCA::ReadRomFSIVFCOffset() const {
+    if (nca == nullptr)
+        return 0;
+    return nca->GetBaseIVFCOffset();
+}
+
 ResultStatus AppLoader_NCA::ReadProgramId(u64& out_program_id) {
     if (nca == nullptr || nca->GetStatus() != ResultStatus::Success)
         return ResultStatus::ErrorNotInitialized;
diff --git a/src/core/loader/nca.h b/src/core/loader/nca.h
index 326f84857971539a5dd8f4e4083fbea87d7b3e5d..10be197c4ba53ffdb4f44a2b39a540dd1d8bfd99 100644
--- a/src/core/loader/nca.h
+++ b/src/core/loader/nca.h
@@ -37,6 +37,7 @@ public:
     ResultStatus Load(Kernel::SharedPtr<Kernel::Process>& process) override;
 
     ResultStatus ReadRomFS(FileSys::VirtualFile& dir) override;
+    u64 ReadRomFSIVFCOffset() const override;
     ResultStatus ReadProgramId(u64& out_program_id) override;
 
 private:
diff --git a/src/core/loader/nro.cpp b/src/core/loader/nro.cpp
index 77026b850d6ac30e09f86f50ed0673222d533212..bb89a9da3396a743a662e80408fd292504d08d7d 100644
--- a/src/core/loader/nro.cpp
+++ b/src/core/loader/nro.cpp
@@ -232,4 +232,9 @@ ResultStatus AppLoader_NRO::ReadTitle(std::string& title) {
     title = nacp->GetApplicationName();
     return ResultStatus::Success;
 }
+
+bool AppLoader_NRO::IsRomFSUpdatable() const {
+    return false;
+}
+
 } // namespace Loader
diff --git a/src/core/loader/nro.h b/src/core/loader/nro.h
index bb01c9e25b4fd8c00670366097aa6869d2061c52..96d2de30501c0fcf6ca668bb2eee103b11b76799 100644
--- a/src/core/loader/nro.h
+++ b/src/core/loader/nro.h
@@ -39,6 +39,7 @@ public:
     ResultStatus ReadProgramId(u64& out_program_id) override;
     ResultStatus ReadRomFS(FileSys::VirtualFile& dir) override;
     ResultStatus ReadTitle(std::string& title) override;
+    bool IsRomFSUpdatable() const override;
 
 private:
     bool LoadNro(FileSys::VirtualFile file, VAddr load_base);
diff --git a/src/core/loader/nsp.cpp b/src/core/loader/nsp.cpp
index 7c06239f282e539c1c0d988bcf5c2114f1f13140..291a9876da36c50828a6b1d26db46b80740e0e21 100644
--- a/src/core/loader/nsp.cpp
+++ b/src/core/loader/nsp.cpp
@@ -9,6 +9,8 @@
 #include "core/file_sys/content_archive.h"
 #include "core/file_sys/control_metadata.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/romfs.h"
 #include "core/file_sys/submission_package.h"
 #include "core/hle/kernel/process.h"
@@ -28,24 +30,12 @@ AppLoader_NSP::AppLoader_NSP(FileSys::VirtualFile file)
         return;
 
     const auto control_nca =
-        nsp->GetNCA(nsp->GetFirstTitleID(), FileSys::ContentRecordType::Control);
+        nsp->GetNCA(nsp->GetProgramTitleID(), FileSys::ContentRecordType::Control);
     if (control_nca == nullptr || control_nca->GetStatus() != ResultStatus::Success)
         return;
 
-    const auto romfs = FileSys::ExtractRomFS(control_nca->GetRomFS());
-    if (romfs == nullptr)
-        return;
-
-    for (const auto& language : FileSys::LANGUAGE_NAMES) {
-        icon_file = romfs->GetFile("icon_" + std::string(language) + ".dat");
-        if (icon_file != nullptr)
-            break;
-    }
-
-    const auto nacp_raw = romfs->GetFile("control.nacp");
-    if (nacp_raw == nullptr)
-        return;
-    nacp_file = std::make_shared<FileSys::NACP>(nacp_raw);
+    std::tie(nacp_file, icon_file) =
+        FileSys::PatchManager(nsp->GetProgramTitleID()).ParseControlNCA(control_nca);
 }
 
 AppLoader_NSP::~AppLoader_NSP() = default;
diff --git a/src/core/loader/xci.cpp b/src/core/loader/xci.cpp
index 75b998faaa477749c0475ad2183c88b3da67a938..16509229f5dcfbecb5264fc8649611b099dd4077 100644
--- a/src/core/loader/xci.cpp
+++ b/src/core/loader/xci.cpp
@@ -8,7 +8,9 @@
 #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/patch_manager.h"
 #include "core/file_sys/romfs.h"
+#include "core/file_sys/submission_package.h"
 #include "core/hle/kernel/process.h"
 #include "core/loader/nca.h"
 #include "core/loader/xci.h"
@@ -20,21 +22,13 @@ AppLoader_XCI::AppLoader_XCI(FileSys::VirtualFile file)
       nca_loader(std::make_unique<AppLoader_NCA>(xci->GetProgramNCAFile())) {
     if (xci->GetStatus() != ResultStatus::Success)
         return;
+
     const auto control_nca = xci->GetNCAByType(FileSys::NCAContentType::Control);
     if (control_nca == nullptr || control_nca->GetStatus() != ResultStatus::Success)
         return;
-    const auto romfs = FileSys::ExtractRomFS(control_nca->GetRomFS());
-    if (romfs == nullptr)
-        return;
-    for (const auto& language : FileSys::LANGUAGE_NAMES) {
-        icon_file = romfs->GetFile("icon_" + std::string(language) + ".dat");
-        if (icon_file != nullptr)
-            break;
-    }
-    const auto nacp_raw = romfs->GetFile("control.nacp");
-    if (nacp_raw == nullptr)
-        return;
-    nacp_file = std::make_shared<FileSys::NACP>(nacp_raw);
+
+    std::tie(nacp_file, icon_file) =
+        FileSys::PatchManager(xci->GetProgramTitleID()).ParseControlNCA(control_nca);
 }
 
 AppLoader_XCI::~AppLoader_XCI() = default;
diff --git a/src/core/telemetry_session.cpp b/src/core/telemetry_session.cpp
index 65571b94865e6cd1a1b2be34e67c71c5ab321a1a..3730e85b821518ac3b3acd2d9189ab2cd3929e33 100644
--- a/src/core/telemetry_session.cpp
+++ b/src/core/telemetry_session.cpp
@@ -7,6 +7,8 @@
 #include "common/file_util.h"
 
 #include "core/core.h"
+#include "core/file_sys/control_metadata.h"
+#include "core/file_sys/patch_manager.h"
 #include "core/loader/loader.h"
 #include "core/settings.h"
 #include "core/telemetry_session.h"
@@ -88,12 +90,28 @@ TelemetrySession::TelemetrySession() {
                             std::chrono::system_clock::now().time_since_epoch())
                             .count()};
     AddField(Telemetry::FieldType::Session, "Init_Time", init_time);
-    std::string program_name;
-    const Loader::ResultStatus res{System::GetInstance().GetAppLoader().ReadTitle(program_name)};
+
+    u64 program_id{};
+    const Loader::ResultStatus res{System::GetInstance().GetAppLoader().ReadProgramId(program_id)};
     if (res == Loader::ResultStatus::Success) {
-        AddField(Telemetry::FieldType::Session, "ProgramName", program_name);
+        AddField(Telemetry::FieldType::Session, "ProgramId", program_id);
+
+        std::string name;
+        System::GetInstance().GetAppLoader().ReadTitle(name);
+
+        if (name.empty()) {
+            auto [nacp, icon_file] = FileSys::PatchManager(program_id).GetControlMetadata();
+            if (nacp != nullptr)
+                name = nacp->GetApplicationName();
+        }
+
+        if (!name.empty())
+            AddField(Telemetry::FieldType::Session, "ProgramName", name);
     }
 
+    AddField(Telemetry::FieldType::Session, "ProgramFormat",
+             static_cast<u8>(System::GetInstance().GetAppLoader().GetFileType()));
+
     // Log application information
     Telemetry::AppendBuildInfo(field_collection);
 
diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game_list.cpp
index 3e2a5976bdaad2bd98da44183d38dc2f682e5a37..a3b84168438da0b106c370b683874302db88d4e9 100644
--- a/src/yuzu/game_list.cpp
+++ b/src/yuzu/game_list.cpp
@@ -21,6 +21,7 @@
 #include "core/file_sys/content_archive.h"
 #include "core/file_sys/control_metadata.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/romfs.h"
 #include "core/file_sys/vfs_real.h"
@@ -232,6 +233,7 @@ GameList::GameList(FileSys::VirtualFilesystem vfs, GMainWindow* parent)
     item_model->insertColumns(0, COLUMN_COUNT);
     item_model->setHeaderData(COLUMN_NAME, Qt::Horizontal, "Name");
     item_model->setHeaderData(COLUMN_COMPATIBILITY, Qt::Horizontal, "Compatibility");
+    item_model->setHeaderData(COLUMN_ADD_ONS, Qt::Horizontal, "Add-ons");
     item_model->setHeaderData(COLUMN_FILE_TYPE, Qt::Horizontal, "File type");
     item_model->setHeaderData(COLUMN_SIZE, Qt::Horizontal, "Size");
 
@@ -454,6 +456,25 @@ static QString FormatGameName(const std::string& physical_name) {
     return physical_name_as_qstring;
 }
 
+static QString FormatPatchNameVersions(const FileSys::PatchManager& patch_manager,
+                                       bool updatable = true) {
+    QString out;
+    for (const auto& kv : patch_manager.GetPatchVersionNames()) {
+        if (!updatable && kv.first == FileSys::PatchType::Update)
+            continue;
+
+        if (kv.second.empty()) {
+            out.append(fmt::format("{}\n", FileSys::FormatPatchTypeName(kv.first)).c_str());
+        } else {
+            out.append(fmt::format("{} ({})\n", FileSys::FormatPatchTypeName(kv.first), kv.second)
+                           .c_str());
+        }
+    }
+
+    out.chop(1);
+    return out;
+}
+
 void GameList::RefreshGameDirectory() {
     if (!UISettings::values.gamedir.isEmpty() && current_worker != nullptr) {
         LOG_INFO(Frontend, "Change detected in the games directory. Reloading game list.");
@@ -462,26 +483,14 @@ void GameList::RefreshGameDirectory() {
     }
 }
 
-static void GetMetadataFromControlNCA(const std::shared_ptr<FileSys::NCA>& nca,
+static void GetMetadataFromControlNCA(const FileSys::PatchManager& patch_manager,
+                                      const std::shared_ptr<FileSys::NCA>& nca,
                                       std::vector<u8>& icon, std::string& name) {
-    const auto control_dir = FileSys::ExtractRomFS(nca->GetRomFS());
-    if (control_dir == nullptr)
-        return;
-
-    const auto nacp_file = control_dir->GetFile("control.nacp");
-    if (nacp_file == nullptr)
-        return;
-    FileSys::NACP nacp(nacp_file);
-    name = nacp.GetApplicationName();
-
-    FileSys::VirtualFile icon_file = nullptr;
-    for (const auto& language : FileSys::LANGUAGE_NAMES) {
-        icon_file = control_dir->GetFile("icon_" + std::string(language) + ".dat");
-        if (icon_file != nullptr) {
-            icon = icon_file->ReadAllBytes();
-            break;
-        }
-    }
+    auto [nacp, icon_file] = patch_manager.ParseControlNCA(nca);
+    if (icon_file != nullptr)
+        icon = icon_file->ReadAllBytes();
+    if (nacp != nullptr)
+        name = nacp->GetApplicationName();
 }
 
 GameListWorker::GameListWorker(
@@ -492,7 +501,8 @@ GameListWorker::GameListWorker(
 
 GameListWorker::~GameListWorker() = default;
 
-void GameListWorker::AddInstalledTitlesToGameList(std::shared_ptr<FileSys::RegisteredCache> cache) {
+void GameListWorker::AddInstalledTitlesToGameList() {
+    const auto cache = Service::FileSystem::GetUnionContents();
     const auto installed_games = cache->ListEntriesFilter(FileSys::TitleType::Application,
                                                           FileSys::ContentRecordType::Program);
 
@@ -507,14 +517,25 @@ void GameListWorker::AddInstalledTitlesToGameList(std::shared_ptr<FileSys::Regis
         u64 program_id = 0;
         loader->ReadProgramId(program_id);
 
+        const FileSys::PatchManager patch{program_id};
         const auto& control = cache->GetEntry(game.title_id, FileSys::ContentRecordType::Control);
         if (control != nullptr)
-            GetMetadataFromControlNCA(control, icon, name);
+            GetMetadataFromControlNCA(patch, control, icon, name);
+
+        auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id);
+
+        // The game list uses this as compatibility number for untested games
+        QString compatibility("99");
+        if (it != compatibility_list.end())
+            compatibility = it->second.first;
+
         emit EntryReady({
             new GameListItemPath(
                 FormatGameName(file->GetFullPath()), icon, QString::fromStdString(name),
                 QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType())),
                 program_id),
+            new GameListItemCompat(compatibility),
+            new GameListItem(FormatPatchNameVersions(patch)),
             new GameListItem(
                 QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))),
             new GameListItemSize(file->GetSize()),
@@ -580,12 +601,14 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign
             std::string name = " ";
             const auto res3 = loader->ReadTitle(name);
 
+            const FileSys::PatchManager patch{program_id};
+
             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(nca, icon, name);
+                    GetMetadataFromControlNCA(patch, nca, icon, name);
                 }
             }
 
@@ -602,6 +625,7 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign
                     QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType())),
                     program_id),
                 new GameListItemCompat(compatibility),
+                new GameListItem(FormatPatchNameVersions(patch, loader->IsRomFSUpdatable())),
                 new GameListItem(
                     QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))),
                 new GameListItemSize(FileUtil::GetSize(physical_name)),
@@ -621,9 +645,7 @@ void GameListWorker::run() {
     stop_processing = false;
     watch_list.append(dir_path);
     FillControlMap(dir_path.toStdString());
-    AddInstalledTitlesToGameList(Service::FileSystem::GetUserNANDContents());
-    AddInstalledTitlesToGameList(Service::FileSystem::GetSystemNANDContents());
-    AddInstalledTitlesToGameList(Service::FileSystem::GetSDMCContents());
+    AddInstalledTitlesToGameList();
     AddFstEntriesToGameList(dir_path.toStdString(), deep_scan ? 256 : 0);
     nca_control_map.clear();
     emit Finished(watch_list);
diff --git a/src/yuzu/game_list.h b/src/yuzu/game_list.h
index 84731464a1bd1ef7eb3b600e01b18ea0d0218b7a..3fcb298ed15f2855740503ad9fad2ef06b2a0336 100644
--- a/src/yuzu/game_list.h
+++ b/src/yuzu/game_list.h
@@ -38,6 +38,7 @@ public:
     enum {
         COLUMN_NAME,
         COLUMN_COMPATIBILITY,
+        COLUMN_ADD_ONS,
         COLUMN_FILE_TYPE,
         COLUMN_SIZE,
         COLUMN_COUNT, // Number of columns
diff --git a/src/yuzu/game_list_p.h b/src/yuzu/game_list_p.h
index 4ddd8cd8877398b5c0a18bf5620f87ca55891968..a70a151c57b0309dde5be4e86aae7d05364b110a 100644
--- a/src/yuzu/game_list_p.h
+++ b/src/yuzu/game_list_p.h
@@ -239,7 +239,7 @@ private:
     const std::unordered_map<std::string, std::pair<QString, QString>>& compatibility_list;
     std::atomic_bool stop_processing;
 
-    void AddInstalledTitlesToGameList(std::shared_ptr<FileSys::RegisteredCache> cache);
+    void AddInstalledTitlesToGameList();
     void FillControlMap(const std::string& dir_path);
     void AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion = 0);
 };
diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp
index 56bd3ee2e0f161a3e09cd3614eb52bd36fc332ab..dbe5bd8a4f6bac6ce849d062bea58ab7dbdcd4ad 100644
--- a/src/yuzu/main.cpp
+++ b/src/yuzu/main.cpp
@@ -32,6 +32,8 @@
 #include "core/crypto/key_manager.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/patch_manager.h"
 #include "core/file_sys/registered_cache.h"
 #include "core/file_sys/savedata_factory.h"
 #include "core/file_sys/submission_package.h"
@@ -592,8 +594,16 @@ void GMainWindow::BootGame(const QString& filename) {
 
     std::string title_name;
     const auto res = Core::System::GetInstance().GetGameName(title_name);
-    if (res != Loader::ResultStatus::Success)
-        title_name = FileUtil::GetFilename(filename.toStdString());
+    if (res != Loader::ResultStatus::Success) {
+        const u64 program_id = Core::System::GetInstance().CurrentProcess()->program_id;
+
+        const auto [nacp, icon_file] = FileSys::PatchManager(program_id).GetControlMetadata();
+        if (nacp != nullptr)
+            title_name = nacp->GetApplicationName();
+
+        if (title_name.empty())
+            title_name = FileUtil::GetFilename(filename.toStdString());
+    }
 
     setWindowTitle(QString("yuzu %1| %4 | %2-%3")
                        .arg(Common::g_build_name, Common::g_scm_branch, Common::g_scm_desc,
@@ -868,7 +878,11 @@ void GMainWindow::OnMenuInstallToNAND() {
     } else {
         const auto nca = std::make_shared<FileSys::NCA>(
             vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read));
-        if (nca->GetStatus() != Loader::ResultStatus::Success) {
+        const auto id = nca->GetStatus();
+
+        // Game updates necessary are missing base RomFS
+        if (id != Loader::ResultStatus::Success &&
+            id != Loader::ResultStatus::ErrorMissingBKTRBaseRomFS) {
             failed();
             return;
         }