From de7e6e01d43e8cf2ec193d74fa053b0d7949955f Mon Sep 17 00:00:00 2001 From: John Date: Sun, 18 Oct 2026 00:17:28 +0200 Subject: [PATCH 01/14] Revert [input_common/joycon] Enumerate SDL devices iff SDL signals change (#4486) (#4545) Due to causing the platform windows to crash when a controller is activated. - [x] I have read and followed the [Contribution Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/CONTRIBUTING.md#code-contributions). - [x] I have read and followed the [AI Policy](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/AI.md). - [x] I have read and followed the [Coding Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/Coding.md) to the best of my ability. ------------------- Co-authored-by: Your Name Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4545 Reviewed-by: lizzie Reviewed-by: Maufeat --- src/input_common/drivers/joycon.cpp | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/input_common/drivers/joycon.cpp b/src/input_common/drivers/joycon.cpp index 047e7eb6d8..ded92d3f1d 100644 --- a/src/input_common/drivers/joycon.cpp +++ b/src/input_common/drivers/joycon.cpp @@ -83,21 +83,20 @@ void Joycons::ScanThread(std::stop_token stop_token) { constexpr u16 nintendo_vendor_id = 0x057e; Common::SetCurrentThreadName("JoyconScanThread"); - auto last_ret = 0u; do { - auto ret = SDL_hid_device_change_count(); - if (last_ret == 0u || last_ret != ret) { - last_ret = ret; - SDL_hid_device_info* devs = SDL_hid_enumerate(nintendo_vendor_id, 0x0); - for (auto* cur_dev = devs; cur_dev; cur_dev = cur_dev->next) { - if (IsDeviceNew(cur_dev)) { - LOG_DEBUG(Input, "Device Found,type : {:04X} {:04X}", cur_dev->vendor_id, cur_dev->product_id); - RegisterNewDevice(cur_dev); - } - cur_dev = cur_dev->next; + SDL_hid_device_info* devs = SDL_hid_enumerate(nintendo_vendor_id, 0x0); + SDL_hid_device_info* cur_dev = devs; + + while (cur_dev) { + if (IsDeviceNew(cur_dev)) { + LOG_DEBUG(Input, "Device Found,type : {:04X} {:04X}", cur_dev->vendor_id, + cur_dev->product_id); + RegisterNewDevice(cur_dev); } - SDL_hid_free_enumeration(devs); + cur_dev = cur_dev->next; } + + SDL_hid_free_enumeration(devs); } while (Common::StoppableTimedWait(stop_token, std::chrono::seconds{5})); } From 989768aa46720e3fcab9e2111e3ddd70819c382b Mon Sep 17 00:00:00 2001 From: John Date: Mon, 19 Oct 2026 15:58:04 +0200 Subject: [PATCH 02/14] Fix [core, gpu, threads] Multithreading refactor (#4254) (#4548) Performance loss regression fixed in Link's Awakening, Castlevania, Super Mario RPG, etc Tested by Mr. Knobs on Android and myself on Linux Multicore ON may be fixed for some users using Intel CPUs. Games include: FF Tactics, Shredder's Revenge, Unity Engine Games. - [x] I have read and followed the [Contribution Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/CONTRIBUTING.md#code-contributions). - [x] I have read and followed the [AI Policy](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/AI.md). - [x] I have read and followed the [Coding Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/Coding.md) to the best of my ability. ------------------- Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4548 Reviewed-by: Maufeat Reviewed-by: lizzie --- .../service/nvnflinger/hardware_composer.cpp | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/core/hle/service/nvnflinger/hardware_composer.cpp b/src/core/hle/service/nvnflinger/hardware_composer.cpp index 5decb791e1..9ac56e17bb 100644 --- a/src/core/hle/service/nvnflinger/hardware_composer.cpp +++ b/src/core/hle/service/nvnflinger/hardware_composer.cpp @@ -57,8 +57,21 @@ u32 HardwareComposer::ComposeLocked(f32* out_speed_scale, Display& display, // Set default speed limit to 100%. *out_speed_scale = 1.0f; + // If no layers are visible, skip the logic. Acquiring and holding buffers here would + // consume the producer's queue without ever presenting. + bool any_visible = false; + for (auto& layer : display.stack.layers) { + if (layer->visible) { + any_visible = true; + break; + } + } + if (!any_visible) { + return 1; + } + + // Wait for the previous composite, bounding the number of in-flight composites. nvdisp.WaitForComposite(); - this->ReleaseFramebuffersLocked(display); // Determine the number of vsync periods to wait before composing again. std::optional swap_interval{}; @@ -152,6 +165,13 @@ u32 HardwareComposer::ComposeLocked(f32* out_speed_scale, Display& display, // Advance by 1 frame (60 FPS compositing) m_frame_number += 1; + // Release framebuffers once the frame has been handed to the compositor. This happens + // within the same vsync period as the acquire (release_frame_number is checked against + // the advanced counter), so the producer gets its buffers back immediately instead of + // losing a queue slot for a whole frame. + // TODO: support release fences; releasing before the composite has finished can tear. + this->ReleaseFramebuffersLocked(display); + return 1; } From 6f5a89e9b526ec2239f918995a8a7f0f8359738a Mon Sep 17 00:00:00 2001 From: PavelBARABANOV Date: Sun, 18 Oct 2026 13:14:14 +0300 Subject: [PATCH 03/14] [core] Support block-compressed NSZ and XCZ --- .../main/java/org/yuzu/yuzu_emu/model/Game.kt | 2 +- .../org/yuzu/yuzu_emu/utils/GameHelper.kt | 2 +- src/common/zstd_compression.cpp | 9 + src/common/zstd_compression.h | 14 + src/core/CMakeLists.txt | 2 + src/core/file_sys/ncz_decompressor.cpp | 279 ++++++++++++++++++ src/core/file_sys/ncz_decompressor.h | 31 ++ src/core/file_sys/registered_cache.cpp | 37 +++ src/core/file_sys/submission_package.cpp | 13 + src/core/loader/loader.cpp | 17 +- src/core/loader/loader.h | 2 + src/frontend_common/content_manager.h | 4 +- src/qt_common/game_list/worker.cpp | 10 +- src/qt_common/qt_common.cpp | 3 +- 14 files changed, 415 insertions(+), 10 deletions(-) create mode 100644 src/core/file_sys/ncz_decompressor.cpp create mode 100644 src/core/file_sys/ncz_decompressor.h diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt index 6717894c93..f5f0cda0e9 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt @@ -100,7 +100,7 @@ class Game( companion object { val extensions: Set = HashSet( - listOf("xci", "nsp", "nca", "nro") + listOf("xci", "nsp", "nca", "nro", "nsz", "xcz") ) } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt index 411b764e36..bb2337a310 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt @@ -109,7 +109,7 @@ object GameHelper { // File extensions considered as external content, buuut should // be done better imo. - private val externalContentExtensions = setOf("nsp", "xci") + private val externalContentExtensions = setOf("nsp", "xci", "nsz", "xcz") private fun scanContentContainersRecursive( files: Array, diff --git a/src/common/zstd_compression.cpp b/src/common/zstd_compression.cpp index cb6ec171b5..7665482ee6 100644 --- a/src/common/zstd_compression.cpp +++ b/src/common/zstd_compression.cpp @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -46,4 +49,10 @@ std::vector DecompressDataZSTD(std::span compressed) { return decompressed; } +std::size_t DecompressDataZSTD(std::span compressed, std::span output) { + const std::size_t result = + ZSTD_decompress(output.data(), output.size(), compressed.data(), compressed.size()); + return ZSTD_isError(result) ? 0 : result; +} + } // namespace Common::Compression diff --git a/src/common/zstd_compression.h b/src/common/zstd_compression.h index a5ab2d05bd..819d7d201a 100644 --- a/src/common/zstd_compression.h +++ b/src/common/zstd_compression.h @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -42,4 +45,15 @@ namespace Common::Compression { */ [[nodiscard]] std::vector DecompressDataZSTD(std::span compressed); +/** + * Decompresses a single Zstandard frame into a caller-provided buffer of known size. + * + * @param compressed the compressed source memory region. + * @param output the destination buffer. Its size is the maximum number of bytes to write. + * + * @return the number of bytes written, or 0 if the frame is invalid or doesn't fit. An empty + * frame also returns 0. + */ +[[nodiscard]] std::size_t DecompressDataZSTD(std::span compressed, std::span output); + } // namespace Common::Compression diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 31f9931d3a..31e5e75858 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -122,6 +122,8 @@ add_library(core STATIC file_sys/kernel_executable.h file_sys/nca_metadata.cpp file_sys/nca_metadata.h + file_sys/ncz_decompressor.cpp + file_sys/ncz_decompressor.h file_sys/partition_filesystem.cpp file_sys/partition_filesystem.h file_sys/patch_manager.cpp diff --git a/src/core/file_sys/ncz_decompressor.cpp b/src/core/file_sys/ncz_decompressor.cpp new file mode 100644 index 0000000000..1dbd3ebd40 --- /dev/null +++ b/src/core/file_sys/ncz_decompressor.cpp @@ -0,0 +1,279 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common/common_types.h" +#include "common/logging.h" +#include "common/zstd_compression.h" +#include "core/crypto/aes_util.h" +#include "core/crypto/key_manager.h" +#include "core/file_sys/ncz_decompressor.h" +#include "core/file_sys/vfs/vfs.h" + +namespace Core::Crypto { + +namespace { + +constexpr u64 HeaderSize = 0x4000; +constexpr std::size_t SectionWire = 0x40; +constexpr std::size_t BlockFixed = 0x18; +constexpr std::array SectionsMagic{'N', 'C', 'Z', 'S', 'E', 'C', 'T', 'N'}; +constexpr std::array BlockMagic{'N', 'C', 'Z', 'B', 'L', 'O', 'C', 'K'}; + +struct Section { + s64 offset, size; + u64 crypto_type; + Key128 key, counter; + bool Ctr() const { return crypto_type == 3 || crypto_type == 4; } +}; + +struct BlockHeader { + u32 exponent; + u64 decompressed_size; + std::vector sizes; +}; + +u64 Le(const u8* p, std::size_t n) { + u64 v = 0; + for (std::size_t i = n; i-- > 0;) v = (v << 8) | p[i]; + return v; +} + +// Mirrors nsz AESCTR.seek(): upper 8 bytes = stored counter, lower 8 = absolute NCA offset / 16. +void SetIv(Key128& iv, const Key128& base, u64 off) { + iv = base; + u64 blk = off >> 4; + for (std::size_t i = 0; i < 8; ++i) { iv[15 - i] = static_cast(blk & 0xFF); blk >>= 8; } +} + +// Parse header + section table + optional block header. Returns payload_start. +std::optional, std::optional>> ParseHeader( + const FileSys::VirtualFile& f, u64& payload_start) { + const u64 fsize = f->GetSize(); + if (fsize < HeaderSize + 16) return std::nullopt; + + const auto head = f->ReadBytes(16, HeaderSize); + if (head.size() != 16 || !std::equal(head.begin(), head.begin() + 8, SectionsMagic.begin())) + return std::nullopt; + const u64 count = Le(head.data() + 8, 8); + if (count == 0 || count > 64 || count * SectionWire > fsize - HeaderSize - 16) + return std::nullopt; + + const auto table = f->ReadBytes(count * SectionWire, HeaderSize + 16); + if (table.size() != count * SectionWire) return std::nullopt; + + std::vector
sections; + sections.reserve(count + 1); + for (u64 i = 0; i < count; ++i) { + const u8* e = table.data() + i * SectionWire; + Section s{}; + s.offset = static_cast(Le(e, 8)); + s.size = static_cast(Le(e + 8, 8)); + s.crypto_type = Le(e + 16, 8); + std::memcpy(s.key.data(), e + 32, 16); + std::memcpy(s.counter.data(), e + 48, 16); + sections.push_back(s); + } + + const u64 block_start = HeaderSize + 16 + count * SectionWire; + const auto fixed = f->ReadBytes(BlockFixed, block_start); + std::optional bh; + if (fixed.size() == BlockFixed && + std::equal(fixed.begin(), fixed.begin() + 8, BlockMagic.begin())) { + BlockHeader b{}; + b.exponent = fixed[11]; + const s32 n = static_cast(Le(fixed.data() + 12, 4)); + if (n <= 0) return std::nullopt; + b.decompressed_size = Le(fixed.data() + 16, 8); + const auto sizes = f->ReadBytes(static_cast(n) * 4, block_start + BlockFixed); + if (sizes.size() != static_cast(n) * 4) return std::nullopt; + b.sizes.resize(n); + for (s32 i = 0; i < n; ++i) b.sizes[i] = static_cast(Le(sizes.data() + i * 4, 4)); + payload_start = block_start + BlockFixed + static_cast(n) * 4; + bh = std::move(b); + } else { + payload_start = block_start; + } + + if (!sections.empty() && sections[0].offset > static_cast(HeaderSize)) { + Section fake{}; + fake.offset = static_cast(HeaderSize); + fake.size = sections[0].offset - static_cast(HeaderSize); + sections.insert(sections.begin(), fake); + } + return std::make_pair(std::move(sections), std::move(bh)); +} + +// Sequential reader over independently-compressed blocks, one block cached (like nsz_rs). +class BlockReader { +public: + BlockReader(FileSys::VirtualFile f, const BlockHeader& bh, u64 payload_start) + : source{std::move(f)}, block_size{1ULL << bh.exponent}, sizes{bh.sizes}, + decompressed_size{bh.decompressed_size} { + if (bh.exponent < 14 || bh.exponent > 32 || sizes.empty()) + throw std::runtime_error("bad NCZBLOCK header"); + u64 off = payload_start; + offsets.reserve(sizes.size()); + for (u32 s : sizes) { offsets.push_back(off); off += s; } + } + + // Fills `out` from `position`; returns bytes written. + std::size_t ReadAt(u64 position, std::span out) { + std::size_t done = 0; + while (done < out.size() && position < decompressed_size) { + const u64 id = position / block_size; + if (id >= sizes.size()) break; + if (cache_id != id && !Load(id)) break; + const u64 in = position - id * block_size; + const std::size_t n = static_cast( + std::min(out.size() - done, cache.size() - in)); + std::memcpy(out.data() + done, cache.data() + in, n); + done += n; + position += n; + } + return done; + } + +private: + bool Load(u64 id) { + u64 want = block_size; + if (id + 1 == sizes.size()) { + const u64 rem = decompressed_size % block_size; + if (rem > 0) want = rem; // avoid the 0-size bug from the original nsz + } + try { cache.resize(static_cast(want)); } + catch (const std::bad_alloc&) { return false; } + const u32 csize = sizes[id]; + if (csize < want) { + const auto comp = source->ReadBytes(csize, offsets[id]); + if (comp.size() != csize || + Common::Compression::DecompressDataZSTD(comp, cache) != want) return false; + } else if (source->Read(cache.data(), cache.size(), offsets[id]) != cache.size()) { + return false; + } + cache_id = id; + return true; + } + + FileSys::VirtualFile source; + u64 block_size; + std::vector offsets; + std::vector sizes; + u64 decompressed_size; + u64 cache_id = ~0ULL; + std::vector cache; +}; + +class BlockNczFile final : public FileSys::VfsFile { +public: + BlockNczFile(std::string name_, std::vector hdr_, std::vector
sections_, + std::unique_ptr body_) + : name{std::move(name_)}, hdr{std::move(hdr_)}, sections{std::move(sections_)}, + body{std::move(body_)} { + nca_size = HeaderSize; + for (const auto& s : sections) + nca_size = std::max(nca_size, static_cast(s.offset + s.size)); + ciphers.reserve(sections.size()); + for (const auto& s : sections) + ciphers.push_back(s.Ctr() ? std::make_unique>(s.key, Mode::CTR) + : nullptr); + } + + std::string GetName() const override { return name; } + std::size_t GetSize() const override { return static_cast(nca_size); } + bool Resize(std::size_t) override { return false; } + FileSys::VirtualDir GetContainingDirectory() const override { return nullptr; } + bool IsWritable() const override { return false; } + bool IsReadable() const override { return true; } + + std::size_t Read(u8* data, std::size_t len, std::size_t off) const override { + if (off >= nca_size) return 0; + len = static_cast(std::min(len, nca_size - off)); + const u64 end = off + len; + u64 pos = off; + if (pos < HeaderSize) { + const u64 n = std::min(end, HeaderSize) - pos; + std::memcpy(data, hdr.data() + pos, static_cast(n)); + pos += n; + } + std::scoped_lock lock{mutex}; + for (std::size_t i = 0; i < sections.size() && pos < end; ++i) { + const auto& s = sections[i]; + const u64 begin = std::max(static_cast(s.offset), HeaderSize); + const u64 stop = std::max(static_cast(s.offset + s.size), begin); + if (stop <= pos) continue; + if (pos < begin) { + const u64 g = std::min(end, begin); + std::memset(data + (pos - off), 0, static_cast(g - pos)); + pos = g; + if (pos >= end) break; + } + const u64 pe = std::min(end, stop); + const std::span piece(data + (pos - off), static_cast(pe - pos)); + if (body->ReadAt(pos - HeaderSize, piece) != piece.size()) + return static_cast(pos - off); + if (ciphers[i]) { + Key128 iv{}; + SetIv(iv, s.counter, pos); + ciphers[i]->SetIV(iv); + ciphers[i]->Transcode(piece.data(), piece.size(), piece.data(), Op::Encrypt); + } + pos = pe; + } + return len; + } + + std::size_t Write(const u8*, std::size_t, std::size_t) override { return 0; } + bool Rename(std::string_view) override { return false; } + +private: + const std::string name; + const std::vector hdr; + const std::vector
sections; + const std::unique_ptr body; + std::vector>> ciphers; + mutable std::mutex mutex; + u64 nca_size{}; +}; + +} // Anonymous namespace + +bool IsBlockCompressedNCZ(const FileSys::VirtualFile& f) { + if (!f) return false; + u64 ps = 0; + const auto p = ParseHeader(f, ps); + return p && p->second.has_value(); +} + +FileSys::VirtualFile DecompressNCZ(const FileSys::VirtualFile& f, std::string_view name) { + if (!f) return nullptr; + u64 ps = 0; + auto parsed = ParseHeader(f, ps); + if (!parsed) return nullptr; + auto& [sections, block] = *parsed; + if (!block) { + LOG_ERROR(Crypto, "Solid NCZ is not supported (only NCZBLOCK / nsz -B)"); + return nullptr; + } + auto hdr = f->ReadBytes(HeaderSize, 0); + if (hdr.size() != HeaderSize) return nullptr; + std::unique_ptr body; + try { body = std::make_unique(f, *block, ps); } + catch (const std::exception& e) { + LOG_ERROR(Crypto, "Failed to open NCZBLOCK: {}", e.what()); + return nullptr; + } + return std::make_shared(std::string{name}, std::move(hdr), + std::move(sections), std::move(body)); +} + +} // namespace Core::Crypto \ No newline at end of file diff --git a/src/core/file_sys/ncz_decompressor.h b/src/core/file_sys/ncz_decompressor.h new file mode 100644 index 0000000000..0b001067ec --- /dev/null +++ b/src/core/file_sys/ncz_decompressor.h @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "core/file_sys/vfs/vfs_types.h" + +namespace Core::Crypto { + +/** + * Opens an NCZBLOCK-compressed NCA (nsz -B / .xcz block format) as a VirtualFile backed by + * on-demand block decoding. Nothing is written to disk; each read decodes only the blocks it + * covers and re-encrypts CTR sections. Solid NCZ is not supported. + * + * @param ncz_file The raw NCZ payload (e.g. ".ncz" entry from an NSZ/XCZ PFS0). + * @param output_name Name to give the returned VirtualFile (e.g. ".nca"). + * + * @return a VirtualFile for the NCA, or nullptr if the NCZ is malformed or not NCZBLOCK. + */ +[[nodiscard]] FileSys::VirtualFile DecompressNCZ(const FileSys::VirtualFile& ncz_file, + std::string_view output_name); + +/** + * Returns true if the given file is an NCZ payload in block-compressed (NCZBLOCK) form, + * i.e. one that DecompressNCZ() can open. Solid NCZ returns false. + */ +[[nodiscard]] bool IsBlockCompressedNCZ(const FileSys::VirtualFile& ncz_file); + +} // namespace Core::Crypto \ No newline at end of file diff --git a/src/core/file_sys/registered_cache.cpp b/src/core/file_sys/registered_cache.cpp index 1f430434f7..360f6e5344 100644 --- a/src/core/file_sys/registered_cache.cpp +++ b/src/core/file_sys/registered_cache.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/ncz_decompressor.h" #include "core/file_sys/registered_cache.h" #include "core/file_sys/romfs.h" #include "core/file_sys/submission_package.h" @@ -158,6 +159,25 @@ static std::shared_ptr OpenContainerAsNsp(const VirtualFile& file, Loader:: return nullptr; } +// Checks that every .ncz entry in the container is NCZBLOCK. Solid NCZ has no random access, +// and the whole point of this build is to reject it at scan time so such files never appear +// in the game list. Containers without any .ncz (plain NSP/XCI) pass through. +static bool ContainerIsFullyBlockCompressed(const std::shared_ptr& nsp) { + if (!nsp) { + return false; + } + for (const auto& entry : nsp->GetFiles()) { + const auto name = entry->GetName(); + if (name.size() < 4 || name.substr(name.size() - 4) != ".ncz") { + continue; + } + if (!Core::Crypto::IsBlockCompressedNCZ(entry)) { + return false; // solid NCZ -> reject the whole container + } + } + return true; // no .ncz at all, or all block-compressed +} + template bool ForEachContainerEntry(const std::shared_ptr& nsp, bool only_content, std::optional base_program_id, Callback&& on_entry) { @@ -1273,6 +1293,12 @@ bool ManualContentProvider::AddEntriesFromContainer(VirtualFile file, bool only_ return false; } + // Reject solid NCZ (.nsz without -B): only block-compressed NCZ is supported. + if (!ContainerIsFullyBlockCompressed(nsp)) { + LOG_DEBUG(Service_FS, "Skipping solid-compressed container: {}", file->GetName()); + return false; + } + return ForEachContainerEntry( nsp, only_content, base_program_id, [this](TitleType title_type, ContentRecordType content_type, u64 title_id, @@ -1434,6 +1460,12 @@ void ExternalContentProvider::ProcessNSP(const VirtualFile& file) { return; } + // Reject solid NCZ (.nsz without -B): Eden only supports block-compressed NCZ. + if (!ContainerIsFullyBlockCompressed(nsp)) { + LOG_DEBUG(Service_FS, "Skipping solid-compressed container: {}", file->GetName()); + return; + } + LOG_DEBUG(Service_FS, "Processing NSP file: {}", file->GetName()); AddExternalEntriesFromContainer(nsp, entries, versions, multi_version_entries); } @@ -1444,6 +1476,11 @@ void ExternalContentProvider::ProcessXCI(const VirtualFile& file) { return; } + if (!ContainerIsFullyBlockCompressed(nsp)) { + LOG_DEBUG(Service_FS, "Skipping solid-compressed container: {}", file->GetName()); + return; + } + AddExternalEntriesFromContainer(nsp, entries, versions, multi_version_entries); } diff --git a/src/core/file_sys/submission_package.cpp b/src/core/file_sys/submission_package.cpp index 0ea323492b..896d9493aa 100644 --- a/src/core/file_sys/submission_package.cpp +++ b/src/core/file_sys/submission_package.cpp @@ -15,6 +15,7 @@ #include "core/file_sys/common_funcs.h" #include "core/file_sys/content_archive.h" #include "core/file_sys/nca_metadata.h" +#include "core/file_sys/ncz_decompressor.h" #include "core/file_sys/partition_filesystem.h" #include "core/file_sys/program_metadata.h" #include "core/file_sys/submission_package.h" @@ -246,6 +247,18 @@ void NSP::ReadNCAs(const std::vector& files) { const auto id_string = Common::HexToString(rec.nca_id, false); auto next_file = pfs->GetFile(fmt::format("{}.nca", id_string)); + if (next_file == nullptr) { + auto ncz_file = pfs->GetFile(fmt::format("{}.ncz", id_string)); + if (ncz_file != nullptr) { + next_file = Core::Crypto::DecompressNCZ(ncz_file, fmt::format("{}.nca", id_string)); + if (next_file == nullptr) { + LOG_WARNING(Service_FS, "Skipping unsupported or corrupt NCZ content {}.ncz", + id_string); + continue; + } + } + } + if (next_file == nullptr) { if (rec.type != ContentRecordType::DeltaFragment) { LOG_WARNING(Service_FS, diff --git a/src/core/loader/loader.cpp b/src/core/loader/loader.cpp index 90e23ccc11..48daf9c3fb 100644 --- a/src/core/loader/loader.cpp +++ b/src/core/loader/loader.cpp @@ -82,12 +82,12 @@ std::shared_ptr OpenContainerAsNsp(FileSys::VirtualFile file, File return nullptr; } - if (type == FileType::NSP) { + if (type == FileType::NSP || type == FileType::NSZ) { auto nsp = std::make_shared(file, program_id, program_index); return nsp->GetStatus() == ResultStatus::Success ? nsp : nullptr; } - if (type == FileType::XCI) { + if (type == FileType::XCI || type == FileType::XCZ) { FileSys::XCI xci{file, program_id, program_index}; if (xci.GetStatus() != ResultStatus::Success) { return nullptr; @@ -144,7 +144,8 @@ FileType IdentifyFile(FileSys::VirtualFile file) { } bool IsContainerType(FileType type) { - return type == FileType::NSP || type == FileType::XCI; + return type == FileType::NSP || type == FileType::NSZ || type == FileType::XCI || + type == FileType::XCZ; } bool IsBootableGameContainer(FileSys::VirtualFile file, FileType type, u64 program_id, @@ -180,8 +181,12 @@ FileType GuessFromFilename(const std::string& name) { return FileType::NCA; else if (extension == "xci") return FileType::XCI; + else if (extension == "xcz") + return FileType::XCZ; else if (extension == "nsp") return FileType::NSP; + else if (extension == "nsz") + return FileType::NSZ; else if (extension == "kip") return FileType::KIP; return FileType::Unknown; @@ -197,10 +202,14 @@ std::string GetFileTypeString(FileType type) { return "NCA"; case FileType::XCI: return "XCI"; + case FileType::XCZ: + return "XCZ"; case FileType::NAX: return "NAX"; case FileType::NSP: return "NSP"; + case FileType::NSZ: + return "NSZ"; case FileType::KIP: return "KIP"; case FileType::DeconstructedRomDirectory: @@ -322,6 +331,7 @@ static std::unique_ptr GetFileLoader(Core::System& system, FileSys::V // NX XCI (nX Card Image) file format. case FileType::XCI: + case FileType::XCZ: return std::make_unique(std::move(file), system.GetFileSystemController(), system.GetContentProvider(), program_id, program_index); @@ -332,6 +342,7 @@ static std::unique_ptr GetFileLoader(Core::System& system, FileSys::V // NX NSP (Nintendo Submission Package) file format case FileType::NSP: + case FileType::NSZ: return std::make_unique(std::move(file), system.GetFileSystemController(), system.GetContentProvider(), program_id, program_index); diff --git a/src/core/loader/loader.h b/src/core/loader/loader.h index 95ce638da0..d3c0230cb0 100644 --- a/src/core/loader/loader.h +++ b/src/core/loader/loader.h @@ -42,7 +42,9 @@ enum class FileType { NRO, NCA, NSP, + NSZ, XCI, + XCZ, NAX, KIP, DeconstructedRomDirectory, diff --git a/src/frontend_common/content_manager.h b/src/frontend_common/content_manager.h index 4f3112e3fb..d7b76fc3a6 100644 --- a/src/frontend_common/content_manager.h +++ b/src/frontend_common/content_manager.h @@ -163,7 +163,9 @@ inline InstallResult InstallNSP(Core::System& system, FileSys::VfsFilesystem& vf std::shared_ptr nsp; FileSys::VirtualFile file = vfs.OpenFile(filename, FileSys::OpenMode::Read); - if (boost::to_lower_copy(file->GetName()).ends_with("nsp")) { + const auto lower_name = boost::to_lower_copy(file->GetName()); + + if (lower_name.ends_with("nsp") || lower_name.ends_with("nsz")) { nsp = std::make_shared(file); if (nsp->IsExtractedType()) { return InstallResult::Failure; diff --git a/src/qt_common/game_list/worker.cpp b/src/qt_common/game_list/worker.cpp index 586a26e8b9..6b6f93447d 100644 --- a/src/qt_common/game_list/worker.cpp +++ b/src/qt_common/game_list/worker.cpp @@ -360,7 +360,8 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa } if (target == ScanTarget::PopulateGameList && - (file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP) && + (file_type == Loader::FileType::XCI || file_type == Loader::FileType::XCZ || + file_type == Loader::FileType::NSP || file_type == Loader::FileType::NSZ) && !Loader::IsBootableGameContainer(file, file_type)) { return true; } @@ -375,7 +376,9 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa program_id, file); } else if (Settings::values.ext_content_from_game_dirs.GetValue() && (file_type == Loader::FileType::XCI || - file_type == Loader::FileType::NSP)) { + file_type == Loader::FileType::XCZ || + file_type == Loader::FileType::NSP || + file_type == Loader::FileType::NSZ)) { void(provider->AddEntriesFromContainer(file)); } } else { @@ -402,7 +405,8 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa }; if (res2 == Loader::ResultStatus::Success && program_ids.size() > 1 && - (file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP)) { + (file_type == Loader::FileType::XCI || file_type == Loader::FileType::XCZ || + file_type == Loader::FileType::NSP || file_type == Loader::FileType::NSZ)) { for (const auto id : program_ids) { // dravee suggested this, only viable way to // not show sub-games in qlaunch for now. diff --git a/src/qt_common/qt_common.cpp b/src/qt_common/qt_common.cpp index b9b8caf6e8..794aeaabb1 100644 --- a/src/qt_common/qt_common.cpp +++ b/src/qt_common/qt_common.cpp @@ -57,7 +57,8 @@ std::unique_ptr emu_thread = nullptr; const QStringList supported_file_extensions = {QStringLiteral("nro"), QStringLiteral("nso"), QStringLiteral("nca"), QStringLiteral("xci"), - QStringLiteral("nsp"), QStringLiteral("kip")}; + QStringLiteral("xcz"), QStringLiteral("nsp"), + QStringLiteral("nsz"), QStringLiteral("kip")}; Core::Frontend::WindowSystemType GetWindowSystemType() { // Determine WSI type based on Qt platform. From 9ada6f386a24035b60a5ab5700fdd4cd9c1c2892 Mon Sep 17 00:00:00 2001 From: PavelBARABANOV Date: Sun, 18 Oct 2026 13:21:06 +0300 Subject: [PATCH 04/14] license --- src/frontend_common/content_manager.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend_common/content_manager.h b/src/frontend_common/content_manager.h index d7b76fc3a6..6d79098efa 100644 --- a/src/frontend_common/content_manager.h +++ b/src/frontend_common/content_manager.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: 2024 yuzu Emulator Project From 263c048a140b53d78d6a65428928d0ccff4f9461 Mon Sep 17 00:00:00 2001 From: PavelBARABANOV Date: Sun, 18 Oct 2026 14:09:03 +0300 Subject: [PATCH 05/14] oops --- src/core/CMakeLists.txt | 4 ++-- src/core/{file_sys => crypto}/ncz_decompressor.cpp | 2 +- src/core/{file_sys => crypto}/ncz_decompressor.h | 0 src/core/file_sys/registered_cache.cpp | 2 +- src/core/file_sys/submission_package.cpp | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) rename src/core/{file_sys => crypto}/ncz_decompressor.cpp (99%) rename src/core/{file_sys => crypto}/ncz_decompressor.h (100%) diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 31e5e75858..1b578f67ba 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -33,6 +33,8 @@ add_library(core STATIC crypto/encryption_layer.h crypto/key_manager.cpp crypto/key_manager.h + crypto/ncz_decompressor.cpp + crypto/ncz_decompressor.h crypto/partition_data_manager.cpp crypto/partition_data_manager.h crypto/xts_encryption_layer.cpp @@ -122,8 +124,6 @@ add_library(core STATIC file_sys/kernel_executable.h file_sys/nca_metadata.cpp file_sys/nca_metadata.h - file_sys/ncz_decompressor.cpp - file_sys/ncz_decompressor.h file_sys/partition_filesystem.cpp file_sys/partition_filesystem.h file_sys/patch_manager.cpp diff --git a/src/core/file_sys/ncz_decompressor.cpp b/src/core/crypto/ncz_decompressor.cpp similarity index 99% rename from src/core/file_sys/ncz_decompressor.cpp rename to src/core/crypto/ncz_decompressor.cpp index 1dbd3ebd40..98800d91c9 100644 --- a/src/core/file_sys/ncz_decompressor.cpp +++ b/src/core/crypto/ncz_decompressor.cpp @@ -16,7 +16,7 @@ #include "common/zstd_compression.h" #include "core/crypto/aes_util.h" #include "core/crypto/key_manager.h" -#include "core/file_sys/ncz_decompressor.h" +#include "core/crypto/ncz_decompressor.h" #include "core/file_sys/vfs/vfs.h" namespace Core::Crypto { diff --git a/src/core/file_sys/ncz_decompressor.h b/src/core/crypto/ncz_decompressor.h similarity index 100% rename from src/core/file_sys/ncz_decompressor.h rename to src/core/crypto/ncz_decompressor.h diff --git a/src/core/file_sys/registered_cache.cpp b/src/core/file_sys/registered_cache.cpp index 360f6e5344..99a6edcc57 100644 --- a/src/core/file_sys/registered_cache.cpp +++ b/src/core/file_sys/registered_cache.cpp @@ -16,12 +16,12 @@ #include "common/random.h" #include "common/string_util.h" #include "core/crypto/key_manager.h" +#include "core/crypto/ncz_decompressor.h" #include "core/file_sys/card_image.h" #include "core/file_sys/common_funcs.h" #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/ncz_decompressor.h" #include "core/file_sys/registered_cache.h" #include "core/file_sys/romfs.h" #include "core/file_sys/submission_package.h" diff --git a/src/core/file_sys/submission_package.cpp b/src/core/file_sys/submission_package.cpp index 896d9493aa..15ec768784 100644 --- a/src/core/file_sys/submission_package.cpp +++ b/src/core/file_sys/submission_package.cpp @@ -12,10 +12,10 @@ #include "common/hex_util.h" #include "common/logging.h" #include "core/crypto/key_manager.h" +#include "core/crypto/ncz_decompressor.h" #include "core/file_sys/common_funcs.h" #include "core/file_sys/content_archive.h" #include "core/file_sys/nca_metadata.h" -#include "core/file_sys/ncz_decompressor.h" #include "core/file_sys/partition_filesystem.h" #include "core/file_sys/program_metadata.h" #include "core/file_sys/submission_package.h" From 77a5e306e906c108b70c63097d6150328cf643c6 Mon Sep 17 00:00:00 2001 From: PavelBARABANOV Date: Sun, 18 Oct 2026 22:21:52 +0300 Subject: [PATCH 06/14] no need --- src/core/file_sys/registered_cache.cpp | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/core/file_sys/registered_cache.cpp b/src/core/file_sys/registered_cache.cpp index 99a6edcc57..defc3d7d9b 100644 --- a/src/core/file_sys/registered_cache.cpp +++ b/src/core/file_sys/registered_cache.cpp @@ -1460,12 +1460,6 @@ void ExternalContentProvider::ProcessNSP(const VirtualFile& file) { return; } - // Reject solid NCZ (.nsz without -B): Eden only supports block-compressed NCZ. - if (!ContainerIsFullyBlockCompressed(nsp)) { - LOG_DEBUG(Service_FS, "Skipping solid-compressed container: {}", file->GetName()); - return; - } - LOG_DEBUG(Service_FS, "Processing NSP file: {}", file->GetName()); AddExternalEntriesFromContainer(nsp, entries, versions, multi_version_entries); } @@ -1476,11 +1470,6 @@ void ExternalContentProvider::ProcessXCI(const VirtualFile& file) { return; } - if (!ContainerIsFullyBlockCompressed(nsp)) { - LOG_DEBUG(Service_FS, "Skipping solid-compressed container: {}", file->GetName()); - return; - } - AddExternalEntriesFromContainer(nsp, entries, versions, multi_version_entries); } From 35cb16f8ca2941a96e26d62c59aa334631f654ca Mon Sep 17 00:00:00 2001 From: PavelBARABANOV Date: Mon, 19 Oct 2026 01:48:34 +0300 Subject: [PATCH 07/14] Install NSZ Solid to NAND --- src/core/crypto/ncz_decompressor.cpp | 191 ++++++++++++++++++++--- src/core/crypto/ncz_decompressor.h | 4 + src/core/file_sys/registered_cache.cpp | 9 +- src/core/file_sys/submission_package.cpp | 3 +- 4 files changed, 184 insertions(+), 23 deletions(-) diff --git a/src/core/crypto/ncz_decompressor.cpp b/src/core/crypto/ncz_decompressor.cpp index 98800d91c9..5f56e55375 100644 --- a/src/core/crypto/ncz_decompressor.cpp +++ b/src/core/crypto/ncz_decompressor.cpp @@ -11,6 +11,8 @@ #include #include +#include + #include "common/common_types.h" #include "common/logging.h" #include "common/zstd_compression.h" @@ -26,6 +28,11 @@ namespace { constexpr u64 HeaderSize = 0x4000; constexpr std::size_t SectionWire = 0x40; constexpr std::size_t BlockFixed = 0x18; +constexpr std::size_t Chunk = 0x100000; +// Upper bound on NCZ section entries (64 bytes each, so the table is at most 4 MiB). A base NCA +// has a handful of sections, but nsz stores an update's patch (BKTR) data one entry per +// subsection, which is thousands of them. +constexpr u64 MaxSectionCount = 0x10000; constexpr std::array SectionsMagic{'N', 'C', 'Z', 'S', 'E', 'C', 'T', 'N'}; constexpr std::array BlockMagic{'N', 'C', 'Z', 'B', 'L', 'O', 'C', 'K'}; @@ -65,7 +72,7 @@ std::optional, std::optional>> Parse if (head.size() != 16 || !std::equal(head.begin(), head.begin() + 8, SectionsMagic.begin())) return std::nullopt; const u64 count = Le(head.data() + 8, 8); - if (count == 0 || count > 64 || count * SectionWire > fsize - HeaderSize - 16) + if (count == 0 || count > MaxSectionCount || count * SectionWire > fsize - HeaderSize - 16) return std::nullopt; const auto table = f->ReadBytes(count * SectionWire, HeaderSize + 16); @@ -245,6 +252,163 @@ private: u64 nca_size{}; }; +// Solid NCZ: one zstd stream over the whole body. Decoded incrementally into memory, up to the +// requested offset, so sequential install reads are O(n) instead of O(n^2). +class SolidNczFile final : public FileSys::VfsFile { +public: + SolidNczFile(std::string name_, FileSys::VirtualFile source_, + std::vector
sections_, u64 payload_start_) + : name{std::move(name_)}, source{std::move(source_)}, sections{std::move(sections_)}, + payload_start{payload_start_} { + const auto raw_header = source->ReadBytes(HeaderSize, 0); + data.assign(raw_header.begin(), raw_header.end()); + frontier = HeaderSize; + } + + ~SolidNczFile() override { + if (stream != nullptr) ZSTD_freeDStream(stream); + } + + std::string GetName() const override { return name; } + std::size_t GetSize() const override { + u64 size = HeaderSize; + for (const auto& s : sections) + size = std::max(size, static_cast(s.offset + s.size)); + return static_cast(size); + } + bool Resize(std::size_t) override { return false; } + FileSys::VirtualDir GetContainingDirectory() const override { return nullptr; } + bool IsWritable() const override { return false; } + bool IsReadable() const override { return true; } + + std::size_t Read(u8* out, std::size_t len, std::size_t off) const override { + std::scoped_lock lock{mutex}; + const u64 nca_size = GetSize(); + if (off >= nca_size) return 0; + len = static_cast(std::min(len, nca_size - off)); + const u64 needed = off + len; + if (needed > frontier && !DecodeTo(needed)) return 0; + std::memcpy(out, data.data() + off, len); + return len; + } + + std::size_t Write(const u8*, std::size_t, std::size_t) override { return 0; } + bool Rename(std::string_view) override { return false; } + +private: + bool DecodeTo(u64 needed) const { + if (stream == nullptr) { + stream = ZSTD_createDStream(); + if (stream == nullptr) return false; + ZSTD_DCtx_setParameter(stream, ZSTD_d_windowLogMax, 31); + read_pos = payload_start; + } + + const u64 file_size = source->GetSize(); + std::vector out_buf(Chunk); + + std::size_t index = 0; + for (; index < sections.size(); ++index) { + const auto& s = sections[index]; + const u64 begin = std::max(static_cast(s.offset), HeaderSize); + const u64 end = std::max(static_cast(s.offset) + static_cast(s.size), + begin); + if (end > frontier) break; + } + + for (; index < sections.size() && frontier < needed; ++index) { + const auto& s = sections[index]; + const u64 begin = std::max(static_cast(s.offset), HeaderSize); + const u64 end = std::max(static_cast(s.offset) + static_cast(s.size), + begin); + if (end <= frontier) continue; + u64 pos = std::max(begin, frontier); + + std::optional> cipher; + if (s.Ctr()) cipher.emplace(s.key, Mode::CTR); + + while (pos < end && frontier < needed) { + const u64 want = std::min(end - pos, out_buf.size()); + std::size_t produced = 0; + while (produced < want) { + if (in.pos >= in.size) { + const u64 remaining = file_size - read_pos; + const std::size_t chunk = static_cast( + std::min(Chunk, remaining)); + if (chunk == 0) { LOG_ERROR(Crypto, "Solid NCZ: truncated"); return false; } + const auto bytes = source->ReadBytes(chunk, read_pos); + if (bytes.empty()) { LOG_ERROR(Crypto, "Solid NCZ: truncated"); return false; } + read_pos += bytes.size(); + in_buf.assign(bytes.begin(), bytes.end()); + in.src = in_buf.data(); + in.size = in_buf.size(); + in.pos = 0; + } + ZSTD_outBuffer outb{out_buf.data() + produced, want - produced, 0}; + const std::size_t res = ZSTD_decompressStream(stream, &outb, &in); + if (ZSTD_isError(res)) { + LOG_ERROR(Crypto, "Solid NCZ: zstd error"); + return false; + } + produced += outb.pos; + } + if (cipher) { + Key128 iv{}; + SetIv(iv, s.counter, pos); + cipher->SetIV(iv); + cipher->Transcode(out_buf.data(), produced, out_buf.data(), Op::Encrypt); + } + data.insert(data.end(), out_buf.begin(), out_buf.begin() + produced); + pos += produced; + frontier = pos; + } + } + return frontier >= needed; + } + + const std::string name; + const FileSys::VirtualFile source; + const std::vector
sections; + const u64 payload_start; + mutable std::mutex mutex; + mutable std::vector data; + mutable ZSTD_DStream* stream = nullptr; + mutable u64 read_pos = 0; + mutable u64 frontier = 0; + mutable std::vector in_buf; + mutable ZSTD_inBuffer in{nullptr, 0, 0}; +}; + +// Shared opener: block path always; solid only when allow_solid is true. +FileSys::VirtualFile OpenNcz(const FileSys::VirtualFile& f, std::string_view name, + bool allow_solid) { + if (!f) return nullptr; + u64 ps = 0; + auto parsed = ParseHeader(f, ps); + if (!parsed) return nullptr; + auto& [sections, block] = *parsed; + + if (block) { + auto hdr = f->ReadBytes(HeaderSize, 0); + if (hdr.size() != HeaderSize) return nullptr; + std::unique_ptr body; + try { body = std::make_unique(f, *block, ps); } + catch (const std::exception& e) { + LOG_ERROR(Crypto, "Failed to open NCZBLOCK: {}", e.what()); + return nullptr; + } + return std::make_shared(std::string{name}, std::move(hdr), + std::move(sections), std::move(body)); + } + + if (allow_solid) { + return std::make_shared(std::string{name}, f, std::move(sections), ps); + } + + LOG_DEBUG(Crypto, "Solid NCZ {} skipped (only NCZBLOCK is supported for play)", name); + return nullptr; +} + } // Anonymous namespace bool IsBlockCompressedNCZ(const FileSys::VirtualFile& f) { @@ -255,25 +419,12 @@ bool IsBlockCompressedNCZ(const FileSys::VirtualFile& f) { } FileSys::VirtualFile DecompressNCZ(const FileSys::VirtualFile& f, std::string_view name) { - if (!f) return nullptr; - u64 ps = 0; - auto parsed = ParseHeader(f, ps); - if (!parsed) return nullptr; - auto& [sections, block] = *parsed; - if (!block) { - LOG_ERROR(Crypto, "Solid NCZ is not supported (only NCZBLOCK / nsz -B)"); - return nullptr; - } - auto hdr = f->ReadBytes(HeaderSize, 0); - if (hdr.size() != HeaderSize) return nullptr; - std::unique_ptr body; - try { body = std::make_unique(f, *block, ps); } - catch (const std::exception& e) { - LOG_ERROR(Crypto, "Failed to open NCZBLOCK: {}", e.what()); - return nullptr; - } - return std::make_shared(std::string{name}, std::move(hdr), - std::move(sections), std::move(body)); + return OpenNcz(f, name, /*allow_solid=*/false); +} + +FileSys::VirtualFile DecompressNCZForInstall(const FileSys::VirtualFile& f, + std::string_view name) { + return OpenNcz(f, name, /*allow_solid=*/true); } } // namespace Core::Crypto \ No newline at end of file diff --git a/src/core/crypto/ncz_decompressor.h b/src/core/crypto/ncz_decompressor.h index 0b001067ec..9557f8c517 100644 --- a/src/core/crypto/ncz_decompressor.h +++ b/src/core/crypto/ncz_decompressor.h @@ -28,4 +28,8 @@ namespace Core::Crypto { */ [[nodiscard]] bool IsBlockCompressedNCZ(const FileSys::VirtualFile& ncz_file); +// Install to NAND: same as DecompressNCZ, but solid NCZ is decoded on demand too. +[[nodiscard]] FileSys::VirtualFile DecompressNCZForInstall( + const FileSys::VirtualFile& ncz_file, std::string_view output_name); + } // namespace Core::Crypto \ No newline at end of file diff --git a/src/core/file_sys/registered_cache.cpp b/src/core/file_sys/registered_cache.cpp index defc3d7d9b..287a46aef7 100644 --- a/src/core/file_sys/registered_cache.cpp +++ b/src/core/file_sys/registered_cache.cpp @@ -831,7 +831,14 @@ std::vector RegisteredCache::ListEntriesFilter( } static std::shared_ptr GetNCAFromNSPForID(const NSP& nsp, const NcaID& id) { - auto file = nsp.GetFile(fmt::format("{}.nca", Common::HexToString(id, false))); + const auto id_string = Common::HexToString(id, false); + auto file = nsp.GetFile(fmt::format("{}.nca", id_string)); + if (file == nullptr) { + if (auto ncz_file = nsp.GetFile(fmt::format("{}.ncz", id_string))) { + file = Core::Crypto::DecompressNCZForInstall( + ncz_file, fmt::format("{}.nca", id_string)); + } + } if (file == nullptr) { return nullptr; } diff --git a/src/core/file_sys/submission_package.cpp b/src/core/file_sys/submission_package.cpp index 15ec768784..1a39d819f6 100644 --- a/src/core/file_sys/submission_package.cpp +++ b/src/core/file_sys/submission_package.cpp @@ -252,8 +252,7 @@ void NSP::ReadNCAs(const std::vector& files) { if (ncz_file != nullptr) { next_file = Core::Crypto::DecompressNCZ(ncz_file, fmt::format("{}.nca", id_string)); if (next_file == nullptr) { - LOG_WARNING(Service_FS, "Skipping unsupported or corrupt NCZ content {}.ncz", - id_string); + LOG_DEBUG(Service_FS, "Skipping solid or unsupported NCZ content {}.ncz", id_string); continue; } } From 296db5e1ee777dd45da3243df33e0e2efc1c0bf1 Mon Sep 17 00:00:00 2001 From: PavelBARABANOV Date: Mon, 19 Oct 2026 05:33:16 +0300 Subject: [PATCH 08/14] stub --- src/core/loader/nsp.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/core/loader/nsp.cpp b/src/core/loader/nsp.cpp index cfc908b4ef..7081798d9b 100644 --- a/src/core/loader/nsp.cpp +++ b/src/core/loader/nsp.cpp @@ -7,6 +7,7 @@ #include #include "common/common_types.h" +#include "common/string_util.h" #include "core/core.h" #include "core/file_sys/common_funcs.h" #include "core/file_sys/content_archive.h" @@ -56,6 +57,12 @@ AppLoader_NSP::AppLoader_NSP(FileSys::VirtualFile file_, AppLoader_NSP::~AppLoader_NSP() = default; FileType AppLoader_NSP::IdentifyType(const FileSys::VirtualFile& nsp_file) { + + // .nsz is NSP-compatible; return NSZ so the game list shows the right type. + if (name.ends_with(".nsz")) { + return FileType::NSZ; + } + const FileSys::NSP nsp(nsp_file); if (nsp.GetStatus() != ResultStatus::Success) { From 650fc625f11a140e26b8ec521b2607fae30ef88f Mon Sep 17 00:00:00 2001 From: PavelBARABANOV Date: Mon, 19 Oct 2026 06:19:36 +0300 Subject: [PATCH 09/14] fix --- src/core/loader/nsp.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/loader/nsp.cpp b/src/core/loader/nsp.cpp index 7081798d9b..b90629983c 100644 --- a/src/core/loader/nsp.cpp +++ b/src/core/loader/nsp.cpp @@ -57,7 +57,7 @@ AppLoader_NSP::AppLoader_NSP(FileSys::VirtualFile file_, AppLoader_NSP::~AppLoader_NSP() = default; FileType AppLoader_NSP::IdentifyType(const FileSys::VirtualFile& nsp_file) { - + const auto name = Common::ToLower(nsp_file->GetName()); // .nsz is NSP-compatible; return NSZ so the game list shows the right type. if (name.ends_with(".nsz")) { return FileType::NSZ; From 700317664353655a6dd9d4ead378e4dd786467db Mon Sep 17 00:00:00 2001 From: PavelBARABANOV Date: Mon, 19 Oct 2026 17:29:29 +0300 Subject: [PATCH 10/14] f --- src/core/loader/nsp.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/core/loader/nsp.cpp b/src/core/loader/nsp.cpp index b90629983c..3b9112642e 100644 --- a/src/core/loader/nsp.cpp +++ b/src/core/loader/nsp.cpp @@ -7,7 +7,6 @@ #include #include "common/common_types.h" -#include "common/string_util.h" #include "core/core.h" #include "core/file_sys/common_funcs.h" #include "core/file_sys/content_archive.h" @@ -57,9 +56,8 @@ AppLoader_NSP::AppLoader_NSP(FileSys::VirtualFile file_, AppLoader_NSP::~AppLoader_NSP() = default; FileType AppLoader_NSP::IdentifyType(const FileSys::VirtualFile& nsp_file) { - const auto name = Common::ToLower(nsp_file->GetName()); // .nsz is NSP-compatible; return NSZ so the game list shows the right type. - if (name.ends_with(".nsz")) { + if (nsp_file->GetName().ends_with(".nsz")) { return FileType::NSZ; } From 0ee9051f59be746725d930ee72060b2f897facab Mon Sep 17 00:00:00 2001 From: PavelBARABANOV Date: Tue, 20 Oct 2026 02:10:15 +0300 Subject: [PATCH 11/14] more --- src/android/app/src/main/jni/game_metadata.cpp | 4 +++- src/yuzu/main_window.cpp | 11 +++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/android/app/src/main/jni/game_metadata.cpp b/src/android/app/src/main/jni/game_metadata.cpp index 49d92bca75..9b6cd44e9f 100644 --- a/src/android/app/src/main/jni/game_metadata.cpp +++ b/src/android/app/src/main/jni/game_metadata.cpp @@ -76,7 +76,9 @@ jboolean Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getIsValid(JNIEnv* env, jobj auto const file_type = loader->GetFileType(); if (file_type == Loader::FileType::Unknown || file_type == Loader::FileType::Error) return false; - if ((file_type == Loader::FileType::NSP || file_type == Loader::FileType::XCI) && !Loader::IsBootableGameContainer(file, file_type)) + if ((file_type == Loader::FileType::NSP || file_type == Loader::FileType::XCI || + file_type == Loader::FileType::NSZ || file_type == Loader::FileType::XCZ) && + !Loader::IsBootableGameContainer(file, file_type)) return false; u64 program_id = 0; return loader->ReadProgramId(program_id) == Loader::ResultStatus::Success; diff --git a/src/yuzu/main_window.cpp b/src/yuzu/main_window.cpp index 60bfbae5cd..d206c9ed26 100644 --- a/src/yuzu/main_window.cpp +++ b/src/yuzu/main_window.cpp @@ -2797,9 +2797,9 @@ void MainWindow::IncrementInstallProgress() { void MainWindow::OnMenuInstallToNAND() { const QString file_filter = - tr("Installable Switch File (*.nca *.nsp *.xci);;Nintendo Content Archive " - "(*.nca);;Nintendo Submission Package (*.nsp);;NX Cartridge " - "Image (*.xci)"); + tr("Installable Switch File (*.nca *.nsp *.nsz *.xci *.xcz);;Nintendo Content Archive " + "(*.nca);;Nintendo Submission Package (*.nsp *.nsz);;NX Cartridge " + "Image (*.xci *.xcz)"); QStringList filenames = QFileDialog::getOpenFileNames( this, tr("Install Files"), QString::fromStdString(UISettings::values.roms_path), @@ -2856,7 +2856,10 @@ void MainWindow::OnMenuInstallToNAND() { QFuture future; ContentManager::InstallResult result; - if (file.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) { + if (file.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive) || + file.endsWith(QStringLiteral("nsz"), Qt::CaseInsensitive) || + file.endsWith(QStringLiteral("xci"), Qt::CaseInsensitive) || + file.endsWith(QStringLiteral("xcz"), Qt::CaseInsensitive)) { const auto progress_callback = [this](size_t size, size_t progress) { emit UpdateInstallProgress(); if (install_progress->wasCanceled()) { From 79603bfdc7b4424c1c309819ef3a1839075cdf56 Mon Sep 17 00:00:00 2001 From: PavelBARABANOV Date: Wed, 21 Oct 2026 00:27:54 +0300 Subject: [PATCH 12/14] scanning from the update folder --- src/core/file_sys/registered_cache.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/file_sys/registered_cache.cpp b/src/core/file_sys/registered_cache.cpp index 287a46aef7..ea8a3b1a55 100644 --- a/src/core/file_sys/registered_cache.cpp +++ b/src/core/file_sys/registered_cache.cpp @@ -1449,9 +1449,9 @@ void ExternalContentProvider::ScanDirectory(const VirtualDir& dir) { const auto extension = Common::ToLower(filename.substr(dot_pos + 1)); - if (extension == "nsp") { + if (extension == "nsp" || extension == "nsz") { ProcessNSP(file); - } else if (extension == "xci") { + } else if (extension == "xci" || extension == "xcz") { ProcessXCI(file); } } From aa1441437eb74f1f79e48f0e823e06bbaf74778e Mon Sep 17 00:00:00 2001 From: PavelBARABANOV Date: Wed, 21 Oct 2026 00:47:53 +0300 Subject: [PATCH 13/14] Revert "no need" This reverts commit ea68a544cb36210157ac39069f7ffb215ba1b4f8. --- src/core/file_sys/registered_cache.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/core/file_sys/registered_cache.cpp b/src/core/file_sys/registered_cache.cpp index ea8a3b1a55..efd40fb5e7 100644 --- a/src/core/file_sys/registered_cache.cpp +++ b/src/core/file_sys/registered_cache.cpp @@ -1467,6 +1467,12 @@ void ExternalContentProvider::ProcessNSP(const VirtualFile& file) { return; } + // Reject solid NCZ (.nsz without -B): Eden only supports block-compressed NCZ. + if (!ContainerIsFullyBlockCompressed(nsp)) { + LOG_DEBUG(Service_FS, "Skipping solid-compressed container: {}", file->GetName()); + return; + } + LOG_DEBUG(Service_FS, "Processing NSP file: {}", file->GetName()); AddExternalEntriesFromContainer(nsp, entries, versions, multi_version_entries); } @@ -1477,6 +1483,11 @@ void ExternalContentProvider::ProcessXCI(const VirtualFile& file) { return; } + if (!ContainerIsFullyBlockCompressed(nsp)) { + LOG_DEBUG(Service_FS, "Skipping solid-compressed container: {}", file->GetName()); + return; + } + AddExternalEntriesFromContainer(nsp, entries, versions, multi_version_entries); } From e73fda66203c7ac4c36d8763bdcf2dfd447db3ea Mon Sep 17 00:00:00 2001 From: PavelBARABANOV Date: Wed, 21 Oct 2026 00:59:14 +0300 Subject: [PATCH 14/14] xcz always block --- src/core/file_sys/registered_cache.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/core/file_sys/registered_cache.cpp b/src/core/file_sys/registered_cache.cpp index efd40fb5e7..cdc6811a27 100644 --- a/src/core/file_sys/registered_cache.cpp +++ b/src/core/file_sys/registered_cache.cpp @@ -1483,11 +1483,6 @@ void ExternalContentProvider::ProcessXCI(const VirtualFile& file) { return; } - if (!ContainerIsFullyBlockCompressed(nsp)) { - LOG_DEBUG(Service_FS, "Skipping solid-compressed container: {}", file->GetName()); - return; - } - AddExternalEntriesFromContainer(nsp, entries, versions, multi_version_entries); }