Compare commits

..

8 Commits

Author SHA1 Message Date
PavelBARABANOV 9bc3503525 Install NSZ Solid to NAND 2026-10-19 01:48:34 +03:00
PavelBARABANOV ea68a544cb no need 2026-10-18 22:21:52 +03:00
PavelBARABANOV c6f33de22b oops 2026-10-18 14:09:03 +03:00
PavelBARABANOV e77cc89bf5 license 2026-10-18 13:21:06 +03:00
PavelBARABANOV 17091b59f1 [core] Support block-compressed NSZ and XCZ 2026-10-18 13:14:14 +03:00
crueter e0d47f2790 [cmake] fix macOS compilation by forcing lz4 to be static (#4540)
Signed-off-by: crueter <crueter@eden-emu.dev>

- [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/4540
2026-10-16 21:41:11 +02:00
PavelBARABANOV d8c6c59b67 Fix Check Strings after 4523 (#4534)
- [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/4534
2026-10-16 01:17:53 +02:00
xbzk aaa0bcb9bf [patcher, ro, settings] runtime tls patch (#4492) optimization and nuke toggle (#4502) (#4523)
- [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.

-------------------
This is a follow up for the biomutant fix. By the time i knew it was shallow, coz i was running patchtext routine even for svc and modules, causing a routine which were needed to run 14 times to run 4000+ instead.
Since some perf loss was reported in 30 sep nightly, and it caused ac3r to not boot, we had added a temp toggle, which is now being removed as optimizations + fix are in.

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4523
Reviewed-by: lizzie <lizzie@eden-emu.dev>
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
2026-10-15 12:13:17 +02:00
27 changed files with 685 additions and 114 deletions
+1 -1
View File
@@ -24,7 +24,7 @@ else()
endif()
if (lz4_FOUND AND NOT TARGET lz4::lz4)
if (TARGET LZ4::lz4_shared)
if (TARGET LZ4::lz4_shared AND NOT YUZU_STATIC_BUILD)
add_library(lz4::lz4 ALIAS LZ4::lz4_shared)
elseif (TARGET LZ4::lz4_static)
add_library(lz4::lz4 ALIAS LZ4::lz4_static)
@@ -17,7 +17,6 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
USE_CUSTOM_CPU_TICKS("use_custom_cpu_ticks"),
SKIP_CPU_INNER_INVALIDATION("skip_cpu_inner_invalidation"),
NCE_INVALIDATION_GPU_READBACK("nce_invalidation_gpu_readback"),
NCE_RUNTIME_NRO_PATCH("nce_runtime_nro_patch"),
FIX_BLOOM_EFFECTS("fix_bloom_effects"),
EMULATE_BGR565("emulate_bgr565"),
RESCALE_HACK("rescale_hack"),
@@ -95,7 +94,7 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
ENABLE_QUICK_SETTINGS("enable_quick_settings");
// external fun isFrameSkippingEnabled(): Boolean
//external fun isFrameSkippingEnabled(): Boolean
external fun isFrameInterpolationEnabled(): Boolean
override fun getBoolean(needsGlobal: Boolean): Boolean =
@@ -866,13 +866,6 @@ abstract class SettingsItem(
descriptionId = R.string.nce_invalidation_gpu_readback_description
)
)
put(
SwitchSetting(
BooleanSetting.NCE_RUNTIME_NRO_PATCH,
titleId = R.string.nce_runtime_nro_patch,
descriptionId = R.string.nce_runtime_nro_patch_description
)
)
put(
SwitchSetting(
BooleanSetting.FIX_BLOOM_EFFECTS,
@@ -555,7 +555,6 @@ class SettingsFragmentPresenter(
add(BooleanSetting.SKIP_CPU_INNER_INVALIDATION.key)
add(BooleanSetting.NCE_INVALIDATION_GPU_READBACK.key)
add(BooleanSetting.NCE_RUNTIME_NRO_PATCH.key)
add(BooleanSetting.FIX_BLOOM_EFFECTS.key)
add(BooleanSetting.EMULATE_BGR565.key)
add(BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS.key)
@@ -100,7 +100,7 @@ class Game(
companion object {
val extensions: Set<String> = HashSet(
listOf("xci", "nsp", "nca", "nro")
listOf("xci", "nsp", "nca", "nro", "nsz", "xcz")
)
}
}
@@ -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<MinimalDocumentFile>,
@@ -589,8 +589,6 @@
<string name="skip_cpu_inner_invalidation_description">يتخطى بعض عمليات إبطال ذاكرة التخزين المؤقتة من جانب وحدة المعالجة المركزية أثناء تحديثات الذاكرة، مما يقلل من استخدام وحدة المعالجة المركزية ويحسن أداءها. قد يتسبب ذلك في حدوث أعطال أو تعطل في بعض الألعاب.</string>
<string name="nce_invalidation_gpu_readback">إعادة قراءة وحدة معالجة الرسومات قبل إبطال صلاحية NCE</string>
<string name="nce_invalidation_gpu_readback_description">إصلاح مؤقت. يعالج مشكلة تعطل لعبة Ender Magnolia (وغيرها من ألعاب UE). قد يؤدي إلى انخفاض في الأداء. يجري العمل على تحسينه.</string>
<string name="nce_runtime_nro_patch">تصحيح حالات NRO في وقت التشغيل لـ NCE</string>
<string name="nce_runtime_nro_patch_description">حل مؤقت. إصلاح مشكلة تشغيل لعبة Biomutant. قد يؤثر ذلك على الأداء. يجري العمل على تحسينه.</string>
<string name="fix_bloom_effects">إصلاح تأثيرات التوهج</string>
<string name="fix_bloom_effects_description">يقلل من ضبابية التوهج في LA/EOW (Adreno A6XX - A7XX/ Turnip)، ويزيل التوهج في Burnout. تحذير: قد يسبب تشوهات رسومية في ألعاب أخرى.</string>
<string name="emulate_bgr565">محاكاة BGR565</string>
@@ -579,8 +579,6 @@
<string name="skip_cpu_inner_invalidation_description">在記憶體更新期間跳過某些 CPU 端快取的失效處理,減少 CPU 使用率並提高其性能。可能會導致某些遊戲出現故障或當機</string>
<string name="nce_invalidation_gpu_readback">在 NCE 無效化前進行 GPU 回讀</string>
<string name="nce_invalidation_gpu_readback_description">(暫時性功能)修復 \"終結者瑪格諾莉亞:霧中綻放\" 及其它使用虛幻引擎之遊戲中的當機問題,可能會降低效能。此功能正在改善中</string>
<string name="nce_runtime_nro_patch">對 NCE Runtime 的 NRO 檔案進行修補</string>
<string name="nce_runtime_nro_patch_description">(暫時性功能)修復 \"Biomutant\" 的啟動問題,可能會對效能造成影響。此功能正在改善中</string>
<string name="fix_bloom_effects">修正光暈特效</string>
<string name="fix_bloom_effects_description">在 \"薩爾達傳說:織夢島\" 和 \"薩爾達傳說:智慧的再現\" 中減少光暈模糊的問題並移除 \"橫衝直撞:狂飆樂園重製版\" 中的光暈效果(適用於 Adreno A6XX - A7XX/ Turnip)。警告:可能會在其它遊戲中導致圖形異常</string>
<string name="emulate_bgr565">模擬 BGR565</string>
@@ -1197,4 +1195,4 @@
<!-- Licenses screen strings -->
<string name="licenses">授權</string>
<string name="license_fidelityfx_fsr_description">來自 AMD 的升級圖像品質</string>
</resources>
</resources>
@@ -595,8 +595,6 @@
<string name="skip_cpu_inner_invalidation_description">Skips certain CPU-side cache invalidations during memory updates, reducing CPU usage and improving it\'s performance. This may cause glitches or crashes on some games.</string>
<string name="nce_invalidation_gpu_readback">GPU Readback Before NCE Invalidation</string>
<string name="nce_invalidation_gpu_readback_description">Temporary. Fix Ender Magnolia (and other UE titles) crashes. May reduce performance. Improvement in progress.</string>
<string name="nce_runtime_nro_patch">Patch Runtime NROs for NCE</string>
<string name="nce_runtime_nro_patch_description">Temporary. Fix Biomutant boot. May impact performance. Improvement in progress.</string>
<string name="fix_bloom_effects">Fix Bloom Effects</string>
<string name="fix_bloom_effects_description">Reduces bloom blur in LA/EOW (Adreno A6XX - A7XX/ Turnip), removes bloom in Burnout. Warning: may cause graphical artifacts in other games.</string>
<string name="emulate_bgr565">Emulate BGR565</string>
-2
View File
@@ -641,8 +641,6 @@ struct Values {
true};
SwitchableSetting<bool> nce_invalidation_gpu_readback{
linkage, false, "nce_invalidation_gpu_readback", Category::RendererHacks};
SwitchableSetting<bool> nce_runtime_nro_patch{
linkage, false, "nce_runtime_nro_patch", Category::RendererHacks};
SwitchableSetting<bool> async_presentation{linkage,
#ifdef __ANDROID__
true,
+9
View File
@@ -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<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,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<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,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
+42
View File
@@ -45,6 +45,23 @@ Patcher::Patcher() : c(m_patch_instructions), c_pre(m_patch_instructions_pre) {
WriteLoadContext(c_pre);
}
Patcher::Patcher(std::span<const u8> text) : c(m_patch_instructions), c_pre(m_patch_instructions_pre) {
modules.emplace_back();
curr_patch = &modules.back();
const auto words = std::span<const u32>{reinterpret_cast<const u32*>(text.data()), text.size() / sizeof(u32)};
for (u32 i = ModuleCodeIndex; i < static_cast<u32>(words.size()); ++i) {
const uintptr_t offset = i * sizeof(u32);
if (auto mrs = MRS{words[i]}; mrs.Verify() && mrs.GetRt() != 31 && (mrs.GetSystemReg() == TpidrEl0 || mrs.GetSystemReg() == TpidrroEl0)) {
BranchToPatch(offset);
const auto src_reg = mrs.GetSystemReg() == TpidrroEl0 ? oaknut::SystemReg::TPIDRRO_EL0 : oaknut::SystemReg::TPIDR_EL0;
WriteMrsHandler(offset + sizeof(u32), oaknut::XReg{static_cast<int>(mrs.GetRt())}, src_reg);
} else if (auto msr = MSR{words[i]}; msr.Verify() && msr.GetSystemReg() == TpidrEl0) {
BranchToPatch(offset);
WriteMsrHandler(offset + sizeof(u32), oaknut::XReg{static_cast<int>(msr.GetRt())});
}
}
}
Patcher::~Patcher() = default;
bool Patcher::PatchText(std::span<const u8> program_image, const Kernel::CodeSet::Segment& code) {
@@ -182,6 +199,31 @@ bool Patcher::PatchText(std::span<const u8> program_image, const Kernel::CodeSet
return true;
}
std::span<const u32> Patcher::RelocateRuntimeTls(
s64 patch_offset, std::vector<InstructionPatch>& out_patches) {
const auto& patch = modules.front();
const auto can_branch = [](s64 offset) {
return offset % 4 == 0 && offset >= -static_cast<s64>(MaxRelativeBranch) && offset < static_cast<s64>(MaxRelativeBranch);
};
out_patches.clear();
out_patches.reserve(patch.m_branch_to_patch_relocations.size());
for (const auto& rel : patch.m_branch_to_patch_relocations) {
const s64 offset = patch_offset + rel.patch_offset - static_cast<s64>(rel.module_offset);
if (!can_branch(offset)) {return {};}
u32 replacement;
oaknut::CodeGenerator rc{&replacement};
rc.B(offset);
out_patches.push_back({rel.module_offset, replacement});
}
for (const auto& rel : patch.m_branch_to_module_relocations) {
const s64 offset = static_cast<s64>(rel.module_offset) - patch_offset - rel.patch_offset;
if (!can_branch(offset)) {return {};}
oaknut::CodeGenerator rc{m_patch_instructions.data() + rel.patch_offset / sizeof(u32)};
rc.B(offset);
}
return m_patch_instructions;
}
bool Patcher::RelocateAndCopy(Common::ProcessAddress load_base, const Kernel::CodeSet::Segment& code, std::vector<u8>& program_image, EntryTrampolines* out_trampolines) {
const size_t patch_size = GetSectionSize();
const size_t pre_patch_size = GetPreSectionSize();
+7
View File
@@ -48,15 +48,22 @@ using ModuleTextAddress = u64;
using PatchTextAddress = u64;
using EntryTrampolines = ::Common::unordered_map<ModuleTextAddress, PatchTextAddress>;
struct InstructionPatch {
u64 offset;
u32 replacement;
};
class Patcher {
public:
void SetModuleID(const ModuleID& id) {
module_id = id;
}
explicit Patcher();
explicit Patcher(std::span<const u8> text);
~Patcher();
bool PatchText(std::span<const u8> program_image, const Kernel::CodeSet::Segment& code);
bool RelocateAndCopy(Common::ProcessAddress load_base, const Kernel::CodeSet::Segment& code, std::vector<u8>& program_image, EntryTrampolines* out_trampolines);
std::span<const u32> RelocateRuntimeTls(s64 patch_offset, std::vector<InstructionPatch>& out_patches);
size_t GetSectionSize() const noexcept;
size_t GetPreSectionSize() const noexcept;
+430
View File
@@ -0,0 +1,430 @@
// 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
@@ -0,0 +1,35 @@
// 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
+34 -1
View File
@@ -16,6 +16,7 @@
#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"
@@ -158,6 +159,25 @@ 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) {
@@ -811,7 +831,14 @@ std::vector<ContentProviderEntry> RegisteredCache::ListEntriesFilter(
}
static std::shared_ptr<NCA> 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;
}
@@ -1273,6 +1300,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,
+12
View File
@@ -12,6 +12,7 @@
#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"
@@ -246,6 +247,17 @@ 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,
+39 -23
View File
@@ -7,6 +7,7 @@
#include <openssl/err.h>
#include <openssl/evp.h>
#include "common/scope_exit.h"
#include "core/hle/kernel/k_process.h"
#include "core/hle/service/cmif_serialization.h"
@@ -425,36 +426,51 @@ public:
R_TRY(context->ValidateNro(std::addressof(nro_info->module_id), std::addressof(rx_size),
std::addressof(ro_size), std::addressof(rw_size),
nro_info->base_address, nro_size, bss_size));
R_TRY(SetNroPerms(context->GetProcess(), nro_info->base_address, rx_size, ro_size, rw_size + bss_size));
#ifdef HAS_NCE
if (Settings::values.nce_runtime_nro_patch.GetValue()) {
if (Settings::IsNceEnabled()) {
auto* process = context->GetProcess();
auto& memory = process->GetMemory();
std::vector<u8> image(total_size);
memory.ReadBlock(nro_info->base_address, image.data(), rx_size);
Kernel::CodeSet::Segment code{.size = static_cast<u32>(rx_size)};
Core::NCE::Patcher patch;
patch.PatchText(image, code);
patch.RelocateAndCopy(nro_info->base_address, code, image, nullptr);
const u64 patch_address = nro_info->base_address + total_size;
const size_t patch_size = patch.GetSectionSize();
if (Settings::IsNceEnabled()) {
auto* process = context->GetProcess();
auto& memory = process->GetMemory();
std::vector<u8> text(rx_size);
memory.ReadBlock(nro_info->base_address, text.data(), text.size());
Core::NCE::Patcher patch{text};
const size_t patch_size = patch.GetSectionSize();
if (patch_size != 0) {
constexpr auto permission = Kernel::Svc::MemoryPermission::ReadExecute;
auto* patch_memory = Kernel::KSharedMemory::Create(kernel);
SCOPE_EXIT { patch_memory->Close(kernel); };
R_TRY(patch_memory->Initialize(kernel, kernel.System().DeviceMemory(), process, permission, permission, patch_size));
std::memcpy(patch_memory->GetPointer(), image.data() + total_size, patch_size);
R_TRY(process->AddSharedMemory(kernel, patch_memory, patch_address, patch_size));
R_TRY(patch_memory->Map(*process, patch_address, patch_size, permission));
memory.WriteBlock(nro_info->base_address, image.data(), rx_size);
Kernel::KSharedMemory::Register(kernel, patch_memory);
const std::array<s64, 2> offsets{static_cast<s64>(total_size + Kernel::PageSize),-static_cast<s64>(patch_size + Kernel::PageSize)};
std::vector<Core::NCE::InstructionPatch> tls_patches;
std::span<const u32> instructions;
u64 patch_address = 0;
for (const s64 offset : offsets) {
instructions = patch.RelocateRuntimeTls(offset, tls_patches);
if (instructions.empty()) {continue;}
const u64 address = static_cast<u64>(static_cast<s64>(nro_info->base_address) + offset);
if (R_FAILED(patch_memory->Map(*process, address, patch_size, permission))) {continue;}
patch_address = address;
break;
}
R_UNLESS(patch_address != 0, RO::ResultOutOfAddressSpace);
const Result add_shared_memory_result = process->AddSharedMemory(kernel, patch_memory, patch_address, patch_size);
if (R_FAILED(add_shared_memory_result)) {
R_ASSERT(patch_memory->Unmap(*process, patch_address, patch_size));
}
R_TRY(add_shared_memory_result);
std::memcpy(patch_memory->GetPointer(), instructions.data(), instructions.size_bytes());
auto* ptr = reinterpret_cast<char*>(patch_address);
__builtin___clear_cache(ptr, ptr + instructions.size_bytes());
for (const auto& instruction : tls_patches) {
const u64 address = nro_info->base_address + instruction.offset;
memory.Write32(address, instruction.replacement);
ptr = reinterpret_cast<char*>(address);
__builtin___clear_cache(ptr, ptr + sizeof(u32));
}
}
}
#endif
// Set NRO perms.
R_TRY(SetNroPerms(context->GetProcess(), nro_info->base_address, rx_size, ro_size,
rw_size + bss_size));
context->SetNroInfoInUse(nro_info, true);
nro_info->code_size = rx_size + ro_size;
+14 -3
View File
@@ -82,12 +82,12 @@ std::shared_ptr<FileSys::NSP> OpenContainerAsNsp(FileSys::VirtualFile file, File
return nullptr;
}
if (type == FileType::NSP) {
if (type == FileType::NSP || type == FileType::NSZ) {
auto nsp = std::make_shared<FileSys::NSP>(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<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);
@@ -332,6 +342,7 @@ 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,7 +42,9 @@ enum class FileType {
NRO,
NCA,
NSP,
NSZ,
XCI,
XCZ,
NAX,
KIP,
DeconstructedRomDirectory,
+4 -2
View File
@@ -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
@@ -163,7 +163,9 @@ inline InstallResult InstallNSP(Core::System& system, FileSys::VfsFilesystem& vf
std::shared_ptr<FileSys::NSP> 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<FileSys::NSP>(file);
if (nsp->IsExtractedType()) {
return InstallResult::Failure;
@@ -198,10 +198,6 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QObject* parent) {
tr("GPU Readback Before NCE Invalidation"),
tr("Flushes GPU buffer data back to guest memory before NCE invalidates the whole "
"guest page, preserving it while the CPU store proceeds."));
INSERT(Settings, nce_runtime_nro_patch,
tr("Patch Runtime NROs for NCE"),
tr("Applies NCE code patches to runtime-loaded NRO modules."
"This is required for some games to run correctly."));
INSERT(Settings, vsync_mode, tr("VSync Mode:"),
tr("FIFO (VSync) does not drop frames or exhibit tearing but is limited by the screen "
"refresh rate.\nFIFO Relaxed allows tearing as it recovers from a slow down.\n"
+7 -3
View File
@@ -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.
+2 -1
View File
@@ -57,7 +57,8 @@ std::unique_ptr<EmuThread> 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.
+27 -55
View File
@@ -1738,64 +1738,36 @@ void BufferCache<P>::TouchBuffer(Buffer& buffer, BufferId buffer_id) noexcept {
template <class P>
bool BufferCache<P>::SynchronizeBuffer(Buffer& buffer, DAddr device_addr, u32 size) {
#ifdef __ANDROID__
const bool use_optimized_vertex_buffers = Settings::values.use_optimized_vertex_buffers.GetValue();
#else
constexpr bool use_optimized_vertex_buffers = true;
#endif
if (use_optimized_vertex_buffers) {
upload_copies.clear();
u64 total_size_bytes = 0;
u64 largest_copy = 0;
const DAddr buffer_start = buffer.cpu_addr_cached;
const auto add_upload = [&](DAddr start, DAddr end) {
if (start == end) return;
const u64 range_size = end - start;
upload_copies.push_back(BufferCopy{
.src_offset = total_size_bytes,
.dst_offset = start - buffer_start,
.size = range_size,
});
total_size_bytes += range_size;
largest_copy = (std::max)(largest_copy, range_size);
};
memory_tracker.ForEachUploadRange(device_addr, size, [&](u64 device_addr_out, u64 range_size) {
DAddr upload_start = device_addr_out;
gpu_modified_ranges.ForEachInRange(device_addr_out, range_size, [&](DAddr gpu_start, DAddr gpu_end) {
add_upload(upload_start, gpu_start);
upload_start = gpu_end;
});
add_upload(upload_start, device_addr_out + range_size);
upload_copies.clear();
u64 total_size_bytes = 0;
u64 largest_copy = 0;
const DAddr buffer_start = buffer.cpu_addr_cached;
const auto add_upload = [&](DAddr start, DAddr end) {
if (start == end) return;
const u64 range_size = end - start;
upload_copies.push_back(BufferCopy{
.src_offset = total_size_bytes,
.dst_offset = start - buffer_start,
.size = range_size,
});
if (total_size_bytes == 0) {
return true;
}
const std::span<BufferCopy> copies_span(upload_copies.data(), upload_copies.size());
UploadMemory(buffer, total_size_bytes, largest_copy, copies_span);
any_buffer_uploaded = true;
return false;
} else {
boost::container::small_vector<BufferCopy, 4> copies;
u64 total_size_bytes = 0;
u64 largest_copy = 0;
DAddr buffer_start = buffer.CpuAddr();
memory_tracker.ForEachUploadRange(device_addr, size, [&](u64 device_addr_out, u64 range_size) {
copies.push_back(BufferCopy{
.src_offset = total_size_bytes,
.dst_offset = device_addr_out - buffer_start,
.size = range_size,
});
total_size_bytes += range_size;
largest_copy = (std::max)(largest_copy, range_size);
total_size_bytes += range_size;
largest_copy = (std::max)(largest_copy, range_size);
};
memory_tracker.ForEachUploadRange(device_addr, size, [&](u64 device_addr_out, u64 range_size) {
DAddr upload_start = device_addr_out;
gpu_modified_ranges.ForEachInRange(device_addr_out, range_size, [&](DAddr gpu_start, DAddr gpu_end) {
add_upload(upload_start, gpu_start);
upload_start = gpu_end;
});
if (total_size_bytes == 0) {
return true;
}
const std::span<BufferCopy> copies_span(copies.data(), copies.size());
UploadMemory(buffer, total_size_bytes, largest_copy, copies_span);
return false;
add_upload(upload_start, device_addr_out + range_size);
});
if (total_size_bytes == 0) {
return true;
}
const std::span<BufferCopy> copies_span(upload_copies.data(), upload_copies.size());
UploadMemory(buffer, total_size_bytes, largest_copy, copies_span);
any_buffer_uploaded = true;
return false;
}
template <class P>