[loader] add ZBIC (zstd variant) NSO decompression support for Switch 22.0+ (#4482)

- [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 <xbzk@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4482
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Reviewed-by: lizzie <lizzie@eden-emu.dev>
Reviewed-by: Maufeat <sahyno1996@gmail.com>
This commit is contained in:
PavelBARABANOV
2026-09-27 12:48:45 +02:00
committed by crueter
parent 37fe911952
commit 815325ccec
7 changed files with 114 additions and 4 deletions
+31 -4
View File
@@ -7,12 +7,14 @@
#include <algorithm>
#include <cinttypes>
#include <cstring>
#include <span>
#include <vector>
#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<VAddr> 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<u8>{decompressed_size}.first(nso_header.segments[i].size),
std::span<const u8>{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;
+6
View File
@@ -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<SHA256Hash, 3> 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>, "NSOHeader must be trivially copyable.");