From 815325cceca948030cf476dce7c46d901e4e4df5 Mon Sep 17 00:00:00 2001 From: PavelBARABANOV Date: Sun, 27 Sep 2026 12:48:45 +0200 Subject: [PATCH] [loader] add ZBIC (zstd variant) NSO decompression support for Switch 22.0+ (#4482) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - [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. ------------------- A new NSO compression method was introduced in Switch 22.0.0. This is a customized variant of zstd and is used when NSO flags have bit 7 set. Key characteristics: - ZSTD_MAGICNUMBER is set to 0x4349425A (b'ZBIC') instead of 0xFD2FB528 - ZSTD_LEGACY_SUPPORT is set to 0 - ZSTD_TRACE is set to 1, zstd version used is 1.5.7 (10507) - FSE_readNCount is replaced with a BIC (Binary Interpolative Coding) version which improves compression of entropy tables significantly Source: https://switchbrew.org/wiki/22.0.0 Implementation: - Detect ZBIC segments via NSO flag bit 7 (NsoFlags_UseZbicCompression) and/or ZBIC magic scan in nso.cpp - Fall back to LZ4 when ZBIC is not detected or returns unexpected size - Handle segment 0 alternate offset (0x100 vs 0x108) for both ZBIC and LZ4 References: - Atmosphère loader/strat zstd-zbic integration: https://github.com/Atmosphere-NX/Atmosphere/commit/082115187a0509cb6b8a757de639a4e4741b8712 - nxdumptool ZBIC segment compression support: https://github.com/DarkMatterCore/nxdumptool/commit/441e5c0904f29427987433be4eb057a2843d222a - STORM_SWITCH ZBIC implementation: https://github.com/ReiKatari/STORM_SWITCH/commit/1e09eb82b760aaf340b810031400991c0740c091 Tested with firmware 23.0.0 and ZBIC-compressed NSOs. Co-authored-by: xbzk Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4482 Reviewed-by: CamilleLaVey Reviewed-by: lizzie Reviewed-by: Maufeat --- cpmfile.json | 5 ++++ externals/CMakeLists.txt | 5 ++++ src/common/CMakeLists.txt | 6 +++++ src/common/zbic_compression.cpp | 46 +++++++++++++++++++++++++++++++++ src/common/zbic_compression.h | 15 +++++++++++ src/core/loader/nso.cpp | 35 ++++++++++++++++++++++--- src/core/loader/nso.h | 6 +++++ 7 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 src/common/zbic_compression.cpp create mode 100644 src/common/zbic_compression.h diff --git a/cpmfile.json b/cpmfile.json index f574bf5668..616235f27a 100644 --- a/cpmfile.json +++ b/cpmfile.json @@ -333,6 +333,11 @@ "repo": "herumi/xbyak", "version": "v7.40.1" }, + "zbic": { + "hash": "fbe2f37986377d7f0d96ae3224c80b5971df6e8b6961f68061f1975ebb2e8cb78f07b90f014b007be4023dbf5513581504e7f7d61a5237cb2a8a7a61ae11e482", + "repo": "kinnay/zbic", + "version": "11b08f2712264bbed731545085cbd9702096ceb7" + }, "zlib": { "hash": "16fea4df307a68cf0035858abe2fd550250618a97590e202037acd18a666f57afc10f8836cbbd472d54a0e76539d0e558cb26f059d53de52ff90634bbf4f47d4", "min_version": "1.2", diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index a211b7f013..cb1deceb96 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -48,6 +48,11 @@ if (NOT TARGET stb::headers) add_library(stb::headers ALIAS stb) endif() +AddJsonPackage(NAME zbic DOWNLOAD_ONLY) +set(ZBIC_INCLUDE_DIR + "${zbic_SOURCE_DIR}/src" + PARENT_SCOPE) + # ItaniumDemangle (Windows only) if (WIN32 AND NOT TARGET LLVM::Demangle) add_library(demangle demangle/ItaniumDemangle.cpp) diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 0517f0b9e1..5d1b30a549 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -137,6 +137,8 @@ add_library( uuid.cpp uuid.h vector_math.h + zbic_compression.cpp + zbic_compression.h zstd_compression.cpp zstd_compression.h fs/ryujinx_compat.h fs/ryujinx_compat.cpp @@ -147,6 +149,10 @@ add_library( net/net.h net/net.cpp container/unordered_map.h container/unordered_set.h) +set_source_files_properties(zbic_compression.cpp PROPERTIES + INCLUDE_DIRECTORIES "${ZBIC_INCLUDE_DIR}" + COMPILE_OPTIONS "$<$:-Wno-unused-function;-Wno-missing-declarations;-Wno-shadow>") + if(WIN32) target_sources(common PRIVATE windows/timer_resolution.cpp windows/timer_resolution.h) diff --git a/src/common/zbic_compression.cpp b/src/common/zbic_compression.cpp new file mode 100644 index 0000000000..5e0e4d947b --- /dev/null +++ b/src/common/zbic_compression.cpp @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include + +#include "common/zbic_compression.h" + +#define ZSTD_ZBIC_SUPPORT 1 +#define ZSTDLIB_VISIBLE static +#define ZSTDLIB_HIDDEN static +#define ZSTDERRORLIB_VISIBLE static +#define ZSTDERRORLIB_HIDDEN static +#undef ZSTD_MULTITHREAD + +#if defined(__ANDROID__) +#undef _GNU_SOURCE +#endif + +#include "zstd.h" +#define g_ZSTD_threading_useless_symbol g_ZSTD_zbic_threading_useless_symbol +#include "zstd.c" +#undef g_ZSTD_threading_useless_symbol + +namespace Common::Compression { + +bool IsZBIC(std::span src) { + if (src.size() < sizeof(u32)) { + return false; + } + u32 magic = 0; + std::memcpy(&magic, src.data(), sizeof(u32)); + return magic == ZSTD_MAGICNUMBER; // 0x4349425A ("ZBIC") +} + +int DecompressDataZBIC(std::span dst, std::span src) { + if (dst.empty() || src.empty()) { + return -1; + } + const size_t res = ZSTD_decompress(dst.data(), dst.size(), src.data(), src.size()); + if (ZSTD_isError(res)) { + return -1; + } + return static_cast(res); +} + +} // namespace Common::Compression diff --git a/src/common/zbic_compression.h b/src/common/zbic_compression.h new file mode 100644 index 0000000000..c36a559388 --- /dev/null +++ b/src/common/zbic_compression.h @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include "common/common_types.h" + +namespace Common::Compression { + +[[nodiscard]] bool IsZBIC(std::span src); + +[[nodiscard]] int DecompressDataZBIC(std::span dst, std::span src); + +} // namespace Common::Compression diff --git a/src/core/loader/nso.cpp b/src/core/loader/nso.cpp index 0a3dc5885b..ea1d1c9ecd 100644 --- a/src/core/loader/nso.cpp +++ b/src/core/loader/nso.cpp @@ -7,12 +7,14 @@ #include #include #include +#include #include #include "common/common_funcs.h" #include "common/hex_util.h" #include "common/logging.h" #include "common/lz4_compression.h" +#include "common/zbic_compression.h" #include "common/settings.h" #include "common/swap.h" #include "core/core.h" @@ -104,11 +106,36 @@ std::optional AppLoader_NSO::LoadModule(Kernel::KProcess& process, Core:: for (std::size_t i = 0; i < nso_header.segments.size(); ++i) { nso_file.Read(compressed_data.data(), nso_header.segments_compressed_size[i], nso_header.segments[i].offset); if (nso_header.IsSegmentCompressed(i)) { - int r = Common::Compression::DecompressDataLZ4(decompressed_size.data(), nso_header.segments[i].size, compressed_data.data(), nso_header.segments_compressed_size[i]); - ASSERT(r == int(nso_header.segments[i].size)); - std::memcpy(codeset.memory.data() + module_start + nso_header.segments[i].location, decompressed_size.data(), nso_header.segments[i].size); + if (nso_header.IsZBICCompressed()) { + // ZBIC compression + const int r = Common::Compression::DecompressDataZBIC( + std::span{decompressed_size}.first(nso_header.segments[i].size), + std::span{compressed_data}.first(nso_header.segments_compressed_size[i]) + ); + ASSERT(r > 0); + } else { + // LZ4 compression + int r = Common::Compression::DecompressDataLZ4( + decompressed_size.data(), + nso_header.segments[i].size, + compressed_data.data(), + nso_header.segments_compressed_size[i] + ); + ASSERT(r == int(nso_header.segments[i].size)); + } + + std::memcpy( + codeset.memory.data() + module_start + nso_header.segments[i].location, + decompressed_size.data(), + nso_header.segments[i].size + ); } else { - std::memcpy(codeset.memory.data() + module_start + nso_header.segments[i].location, compressed_data.data(), nso_header.segments[i].size); + // Not compressed + std::memcpy( + codeset.memory.data() + module_start + nso_header.segments[i].location, + compressed_data.data(), + nso_header.segments[i].size + ); } codeset.segments[i].addr = module_start + nso_header.segments[i].location; codeset.segments[i].offset = module_start + nso_header.segments[i].location; diff --git a/src/core/loader/nso.h b/src/core/loader/nso.h index 6356697e33..4a373edd07 100644 --- a/src/core/loader/nso.h +++ b/src/core/loader/nso.h @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -58,6 +61,9 @@ struct NSOHeader { std::array segment_hashes; bool IsSegmentCompressed(size_t segment_num) const; + bool IsZBICCompressed() const { + return ((flags >> 7) & 1) != 0; + } }; static_assert(sizeof(NSOHeader) == 0x100, "NSOHeader has incorrect size."); static_assert(std::is_trivially_copyable_v, "NSOHeader must be trivially copyable.");