diff options
39 files changed, 1332 insertions, 196 deletions
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 54afa6a87..7ddc87539 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 72e4bed67..89ade5000 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 3ea60dbd0..296fad419 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 1bd3353e4..8218893b2 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 7cfb6f36b..79bfb6fec 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 0ea666cac..00eca52da 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 000000000..e0111bffc --- /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 000000000..0d9ad95f5 --- /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 000000000..40675de35 --- /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 000000000..28c7ae136 --- /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 cf6f77401..7361a67be 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 467ceeef1..f487b0cf0 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 66f9786e0..d9d90939e 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 f38ddc4f7..26b8f46cc 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 bde879861..11264878d 100644 --- a/src/core/file_sys/submission_package.cpp +++ b/src/core/file_sys/submission_package.cpp @@ -2,9 +2,15 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include <algorithm> +#include <cstring> +#include <string_view> + #include <fmt/ostream.h> -#include "common/assert.h" + #include "common/hex_util.h" +#include "common/logging/log.h" +#include "core/crypto/key_manager.h" #include "core/file_sys/content_archive.h" #include "core/file_sys/nca_metadata.h" #include "core/file_sys/partition_filesystem.h" @@ -13,8 +19,8 @@ namespace FileSys { NSP::NSP(VirtualFile file_) - : file(std::move(file_)), - pfs(std::make_shared<PartitionFilesystem>(file)), status{Loader::ResultStatus::Success} { + : file(std::move(file_)), status{Loader::ResultStatus::Success}, + pfs(std::make_shared<PartitionFilesystem>(file)) { if (pfs->GetStatus() != Loader::ResultStatus::Success) { status = pfs->GetStatus(); return; @@ -60,8 +66,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/file_sys/submission_package.h b/src/core/file_sys/submission_package.h index 0292164f9..1120a4920 100644 --- a/src/core/file_sys/submission_package.h +++ b/src/core/file_sys/submission_package.h @@ -4,20 +4,23 @@ #pragma once -#include <array> #include <map> +#include <memory> #include <vector> #include "common/common_types.h" -#include "common/swap.h" -#include "core/file_sys/content_archive.h" -#include "core/file_sys/romfs_factory.h" #include "core/file_sys/vfs.h" -#include "core/loader/loader.h" + +namespace Loader { +enum class ResultStatus : u16; +} namespace FileSys { +class NCA; class PartitionFilesystem; +enum class ContentRecordType : u8; + class NSP : public ReadOnlyVfsDirectory { public: explicit NSP(VirtualFile file); diff --git a/src/core/hle/service/filesystem/filesystem.cpp b/src/core/hle/service/filesystem/filesystem.cpp index a4426af96..04c9d750f 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 9ba0e2eab..793a7b06f 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 1ae4bb656..2b8f78136 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 b20804f75..8a0dc1b1e 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 446adf557..fa43a2650 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 be66b2257..843c4bb91 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 c036a8a1c..6aaffae59 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 326f84857..10be197c4 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 77026b850..bb89a9da3 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 bb01c9e25..96d2de305 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 7c06239f2..291a9876d 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 75b998faa..16509229f 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 65571b948..3730e85b8 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/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt index aa5bc3bbe..1982b76c4 100644 --- a/src/video_core/CMakeLists.txt +++ b/src/video_core/CMakeLists.txt @@ -22,6 +22,7 @@ add_library(video_core STATIC rasterizer_interface.h renderer_base.cpp renderer_base.h + renderer_opengl/gl_buffer_cache.cpp renderer_opengl/gl_rasterizer.cpp renderer_opengl/gl_rasterizer.h renderer_opengl/gl_rasterizer_cache.cpp diff --git a/src/video_core/renderer_opengl/gl_buffer_cache.cpp b/src/video_core/renderer_opengl/gl_buffer_cache.cpp new file mode 100644 index 000000000..c85fbd306 --- /dev/null +++ b/src/video_core/renderer_opengl/gl_buffer_cache.cpp @@ -0,0 +1,90 @@ +// Copyright 2018 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "common/alignment.h" +#include "common/assert.h" +#include "core/core.h" +#include "core/memory.h" +#include "video_core/renderer_opengl/gl_buffer_cache.h" + +namespace OpenGL { + +OGLBufferCache::OGLBufferCache(size_t size) : stream_buffer(GL_ARRAY_BUFFER, size) {} + +GLintptr OGLBufferCache::UploadMemory(Tegra::GPUVAddr gpu_addr, size_t size, size_t alignment, + bool cache) { + auto& memory_manager = Core::System::GetInstance().GPU().MemoryManager(); + const boost::optional<VAddr> cpu_addr{memory_manager.GpuToCpuAddress(gpu_addr)}; + + // Cache management is a big overhead, so only cache entries with a given size. + // TODO: Figure out which size is the best for given games. + cache &= size >= 2048; + + if (cache) { + auto entry = TryGet(*cpu_addr); + if (entry) { + if (entry->size >= size && entry->alignment == alignment) { + return entry->offset; + } + Unregister(entry); + } + } + + AlignBuffer(alignment); + GLintptr uploaded_offset = buffer_offset; + + Memory::ReadBlock(*cpu_addr, buffer_ptr, size); + + buffer_ptr += size; + buffer_offset += size; + + if (cache) { + auto entry = std::make_shared<CachedBufferEntry>(); + entry->offset = uploaded_offset; + entry->size = size; + entry->alignment = alignment; + entry->addr = *cpu_addr; + Register(entry); + } + + return uploaded_offset; +} + +GLintptr OGLBufferCache::UploadHostMemory(const void* raw_pointer, size_t size, size_t alignment) { + AlignBuffer(alignment); + std::memcpy(buffer_ptr, raw_pointer, size); + GLintptr uploaded_offset = buffer_offset; + + buffer_ptr += size; + buffer_offset += size; + return uploaded_offset; +} + +void OGLBufferCache::Map(size_t max_size) { + bool invalidate; + std::tie(buffer_ptr, buffer_offset_base, invalidate) = + stream_buffer.Map(static_cast<GLsizeiptr>(max_size), 4); + buffer_offset = buffer_offset_base; + + if (invalidate) { + InvalidateAll(); + } +} +void OGLBufferCache::Unmap() { + stream_buffer.Unmap(buffer_offset - buffer_offset_base); +} + +GLuint OGLBufferCache::GetHandle() { + return stream_buffer.GetHandle(); +} + +void OGLBufferCache::AlignBuffer(size_t alignment) { + // Align the offset, not the mapped pointer + GLintptr offset_aligned = + static_cast<GLintptr>(Common::AlignUp(static_cast<size_t>(buffer_offset), alignment)); + buffer_ptr += offset_aligned - buffer_offset; + buffer_offset = offset_aligned; +} + +} // namespace OpenGL diff --git a/src/video_core/renderer_opengl/gl_buffer_cache.h b/src/video_core/renderer_opengl/gl_buffer_cache.h new file mode 100644 index 000000000..9c7ad27e6 --- /dev/null +++ b/src/video_core/renderer_opengl/gl_buffer_cache.h @@ -0,0 +1,57 @@ +// Copyright 2018 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <memory> +#include <unordered_map> + +#include "common/common_types.h" +#include "video_core/rasterizer_cache.h" +#include "video_core/renderer_opengl/gl_resource_manager.h" +#include "video_core/renderer_opengl/gl_stream_buffer.h" + +namespace OpenGL { + +struct CachedBufferEntry final { + VAddr GetAddr() const { + return addr; + } + + size_t GetSizeInBytes() const { + return size; + } + + VAddr addr; + size_t size; + GLintptr offset; + size_t alignment; +}; + +class OGLBufferCache final : public RasterizerCache<std::shared_ptr<CachedBufferEntry>> { +public: + OGLBufferCache(size_t size); + + GLintptr UploadMemory(Tegra::GPUVAddr gpu_addr, size_t size, size_t alignment = 4, + bool cache = true); + + GLintptr UploadHostMemory(const void* raw_pointer, size_t size, size_t alignment = 4); + + void Map(size_t max_size); + void Unmap(); + + GLuint GetHandle(); + +protected: + void AlignBuffer(size_t alignment); + +private: + OGLStreamBuffer stream_buffer; + + u8* buffer_ptr; + GLintptr buffer_offset; + GLintptr buffer_offset_base; +}; + +} // namespace OpenGL diff --git a/src/video_core/renderer_opengl/gl_rasterizer.cpp b/src/video_core/renderer_opengl/gl_rasterizer.cpp index 7ee3f2ae7..c66a18155 100644 --- a/src/video_core/renderer_opengl/gl_rasterizer.cpp +++ b/src/video_core/renderer_opengl/gl_rasterizer.cpp @@ -43,7 +43,7 @@ MICROPROFILE_DEFINE(OpenGL_Blits, "OpenGL", "Blits", MP_RGB(128, 128, 192)); MICROPROFILE_DEFINE(OpenGL_CacheManagement, "OpenGL", "Cache Mgmt", MP_RGB(100, 255, 100)); RasterizerOpenGL::RasterizerOpenGL(Core::Frontend::EmuWindow& window, ScreenInfo& info) - : emu_window{window}, screen_info{info}, stream_buffer(GL_ARRAY_BUFFER, STREAM_BUFFER_SIZE) { + : emu_window{window}, screen_info{info}, buffer_cache(STREAM_BUFFER_SIZE) { // Create sampler objects for (size_t i = 0; i < texture_samplers.size(); ++i) { texture_samplers[i].Create(); @@ -83,14 +83,14 @@ RasterizerOpenGL::RasterizerOpenGL(Core::Frontend::EmuWindow& window, ScreenInfo hw_vao.Create(); - state.draw.vertex_buffer = stream_buffer.GetHandle(); + state.draw.vertex_buffer = buffer_cache.GetHandle(); shader_program_manager = std::make_unique<GLShader::ProgramManager>(); state.draw.shader_program = 0; state.draw.vertex_array = hw_vao.handle; state.Apply(); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, stream_buffer.GetHandle()); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffer_cache.GetHandle()); glEnable(GL_BLEND); @@ -101,14 +101,13 @@ RasterizerOpenGL::RasterizerOpenGL(Core::Frontend::EmuWindow& window, ScreenInfo RasterizerOpenGL::~RasterizerOpenGL() {} -std::pair<u8*, GLintptr> RasterizerOpenGL::SetupVertexArrays(u8* array_ptr, - GLintptr buffer_offset) { +void RasterizerOpenGL::SetupVertexArrays() { MICROPROFILE_SCOPE(OpenGL_VAO); const auto& gpu = Core::System::GetInstance().GPU().Maxwell3D(); const auto& regs = gpu.regs; state.draw.vertex_array = hw_vao.handle; - state.draw.vertex_buffer = stream_buffer.GetHandle(); + state.draw.vertex_buffer = buffer_cache.GetHandle(); state.Apply(); // Upload all guest vertex arrays sequentially to our buffer @@ -127,12 +126,10 @@ std::pair<u8*, GLintptr> RasterizerOpenGL::SetupVertexArrays(u8* array_ptr, ASSERT(end > start); u64 size = end - start + 1; - GLintptr vertex_buffer_offset; - std::tie(array_ptr, buffer_offset, vertex_buffer_offset) = - UploadMemory(array_ptr, buffer_offset, start, size); + GLintptr vertex_buffer_offset = buffer_cache.UploadMemory(start, size); // Bind the vertex array to the buffer at the current offset. - glBindVertexBuffer(index, stream_buffer.GetHandle(), vertex_buffer_offset, + glBindVertexBuffer(index, buffer_cache.GetHandle(), vertex_buffer_offset, vertex_array.stride); if (regs.instanced_arrays.IsInstancingEnabled(index) && vertex_array.divisor != 0) { @@ -177,11 +174,9 @@ std::pair<u8*, GLintptr> RasterizerOpenGL::SetupVertexArrays(u8* array_ptr, } glVertexAttribBinding(index, attrib.buffer); } - - return {array_ptr, buffer_offset}; } -std::pair<u8*, GLintptr> RasterizerOpenGL::SetupShaders(u8* buffer_ptr, GLintptr buffer_offset) { +void RasterizerOpenGL::SetupShaders() { MICROPROFILE_SCOPE(OpenGL_Shader); auto& gpu = Core::System::GetInstance().GPU().Maxwell3D(); @@ -199,21 +194,15 @@ std::pair<u8*, GLintptr> RasterizerOpenGL::SetupShaders(u8* buffer_ptr, GLintptr continue; } - std::tie(buffer_ptr, buffer_offset) = - AlignBuffer(buffer_ptr, buffer_offset, static_cast<size_t>(uniform_buffer_alignment)); - const size_t stage{index == 0 ? 0 : index - 1}; // Stage indices are 0 - 5 GLShader::MaxwellUniformData ubo{}; ubo.SetFromRegs(gpu.state.shader_stages[stage]); - std::memcpy(buffer_ptr, &ubo, sizeof(ubo)); + GLintptr offset = buffer_cache.UploadHostMemory( + &ubo, sizeof(ubo), static_cast<size_t>(uniform_buffer_alignment)); // Bind the buffer - glBindBufferRange(GL_UNIFORM_BUFFER, stage, stream_buffer.GetHandle(), buffer_offset, - sizeof(ubo)); - - buffer_ptr += sizeof(ubo); - buffer_offset += sizeof(ubo); + glBindBufferRange(GL_UNIFORM_BUFFER, stage, buffer_cache.GetHandle(), offset, sizeof(ubo)); Shader shader{shader_cache.GetStageProgram(program)}; @@ -234,9 +223,8 @@ std::pair<u8*, GLintptr> RasterizerOpenGL::SetupShaders(u8* buffer_ptr, GLintptr } // Configure the const buffers for this shader stage. - std::tie(buffer_ptr, buffer_offset, current_constbuffer_bindpoint) = - SetupConstBuffers(buffer_ptr, buffer_offset, static_cast<Maxwell::ShaderStage>(stage), - shader, current_constbuffer_bindpoint); + current_constbuffer_bindpoint = SetupConstBuffers(static_cast<Maxwell::ShaderStage>(stage), + shader, current_constbuffer_bindpoint); // Configure the textures for this shader stage. current_texture_bindpoint = SetupTextures(static_cast<Maxwell::ShaderStage>(stage), shader, @@ -250,8 +238,6 @@ std::pair<u8*, GLintptr> RasterizerOpenGL::SetupShaders(u8* buffer_ptr, GLintptr } shader_program_manager->UseTrivialGeometryShader(); - - return {buffer_ptr, buffer_offset}; } size_t RasterizerOpenGL::CalculateVertexArraysSize() const { @@ -439,31 +425,6 @@ void RasterizerOpenGL::Clear() { glClear(clear_mask); } -std::pair<u8*, GLintptr> RasterizerOpenGL::AlignBuffer(u8* buffer_ptr, GLintptr buffer_offset, - size_t alignment) { - // Align the offset, not the mapped pointer - GLintptr offset_aligned = - static_cast<GLintptr>(Common::AlignUp(static_cast<size_t>(buffer_offset), alignment)); - return {buffer_ptr + (offset_aligned - buffer_offset), offset_aligned}; -} - -std::tuple<u8*, GLintptr, GLintptr> RasterizerOpenGL::UploadMemory(u8* buffer_ptr, - GLintptr buffer_offset, - Tegra::GPUVAddr gpu_addr, - size_t size, size_t alignment) { - std::tie(buffer_ptr, buffer_offset) = AlignBuffer(buffer_ptr, buffer_offset, alignment); - GLintptr uploaded_offset = buffer_offset; - - auto& memory_manager = Core::System::GetInstance().GPU().MemoryManager(); - const boost::optional<VAddr> cpu_addr{memory_manager.GpuToCpuAddress(gpu_addr)}; - Memory::ReadBlock(*cpu_addr, buffer_ptr, size); - - buffer_ptr += size; - buffer_offset += size; - - return {buffer_ptr, buffer_offset, uploaded_offset}; -} - void RasterizerOpenGL::DrawArrays() { if (accelerate_draw == AccelDraw::Disabled) return; @@ -489,7 +450,7 @@ void RasterizerOpenGL::DrawArrays() { const bool is_indexed = accelerate_draw == AccelDraw::Indexed; const u64 index_buffer_size{regs.index_array.count * regs.index_array.FormatSizeInBytes()}; - state.draw.vertex_buffer = stream_buffer.GetHandle(); + state.draw.vertex_buffer = buffer_cache.GetHandle(); state.Apply(); size_t buffer_size = CalculateVertexArraysSize(); @@ -506,25 +467,21 @@ void RasterizerOpenGL::DrawArrays() { // Add space for at least 18 constant buffers buffer_size += Maxwell::MaxConstBuffers * (MaxConstbufferSize + uniform_buffer_alignment); - u8* buffer_ptr; - GLintptr buffer_offset; - std::tie(buffer_ptr, buffer_offset, std::ignore) = - stream_buffer.Map(static_cast<GLsizeiptr>(buffer_size), 4); - u8* buffer_ptr_base = buffer_ptr; + buffer_cache.Map(buffer_size); - std::tie(buffer_ptr, buffer_offset) = SetupVertexArrays(buffer_ptr, buffer_offset); + SetupVertexArrays(); // If indexed mode, copy the index buffer GLintptr index_buffer_offset = 0; if (is_indexed) { MICROPROFILE_SCOPE(OpenGL_Index); - std::tie(buffer_ptr, buffer_offset, index_buffer_offset) = UploadMemory( - buffer_ptr, buffer_offset, regs.index_array.StartAddress(), index_buffer_size); + index_buffer_offset = + buffer_cache.UploadMemory(regs.index_array.StartAddress(), index_buffer_size); } - std::tie(buffer_ptr, buffer_offset) = SetupShaders(buffer_ptr, buffer_offset); + SetupShaders(); - stream_buffer.Unmap(buffer_ptr - buffer_ptr_base); + buffer_cache.Unmap(); shader_program_manager->ApplyTo(state); state.Apply(); @@ -569,6 +526,7 @@ void RasterizerOpenGL::InvalidateRegion(VAddr addr, u64 size) { MICROPROFILE_SCOPE(OpenGL_CacheManagement); res_cache.InvalidateRegion(addr, size); shader_cache.InvalidateRegion(addr, size); + buffer_cache.InvalidateRegion(addr, size); } void RasterizerOpenGL::FlushAndInvalidateRegion(VAddr addr, u64 size) { @@ -658,11 +616,8 @@ void RasterizerOpenGL::SamplerInfo::SyncWithConfig(const Tegra::Texture::TSCEntr } } -std::tuple<u8*, GLintptr, u32> RasterizerOpenGL::SetupConstBuffers(u8* buffer_ptr, - GLintptr buffer_offset, - Maxwell::ShaderStage stage, - Shader& shader, - u32 current_bindpoint) { +u32 RasterizerOpenGL::SetupConstBuffers(Maxwell::ShaderStage stage, Shader& shader, + u32 current_bindpoint) { MICROPROFILE_SCOPE(OpenGL_UBO); const auto& gpu = Core::System::GetInstance().GPU(); const auto& maxwell3d = gpu.Maxwell3D(); @@ -699,13 +654,11 @@ std::tuple<u8*, GLintptr, u32> RasterizerOpenGL::SetupConstBuffers(u8* buffer_pt size = Common::AlignUp(size, sizeof(GLvec4)); ASSERT_MSG(size <= MaxConstbufferSize, "Constbuffer too big"); - GLintptr const_buffer_offset; - std::tie(buffer_ptr, buffer_offset, const_buffer_offset) = - UploadMemory(buffer_ptr, buffer_offset, buffer.address, size, - static_cast<size_t>(uniform_buffer_alignment)); + GLintptr const_buffer_offset = buffer_cache.UploadMemory( + buffer.address, size, static_cast<size_t>(uniform_buffer_alignment)); glBindBufferRange(GL_UNIFORM_BUFFER, current_bindpoint + bindpoint, - stream_buffer.GetHandle(), const_buffer_offset, size); + buffer_cache.GetHandle(), const_buffer_offset, size); // Now configure the bindpoint of the buffer inside the shader glUniformBlockBinding(shader->GetProgramHandle(), @@ -715,7 +668,7 @@ std::tuple<u8*, GLintptr, u32> RasterizerOpenGL::SetupConstBuffers(u8* buffer_pt state.Apply(); - return {buffer_ptr, buffer_offset, current_bindpoint + static_cast<u32>(entries.size())}; + return current_bindpoint + static_cast<u32>(entries.size()); } u32 RasterizerOpenGL::SetupTextures(Maxwell::ShaderStage stage, Shader& shader, u32 current_unit) { diff --git a/src/video_core/renderer_opengl/gl_rasterizer.h b/src/video_core/renderer_opengl/gl_rasterizer.h index 30045ebff..4c4b084b8 100644 --- a/src/video_core/renderer_opengl/gl_rasterizer.h +++ b/src/video_core/renderer_opengl/gl_rasterizer.h @@ -18,7 +18,9 @@ #include "common/common_types.h" #include "video_core/engines/maxwell_3d.h" #include "video_core/memory_manager.h" +#include "video_core/rasterizer_cache.h" #include "video_core/rasterizer_interface.h" +#include "video_core/renderer_opengl/gl_buffer_cache.h" #include "video_core/renderer_opengl/gl_rasterizer_cache.h" #include "video_core/renderer_opengl/gl_resource_manager.h" #include "video_core/renderer_opengl/gl_shader_cache.h" @@ -109,9 +111,8 @@ private: * @param current_bindpoint The offset at which to start counting new buffer bindpoints. * @returns The next available bindpoint for use in the next shader stage. */ - std::tuple<u8*, GLintptr, u32> SetupConstBuffers( - u8* buffer_ptr, GLintptr buffer_offset, Tegra::Engines::Maxwell3D::Regs::ShaderStage stage, - Shader& shader, u32 current_bindpoint); + u32 SetupConstBuffers(Tegra::Engines::Maxwell3D::Regs::ShaderStage stage, Shader& shader, + u32 current_bindpoint); /* * Configures the current textures to use for the draw command. @@ -173,22 +174,16 @@ private: std::array<SamplerInfo, GLShader::NumTextureSamplers> texture_samplers; static constexpr size_t STREAM_BUFFER_SIZE = 128 * 1024 * 1024; - OGLStreamBuffer stream_buffer; + OGLBufferCache buffer_cache; OGLBuffer uniform_buffer; OGLFramebuffer framebuffer; GLint uniform_buffer_alignment; size_t CalculateVertexArraysSize() const; - std::pair<u8*, GLintptr> SetupVertexArrays(u8* array_ptr, GLintptr buffer_offset); + void SetupVertexArrays(); - std::pair<u8*, GLintptr> SetupShaders(u8* buffer_ptr, GLintptr buffer_offset); - - std::pair<u8*, GLintptr> AlignBuffer(u8* buffer_ptr, GLintptr buffer_offset, size_t alignment); - - std::tuple<u8*, GLintptr, GLintptr> UploadMemory(u8* buffer_ptr, GLintptr buffer_offset, - Tegra::GPUVAddr gpu_addr, size_t size, - size_t alignment = 4); + void SetupShaders(); enum class AccelDraw { Disabled, Arrays, Indexed }; AccelDraw accelerate_draw = AccelDraw::Disabled; diff --git a/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp b/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp index 1965ab7d5..f6b2c5a86 100644 --- a/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp +++ b/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp @@ -690,7 +690,7 @@ SurfaceSurfaceRect_Tuple RasterizerCacheOpenGL::GetFramebufferSurfaces(bool usin const auto& regs = Core::System::GetInstance().GPU().Maxwell3D().regs; // TODO(bunnei): This is hard corded to use just the first render buffer - LOG_WARNING(Render_OpenGL, "hard-coded for render target 0!"); + LOG_TRACE(Render_OpenGL, "hard-coded for render target 0!"); // get color and depth surfaces SurfaceParams color_params{}; diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game_list.cpp index 3e2a5976b..a3b841684 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 84731464a..3fcb298ed 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 4ddd8cd88..a70a151c5 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 56bd3ee2e..dbe5bd8a4 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; } |