Compare commits

..

1 Commits

25 changed files with 208 additions and 602 deletions
@@ -100,7 +100,7 @@ class Game(
companion object {
val extensions: Set<String> = HashSet(
listOf("xci", "nsp", "nca", "nro", "nsz", "xcz")
listOf("xci", "nsp", "nca", "nro")
)
}
}
@@ -109,7 +109,7 @@ object GameHelper {
// File extensions considered as external content, buuut should
// be done better imo.
private val externalContentExtensions = setOf("nsp", "xci", "nsz", "xcz")
private val externalContentExtensions = setOf("nsp", "xci")
private fun scanContentContainersRecursive(
files: Array<MinimalDocumentFile>,
@@ -76,9 +76,7 @@ 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 ||
file_type == Loader::FileType::NSZ || file_type == Loader::FileType::XCZ) &&
!Loader::IsBootableGameContainer(file, file_type))
if ((file_type == Loader::FileType::NSP || file_type == Loader::FileType::XCI) && !Loader::IsBootableGameContainer(file, file_type))
return false;
u64 program_id = 0;
return loader->ReadProgramId(program_id) == Loader::ResultStatus::Success;
-9
View File
@@ -1,6 +1,3 @@
// 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
@@ -49,10 +46,4 @@ std::vector<u8> DecompressDataZSTD(std::span<const u8> compressed) {
return decompressed;
}
std::size_t DecompressDataZSTD(std::span<const u8> compressed, std::span<u8> 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
-14
View File
@@ -1,6 +1,3 @@
// 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
@@ -45,15 +42,4 @@ namespace Common::Compression {
*/
[[nodiscard]] std::vector<u8> DecompressDataZSTD(std::span<const u8> 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<const u8> compressed, std::span<u8> output);
} // namespace Common::Compression
-2
View File
@@ -33,8 +33,6 @@ 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
-430
View File
@@ -1,430 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <algorithm>
#include <array>
#include <cstring>
#include <memory>
#include <mutex>
#include <optional>
#include <span>
#include <string>
#include <vector>
#include <zstd.h>
#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/crypto/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::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<u8, 8> SectionsMagic{'N', 'C', 'Z', 'S', 'E', 'C', 'T', 'N'};
constexpr std::array<u8, 8> 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<u32> 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<u8>(blk & 0xFF); blk >>= 8; }
}
// Parse header + section table + optional block header. Returns payload_start.
std::optional<std::pair<std::vector<Section>, std::optional<BlockHeader>>> 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 > MaxSectionCount || 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<Section> 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<s64>(Le(e, 8));
s.size = static_cast<s64>(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<BlockHeader> 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<s32>(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<u64>(n) * 4, block_start + BlockFixed);
if (sizes.size() != static_cast<u64>(n) * 4) return std::nullopt;
b.sizes.resize(n);
for (s32 i = 0; i < n; ++i) b.sizes[i] = static_cast<u32>(Le(sizes.data() + i * 4, 4));
payload_start = block_start + BlockFixed + static_cast<u64>(n) * 4;
bh = std::move(b);
} else {
payload_start = block_start;
}
if (!sections.empty() && sections[0].offset > static_cast<s64>(HeaderSize)) {
Section fake{};
fake.offset = static_cast<s64>(HeaderSize);
fake.size = sections[0].offset - static_cast<s64>(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<u8> 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::size_t>(
std::min<u64>(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<std::size_t>(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<u64> offsets;
std::vector<u32> sizes;
u64 decompressed_size;
u64 cache_id = ~0ULL;
std::vector<u8> cache;
};
class BlockNczFile final : public FileSys::VfsFile {
public:
BlockNczFile(std::string name_, std::vector<u8> hdr_, std::vector<Section> sections_,
std::unique_ptr<BlockReader> 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<u64>(nca_size, static_cast<u64>(s.offset + s.size));
ciphers.reserve(sections.size());
for (const auto& s : sections)
ciphers.push_back(s.Ctr() ? std::make_unique<AESCipher<Key128>>(s.key, Mode::CTR)
: nullptr);
}
std::string GetName() const override { return name; }
std::size_t GetSize() const override { return static_cast<std::size_t>(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::size_t>(std::min<u64>(len, nca_size - off));
const u64 end = off + len;
u64 pos = off;
if (pos < HeaderSize) {
const u64 n = std::min<u64>(end, HeaderSize) - pos;
std::memcpy(data, hdr.data() + pos, static_cast<std::size_t>(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<u64>(static_cast<u64>(s.offset), HeaderSize);
const u64 stop = std::max<u64>(static_cast<u64>(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<std::size_t>(g - pos));
pos = g;
if (pos >= end) break;
}
const u64 pe = std::min(end, stop);
const std::span<u8> piece(data + (pos - off), static_cast<std::size_t>(pe - pos));
if (body->ReadAt(pos - HeaderSize, piece) != piece.size())
return static_cast<std::size_t>(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<u8> hdr;
const std::vector<Section> sections;
const std::unique_ptr<BlockReader> body;
std::vector<std::unique_ptr<AESCipher<Key128>>> ciphers;
mutable std::mutex mutex;
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<Section> 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<u64>(size, static_cast<u64>(s.offset + s.size));
return static_cast<std::size_t>(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::size_t>(std::min<u64>(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<u8> out_buf(Chunk);
std::size_t index = 0;
for (; index < sections.size(); ++index) {
const auto& s = sections[index];
const u64 begin = std::max<u64>(static_cast<u64>(s.offset), HeaderSize);
const u64 end = std::max<u64>(static_cast<u64>(s.offset) + static_cast<u64>(s.size),
begin);
if (end > frontier) break;
}
for (; index < sections.size() && frontier < needed; ++index) {
const auto& s = sections[index];
const u64 begin = std::max<u64>(static_cast<u64>(s.offset), HeaderSize);
const u64 end = std::max<u64>(static_cast<u64>(s.offset) + static_cast<u64>(s.size),
begin);
if (end <= frontier) continue;
u64 pos = std::max<u64>(begin, frontier);
std::optional<AESCipher<Key128>> cipher;
if (s.Ctr()) cipher.emplace(s.key, Mode::CTR);
while (pos < end && frontier < needed) {
const u64 want = std::min<u64>(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::size_t>(
std::min<u64>(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<Section> sections;
const u64 payload_start;
mutable std::mutex mutex;
mutable std::vector<u8> data;
mutable ZSTD_DStream* stream = nullptr;
mutable u64 read_pos = 0;
mutable u64 frontier = 0;
mutable std::vector<u8> 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<BlockReader> body;
try { body = std::make_unique<BlockReader>(f, *block, ps); }
catch (const std::exception& e) {
LOG_ERROR(Crypto, "Failed to open NCZBLOCK: {}", e.what());
return nullptr;
}
return std::make_shared<BlockNczFile>(std::string{name}, std::move(hdr),
std::move(sections), std::move(body));
}
if (allow_solid) {
return std::make_shared<SolidNczFile>(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) {
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) {
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
-35
View File
@@ -1,35 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <string_view>
#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. "<id>.ncz" entry from an NSZ/XCZ PFS0).
* @param output_name Name to give the returned VirtualFile (e.g. "<id>.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);
// 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
+3 -42
View File
@@ -16,7 +16,6 @@
#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"
@@ -159,25 +158,6 @@ static std::shared_ptr<NSP> 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>& 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 <typename Callback>
bool ForEachContainerEntry(const std::shared_ptr<NSP>& nsp, bool only_content,
std::optional<u64> base_program_id, Callback&& on_entry) {
@@ -831,14 +811,7 @@ std::vector<ContentProviderEntry> RegisteredCache::ListEntriesFilter(
}
static std::shared_ptr<NCA> GetNCAFromNSPForID(const NSP& nsp, const NcaID& id) {
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));
}
}
auto file = nsp.GetFile(fmt::format("{}.nca", Common::HexToString(id, false)));
if (file == nullptr) {
return nullptr;
}
@@ -1300,12 +1273,6 @@ 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,
@@ -1449,9 +1416,9 @@ void ExternalContentProvider::ScanDirectory(const VirtualDir& dir) {
const auto extension = Common::ToLower(filename.substr(dot_pos + 1));
if (extension == "nsp" || extension == "nsz") {
if (extension == "nsp") {
ProcessNSP(file);
} else if (extension == "xci" || extension == "xcz") {
} else if (extension == "xci") {
ProcessXCI(file);
}
}
@@ -1467,12 +1434,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);
}
-12
View File
@@ -12,7 +12,6 @@
#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"
@@ -247,17 +246,6 @@ void NSP::ReadNCAs(const std::vector<VirtualFile>& 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_DEBUG(Service_FS, "Skipping solid or unsupported NCZ content {}.ncz", id_string);
continue;
}
}
}
if (next_file == nullptr) {
if (rec.type != ContentRecordType::DeltaFragment) {
LOG_WARNING(Service_FS,
+3 -14
View File
@@ -82,12 +82,12 @@ std::shared_ptr<FileSys::NSP> OpenContainerAsNsp(FileSys::VirtualFile file, File
return nullptr;
}
if (type == FileType::NSP || type == FileType::NSZ) {
if (type == FileType::NSP) {
auto nsp = std::make_shared<FileSys::NSP>(file, program_id, program_index);
return nsp->GetStatus() == ResultStatus::Success ? nsp : nullptr;
}
if (type == FileType::XCI || type == FileType::XCZ) {
if (type == FileType::XCI) {
FileSys::XCI xci{file, program_id, program_index};
if (xci.GetStatus() != ResultStatus::Success) {
return nullptr;
@@ -144,8 +144,7 @@ FileType IdentifyFile(FileSys::VirtualFile file) {
}
bool IsContainerType(FileType type) {
return type == FileType::NSP || type == FileType::NSZ || type == FileType::XCI ||
type == FileType::XCZ;
return type == FileType::NSP || type == FileType::XCI;
}
bool IsBootableGameContainer(FileSys::VirtualFile file, FileType type, u64 program_id,
@@ -181,12 +180,8 @@ 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;
@@ -202,14 +197,10 @@ 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:
@@ -331,7 +322,6 @@ static std::unique_ptr<AppLoader> GetFileLoader(Core::System& system, FileSys::V
// NX XCI (nX Card Image) file format.
case FileType::XCI:
case FileType::XCZ:
return std::make_unique<AppLoader_XCI>(std::move(file), system.GetFileSystemController(),
system.GetContentProvider(), program_id,
program_index);
@@ -342,7 +332,6 @@ static std::unique_ptr<AppLoader> GetFileLoader(Core::System& system, FileSys::V
// NX NSP (Nintendo Submission Package) file format
case FileType::NSP:
case FileType::NSZ:
return std::make_unique<AppLoader_NSP>(std::move(file), system.GetFileSystemController(),
system.GetContentProvider(), program_id,
program_index);
-2
View File
@@ -42,9 +42,7 @@ enum class FileType {
NRO,
NCA,
NSP,
NSZ,
XCI,
XCZ,
NAX,
KIP,
DeconstructedRomDirectory,
-5
View File
@@ -56,11 +56,6 @@ 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 (nsp_file->GetName().ends_with(".nsz")) {
return FileType::NSZ;
}
const FileSys::NSP nsp(nsp_file);
if (nsp.GetStatus() != ResultStatus::Success) {
+2 -4
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
@@ -163,9 +163,7 @@ inline InstallResult InstallNSP(Core::System& system, FileSys::VfsFilesystem& vf
std::shared_ptr<FileSys::NSP> nsp;
FileSys::VirtualFile file = vfs.OpenFile(filename, FileSys::OpenMode::Read);
const auto lower_name = boost::to_lower_copy(file->GetName());
if (lower_name.ends_with("nsp") || lower_name.ends_with("nsz")) {
if (boost::to_lower_copy(file->GetName()).ends_with("nsp")) {
nsp = std::make_shared<FileSys::NSP>(file);
if (nsp->IsExtractedType()) {
return InstallResult::Failure;
+3 -7
View File
@@ -360,8 +360,7 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
}
if (target == ScanTarget::PopulateGameList &&
(file_type == Loader::FileType::XCI || file_type == Loader::FileType::XCZ ||
file_type == Loader::FileType::NSP || file_type == Loader::FileType::NSZ) &&
(file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP) &&
!Loader::IsBootableGameContainer(file, file_type)) {
return true;
}
@@ -376,9 +375,7 @@ 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::XCZ ||
file_type == Loader::FileType::NSP ||
file_type == Loader::FileType::NSZ)) {
file_type == Loader::FileType::NSP)) {
void(provider->AddEntriesFromContainer(file));
}
} else {
@@ -405,8 +402,7 @@ 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::XCZ ||
file_type == Loader::FileType::NSP || file_type == Loader::FileType::NSZ)) {
(file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP)) {
for (const auto id : program_ids) {
// dravee suggested this, only viable way to
// not show sub-games in qlaunch for now.
+1 -2
View File
@@ -57,8 +57,7 @@ std::unique_ptr<EmuThread> emu_thread = nullptr;
const QStringList supported_file_extensions = {QStringLiteral("nro"), QStringLiteral("nso"),
QStringLiteral("nca"), QStringLiteral("xci"),
QStringLiteral("xcz"), QStringLiteral("nsp"),
QStringLiteral("nsz"), QStringLiteral("kip")};
QStringLiteral("nsp"), QStringLiteral("kip")};
Core::Frontend::WindowSystemType GetWindowSystemType() {
// Determine WSI type based on Qt platform.
@@ -40,6 +40,8 @@ struct HostTranslateInfo {
bool support_conditional_barrier{}; ///< True when the device supports barriers in conditional
///< control flow
bool single_lane_geometry_subgroups{};
bool support_attribute_texture_handle{}; ///< True when the host binds texture handles that
///< arrive as vertex attributes
void ApplyDescriptorLimitPolicy() noexcept {
if (min_ssbo_alignment == 0) {
+44 -3
View File
@@ -268,6 +268,10 @@ static inline u32 ReadCbufCached(Environment& env, u32 index, u32 offset) {
return v;
}
static inline bool IsAttributeHandle(const ConstBufferAddr& cbuf) {
return cbuf.index == ATTRIBUTE_HANDLE_CBUF_INDEX;
}
static inline u32 GetTextureHandleCached(Environment& env, const ConstBufferAddr& cbuf) {
// Must all be uniquely different variables
// If has secondary, then it will be cbuf.secondary_{index|offset}, else its 0.
@@ -287,14 +291,23 @@ static inline u32 GetTextureHandleCached(Environment& env, const ConstBufferAddr
// Cached variants of existing helpers
static inline TextureType ReadTextureTypeCached(Environment& env, const ConstBufferAddr& cbuf) {
if (IsAttributeHandle(cbuf)) {
return TextureType::Color2D;
}
return env.ReadTextureType(GetTextureHandleCached(env, cbuf));
}
static inline TexturePixelFormat ReadTexturePixelFormatCached(Environment& env,
const ConstBufferAddr& cbuf) {
if (IsAttributeHandle(cbuf)) {
return TexturePixelFormat::A8B8G8R8_UNORM;
}
return env.ReadTexturePixelFormat(GetTextureHandleCached(env, cbuf));
}
static inline bool IsTexturePixelFormatIntegerCached(Environment& env,
const ConstBufferAddr& cbuf) {
if (IsAttributeHandle(cbuf)) {
return false;
}
return env.IsTexturePixelFormatInteger(GetTextureHandleCached(env, cbuf));
}
@@ -466,15 +479,43 @@ std::optional<ConstBufferAddr> TryGetConstBuffer(const IR::Inst* inst, Environme
};
}
bool IsAttributeSourcedHandle(const IR::Value& handle) {
if (handle.IsImmediate()) {
return false;
}
const IR::Inst* inst{handle.InstRecursive()};
if (inst->GetOpcode() == IR::Opcode::BitCastU32F32 && !inst->Arg(0).IsImmediate()) {
inst = inst->Arg(0).InstRecursive();
}
return inst->GetOpcode() == IR::Opcode::GetAttribute ||
inst->GetOpcode() == IR::Opcode::GetAttributeU32;
}
TextureInst MakeInst(Environment& env, IR::Block* block, IR::Inst& inst, const HostTranslateInfo& host_info) {
ConstBufferAddr addr;
if (IsBindless(inst)) {
const std::optional<ConstBufferAddr> track_addr{TrackCached(inst.Arg(0), env, host_info)};
if (!track_addr) {
throw NotImplementedException("Failed to track bindless texture constant buffer");
} else {
if (track_addr) {
addr = *track_addr;
} else if (host_info.support_attribute_texture_handle &&
IsAttributeSourcedHandle(inst.Arg(0))) {
// The handle travels with the vertices (sprite batches pick the texture per quad), so
// there is no constant buffer to read it from. The host binds the handle of each run
// of vertices that share one.
addr = ConstBufferAddr{
.index = ATTRIBUTE_HANDLE_CBUF_INDEX,
.offset = 0,
.shift_left = 0,
.secondary_index = 0,
.secondary_offset = 0,
.secondary_shift_left = 0,
.dynamic_offset = {},
.count = 1,
.has_secondary = false,
};
} else {
throw NotImplementedException("Failed to track bindless texture constant buffer");
}
} else {
addr = ConstBufferAddr{
+4
View File
@@ -38,6 +38,10 @@ enum class TextureType : u32 {
};
constexpr u32 NUM_TEXTURE_TYPES = 9;
/// Constant buffer index marking a texture handle that the shader receives as a vertex attribute
/// instead of reading it from a constant buffer. The host supplies the handle of each draw run.
constexpr u32 ATTRIBUTE_HANDLE_CBUF_INDEX = 0xFFFF;
enum class TexturePixelFormat {
A8B8G8R8_UNORM,
A8B8G8R8_SNORM,
@@ -276,6 +276,9 @@ GraphicsPipeline::GraphicsPipeline(
num_textures += Shader::NumDescriptors(info->texture_descriptors);
num_image_elements += Shader::NumDescriptors(info->texture_descriptors);
num_image_elements += Shader::NumDescriptors(info->image_descriptors);
for (const auto& desc : info->texture_descriptors) {
uses_attribute_handle |= desc.cbuf_index == Shader::ATTRIBUTE_HANDLE_CBUF_INDEX;
}
num_descriptor_entries += NumDescriptorEntries(*info);
}
fragment_has_color0_output = stage_infos[NUM_STAGES - 1].stores_frag_color[0];
@@ -375,6 +378,9 @@ bool GraphicsPipeline::ConfigureImpl(bool is_indexed) {
}
const auto& cbufs{maxwell3d->state.shader_stages[stage].const_buffers};
const auto read_handle{[&](const auto& desc, u32 index) {
if (desc.cbuf_index == Shader::ATTRIBUTE_HANDLE_CBUF_INDEX) {
return TexturePair(attribute_handle, via_header_index);
}
ASSERT(cbufs[desc.cbuf_index].enabled);
const u32 index_offset{index << desc.size_shift};
const u32 offset{desc.cbuf_offset + index_offset};
@@ -110,6 +110,16 @@ public:
return configure_func(this, is_indexed);
}
/// True when a shader samples a texture whose handle arrives as a vertex attribute.
[[nodiscard]] bool UsesAttributeHandle() const noexcept {
return uses_attribute_handle;
}
/// Handle bound to those textures by the next Configure.
void SetAttributeHandle(u32 handle) noexcept {
attribute_handle = handle;
}
[[nodiscard]] GraphicsPipeline* Next(const GraphicsPipelineCacheKey& current_key) noexcept {
if (key == current_key) {
return this;
@@ -168,6 +178,8 @@ private:
u32 num_descriptor_entries{};
size_t num_image_elements{};
u32 num_textures{};
u32 attribute_handle{};
bool uses_attribute_handle{};
bool fragment_has_color0_output{};
vk::DescriptorSetLayout descriptor_set_layout;
@@ -491,6 +491,7 @@ PipelineCache::PipelineCache(Tegra::MaxwellDeviceMemoryManager& device_memory_,
.support_geometry_shader_passthrough = device.IsNvGeometryShaderPassthroughSupported(),
.support_conditional_barrier = device.SupportsConditionalBarriers(),
.single_lane_geometry_subgroups = !profile.SupportsSubgroupStage(Shader::Stage::Geometry),
.support_attribute_texture_handle = true,
};
host_info.ApplyDescriptorLimitPolicy();
@@ -248,6 +248,15 @@ void RasterizerVulkan::PrepareDraw(bool is_indexed, Func&& draw_func) {
std::scoped_lock lock{buffer_cache.mutex, texture_cache.mutex};
// update engine as channel may be different.
pipeline->SetEngine(maxwell3d, gpu_memory);
attribute_handle_pipeline = nullptr;
attribute_handle_runs.clear();
if (split_attribute_handles && pipeline->UsesAttributeHandle()) {
CollectAttributeHandleRuns(is_indexed);
if (!attribute_handle_runs.empty()) {
attribute_handle_pipeline = pipeline;
pipeline->SetAttributeHandle(attribute_handle_runs.front().handle);
}
}
if (!pipeline->Configure(is_indexed))
return;
@@ -259,22 +268,110 @@ void RasterizerVulkan::PrepareDraw(bool is_indexed, Func&& draw_func) {
draw_func();
}
void RasterizerVulkan::CollectAttributeHandleRuns(bool is_indexed) {
using VertexAttribute = Maxwell::VertexAttribute;
const auto& regs = maxwell3d->regs;
const auto& draw_state = maxwell3d->draw_manager.draw_state;
const u32 total = is_indexed ? draw_state.index_buffer.count : draw_state.vertex_buffer.count;
if (total == 0) {
return;
}
// The handle is an integer attribute; sprite batches repeat it on every vertex of a quad.
const VertexAttribute* source{};
for (const auto& attr : regs.vertex_attrib_format) {
if (attr.constant || attr.size == VertexAttribute::Size::Invalid) {
continue;
}
if (attr.type == VertexAttribute::Type::UInt || attr.type == VertexAttribute::Type::SInt) {
source = &attr;
break;
}
}
const auto& stream = regs.vertex_streams[source ? source->buffer.Value() : 0];
if (!source || !stream.IsEnabled()) {
attribute_handle_runs.push_back({0, total, 0});
return;
}
// Only plain triangle lists are split; anything else keeps the handle of its first vertex.
const u32 primitive_size =
draw_state.topology == Maxwell::PrimitiveTopology::Triangles ? 3 : total;
const GPUVAddr base_address = stream.Address() + source->offset;
const auto read_handle = [&](u32 position) {
u32 vertex;
if (is_indexed) {
const auto& index_buffer = draw_state.index_buffer;
const GPUVAddr address =
index_buffer.IndexStart() + size_t{position} * index_buffer.FormatSizeInBytes();
u32 index{};
gpu_memory->ReadBlockUnsafe(address, &index, index_buffer.FormatSizeInBytes());
vertex = static_cast<u32>(draw_state.base_index + index);
} else {
vertex = draw_state.vertex_buffer.first + position;
}
return gpu_memory->Read<u32>(base_address + static_cast<GPUVAddr>(vertex) * stream.stride);
};
for (u32 position = 0; position + primitive_size <= total; position += primitive_size) {
const u32 handle = read_handle(position);
if (!attribute_handle_runs.empty() && attribute_handle_runs.back().handle == handle) {
attribute_handle_runs.back().count += primitive_size;
} else {
attribute_handle_runs.push_back({position, primitive_size, handle});
}
}
if (attribute_handle_runs.empty()) {
attribute_handle_runs.push_back({0, total, read_handle(0)});
} else {
const u32 covered = attribute_handle_runs.back().first + attribute_handle_runs.back().count;
if (covered < total) {
attribute_handle_runs.back().count += total - covered;
}
}
}
void RasterizerVulkan::Draw(bool is_indexed, u32 instance_count) {
split_attribute_handles = true;
SCOPE_EXIT {
split_attribute_handles = false;
};
PrepareDraw(is_indexed, [this, is_indexed, instance_count] {
const auto& draw_state = maxwell3d->draw_manager.draw_state;
const u32 num_instances{instance_count};
const DrawParams draw_params{MakeDrawParams(draw_state, num_instances, is_indexed)};
scheduler.Record([draw_params](vk::CommandBuffer cmdbuf) {
if (draw_params.is_indexed) {
cmdbuf.DrawIndexed(draw_params.num_vertices, draw_params.num_instances,
draw_params.first_index, draw_params.base_vertex,
draw_params.base_instance);
} else {
cmdbuf.Draw(draw_params.num_vertices, draw_params.num_instances,
draw_params.base_vertex, draw_params.base_instance);
const auto record_draw = [this](const DrawParams& params) {
scheduler.Record([params](vk::CommandBuffer cmdbuf) {
if (params.is_indexed) {
cmdbuf.DrawIndexed(params.num_vertices, params.num_instances,
params.first_index, params.base_vertex,
params.base_instance);
} else {
cmdbuf.Draw(params.num_vertices, params.num_instances, params.base_vertex,
params.base_instance);
}
});
};
if (attribute_handle_pipeline && attribute_handle_runs.size() > 1) {
// The first run is already configured, each further one binds its own texture.
for (size_t i = 0; i < attribute_handle_runs.size(); ++i) {
const AttributeHandleRun& run = attribute_handle_runs[i];
if (i != 0) {
attribute_handle_pipeline->SetAttributeHandle(run.handle);
if (!attribute_handle_pipeline->Configure(is_indexed)) {
continue;
}
}
DrawParams run_params = draw_params;
run_params.num_vertices = run.count;
if (is_indexed) {
run_params.first_index += run.first;
} else {
run_params.base_vertex += run.first;
}
record_draw(run_params);
}
});
} else {
record_draw(draw_params);
}
// Log draw call
if (GPU::Logging::IsActive() &&
@@ -157,6 +157,20 @@ private:
template <typename Func>
void PrepareDraw(bool is_indexed, Func&&);
/// A stretch of a draw whose vertices all carry the same texture handle.
struct AttributeHandleRun {
u32 first;
u32 count;
u32 handle;
};
/// Splits the current draw wherever the texture handle carried in the vertex data changes.
void CollectAttributeHandleRuns(bool is_indexed);
std::vector<AttributeHandleRun> attribute_handle_runs;
GraphicsPipeline* attribute_handle_pipeline{};
bool split_attribute_handles{};
void FlushWork();
void UpdateDynamicStates();
+4 -7
View File
@@ -2797,9 +2797,9 @@ void MainWindow::IncrementInstallProgress() {
void MainWindow::OnMenuInstallToNAND() {
const QString file_filter =
tr("Installable Switch File (*.nca *.nsp *.nsz *.xci *.xcz);;Nintendo Content Archive "
"(*.nca);;Nintendo Submission Package (*.nsp *.nsz);;NX Cartridge "
"Image (*.xci *.xcz)");
tr("Installable Switch File (*.nca *.nsp *.xci);;Nintendo Content Archive "
"(*.nca);;Nintendo Submission Package (*.nsp);;NX Cartridge "
"Image (*.xci)");
QStringList filenames = QFileDialog::getOpenFileNames(
this, tr("Install Files"), QString::fromStdString(UISettings::values.roms_path),
@@ -2856,10 +2856,7 @@ void MainWindow::OnMenuInstallToNAND() {
QFuture<ContentManager::InstallResult> future;
ContentManager::InstallResult result;
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)) {
if (file.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) {
const auto progress_callback = [this](size_t size, size_t progress) {
emit UpdateInstallProgress();
if (install_progress->wasCanceled()) {