Compare commits

..

13 Commits

Author SHA1 Message Date
PavelBARABANOV 653b26ece6 display xcz as xcz, fix nsz unpacking (there was a regression in zstd) 2026-10-25 14:34:31 +01:00
PavelBARABANOV ebc5cca77c xcz always block 2026-10-25 14:34:31 +01:00
PavelBARABANOV 43e16573ab Revert "no need"
This reverts commit ea68a544cb.
2026-10-25 14:34:31 +01:00
PavelBARABANOV 0c6d17ac3a scanning from the update folder 2026-10-25 14:34:31 +01:00
PavelBARABANOV e15f2bf66b more 2026-10-25 14:34:31 +01:00
PavelBARABANOV 36fec60cd6 f 2026-10-25 14:34:31 +01:00
PavelBARABANOV 516d95513b fix 2026-10-25 14:34:31 +01:00
PavelBARABANOV 597bed3ca0 stub 2026-10-25 14:34:31 +01:00
PavelBARABANOV 6b2f234e45 Install NSZ Solid to NAND 2026-10-25 14:34:31 +01:00
PavelBARABANOV 99ad87d885 no need 2026-10-25 14:34:31 +01:00
PavelBARABANOV 3365126808 oops 2026-10-25 14:34:31 +01:00
PavelBARABANOV f86b6f0304 license 2026-10-25 14:34:31 +01:00
PavelBARABANOV 151bb88d2b [core] Support block-compressed NSZ and XCZ 2026-10-25 14:34:31 +01:00
33 changed files with 630 additions and 1187 deletions
-10
View File
@@ -304,16 +304,6 @@ if (ARCHITECTURE_arm64 AND (ANDROID OR LINUX))
add_compile_definitions(HAS_NCE=1)
endif()
set(ENABLE_SCARLETNX_DEFAULT ON)
if ((NOT LINUX AND NOT FREEBSD) OR (NOT CMAKE_C_COMPILER_ID MATCHES "Clang" OR NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang"))
set(ENABLE_SCARLETNX_DEFAULT OFF)
endif()
option(ENABLE_SCARLETNX "Enable the ScarletNX recompiler CPU backend (requires Clang and LLVM)" "${ENABLE_SCARLETNX_DEFAULT}")
if (ENABLE_SCARLETNX)
set(HAS_SCARLETNX 1)
add_compile_definitions(HAS_SCARLETNX=1)
endif()
if (YUZU_ROOM)
add_compile_definitions(YUZU_ROOM)
endif()
+2 -23
View File
@@ -243,27 +243,6 @@
"repo": "crosire/reshade",
"version": "v6.8.0"
},
"scarletnx": {
"bundled": true,
"hash": "93f7e6810644253d6396239092d5769c44b005fa7a17cf39c6d773050a19de74fca1f96bb6eec7cb40384eb158b2595c9e1171ad592d8eb8326fd31f5632f505",
"options": [
"SNX_BUILD_NX OFF",
"SNX_BUILD_GNUABI OFF",
"SNX_BUILD_3DS OFF",
"SNX_BUILD_VITA OFF",
"SNX_BUILD_AGB OFF",
"SNX_DEPLOY OFF",
"SNX_BUILD_TESTING OFF",
"SNX_BUILD_GUI OFF",
"SNX_MULTICALL OFF",
"SNX_TRACY OFF",
"SNX_RECOMPILER_SHARED OFF",
"SNX_LLVM_SHARED OFF"
],
"package": "ScarletNX",
"repo": "scarletnx/ScarletNX",
"version": "f16810ce9c"
},
"sdl3": {
"hash": "df5a323af7ac366661a3c0e887969c72584d232f3cc211419d59b0487b620b6b2859d4549c9e8df002ee489290062e466fcfddf7edc0872a37b1f2845e81c0f3",
"min_version": "3.2.10",
@@ -372,13 +351,13 @@
},
"zstd": {
"find_args": "MODULE",
"hash": "cc5ad4b119a9c2ea57f0b71eeff01113bb506e0d17000159c5409cb8236d22e38c52d5e9e97e7947a4bf1b2dfc44b6c503ab2d9aedbd59458435c6a2849cb029",
"hash": "2b006c3c110c38bd2f5896b42d36a38931c8eae261f30552f9a9fd04458d218d8f1a1425f117fd12af545ed77042e4b2d1a1a9b6286c4d0b9d74ef79f20a8f08",
"min_version": "1.5",
"options": [
"ZSTD_BUILD_SHARED OFF"
],
"repo": "facebook/zstd",
"source_subdir": "build/cmake",
"version": "b8d6101fba"
"version": "33618c8"
}
}
-3
View File
@@ -78,9 +78,6 @@ The following options are desktop only.
- `ENABLE_OPENGL` (ON) Enable the OpenGL graphics backend
- Unavailable on Windows/ARM64 and on Android
- You probably shouldn't turn this off.
- `ENABLE_SCARLETNX` (OFF) Enable the ScarletNX recompiler
- Requires Clang and LLVM's development files
- Needs multicore and fastmem
### Qt
-8
View File
@@ -249,14 +249,6 @@ if (YUZU_TESTS OR DYNARMIC_TESTS)
AddJsonPackage(catch2)
endif()
# ScarletNX
if (ENABLE_SCARLETNX)
AddJsonPackage(scarletnx)
if (TARGET scarletnx-recompiler)
set_target_properties(scarletnx-recompiler PROPERTIES SYSTEM ON)
endif()
endif()
# getopt
if (MSVC)
add_subdirectory(getopt)
@@ -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>,
@@ -76,7 +76,9 @@ jboolean Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getIsValid(JNIEnv* env, jobj
auto const file_type = loader->GetFileType();
if (file_type == Loader::FileType::Unknown || file_type == Loader::FileType::Error)
return false;
if ((file_type == Loader::FileType::NSP || file_type == Loader::FileType::XCI) && !Loader::IsBootableGameContainer(file, file_type))
if ((file_type == Loader::FileType::NSP || file_type == Loader::FileType::XCI ||
file_type == Loader::FileType::NSZ || file_type == Loader::FileType::XCZ) &&
!Loader::IsBootableGameContainer(file, file_type))
return false;
u64 program_id = 0;
return loader->ReadProgramId(program_id) == Loader::ResultStatus::Success;
+1 -3
View File
@@ -256,9 +256,7 @@ struct Values {
CpuBackend::Dynarmic,
#endif
CpuBackend::Dynarmic,
#if defined(HAS_SCARLETNX)
CpuBackend::ScarletNx,
#elif defined(HAS_NCE)
#ifdef HAS_NCE
CpuBackend::Nce,
#else
CpuBackend::Dynarmic,
+1 -1
View File
@@ -138,7 +138,7 @@ ENUM(RendererBackend, OpenGL_GLSL, Vulkan, Null, OpenGL_GLASM, OpenGL_SPIRV);
ENUM(GpuAccuracy, Low, High);
ENUM(DmaAccuracy, Default, Unsafe, Safe);
ENUM(GpuFenceBehavior, Default, Immediate, Balanced, Accurate);
ENUM(CpuBackend, Dynarmic, Nce, ScarletNx);
ENUM(CpuBackend, Dynarmic, Nce);
ENUM(CpuAccuracy, Auto, Accurate, Unsafe, Paranoid, Debugging);
ENUM(CpuClock, Normal, Boost, Overclock)
ENUM(MemoryLayout, Memory_4Gb, Memory_6Gb, Memory_8Gb, Memory_10Gb, Memory_12Gb);
+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 -12
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
@@ -1248,18 +1250,6 @@ if (HAS_NCE)
target_link_libraries(core PRIVATE merry::oaknut)
endif()
if (HAS_SCARLETNX)
target_sources(core PRIVATE
arm/scarletnx/arm_scarletnx_32.cpp
arm/scarletnx/arm_scarletnx_32.h
arm/scarletnx/arm_scarletnx_64.cpp
arm/scarletnx/arm_scarletnx_64.h
arm/scarletnx/scarletnx_api.h
arm/scarletnx/scarletnx_process.cpp
arm/scarletnx/scarletnx_process.h)
target_link_libraries(core PRIVATE ScarletNX::recompiler)
endif()
if (ARCHITECTURE_x86_64 OR ARCHITECTURE_arm64 OR ARCHITECTURE_riscv64 OR ARCHITECTURE_loongarch64)
target_sources(core PRIVATE
arm/dynarmic/arm_dynarmic.h
-227
View File
@@ -1,227 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <cstring>
#include <utility>
#include <atomic>
#include "common/logging.h"
#include "core/arm/scarletnx/arm_scarletnx_32.h"
#include "core/core.h"
#include "core/core_timing.h"
#include "core/hle/kernel/k_process.h"
namespace Core {
namespace {
// The exit SignalInterrupt sets, which no block sets itself.
constexpr u32 InterruptExit = 0x1000;
constexpr u32 UserMode = 0x10;
constexpr u32 ModeMask = 0x1F;
// FPSCR splits into FPSR and FPCR as in ArmDynarmic32.
std::pair<u32, u32> FpscrToFpsrFpcr(u32 fpscr) {
const u32 fpsr = (fpscr & 0xf8000000) | (fpscr & 0x80) | (fpscr & 0x1f);
const u32 fpcr = (fpscr & 0x7ff8000) | (fpscr & 0x1f00);
return {fpsr, fpcr};
}
u32 FpsrFpcrToFpscr(u64 fpsr, u64 fpcr) {
const auto [s, c] = FpscrToFpsrFpcr(u32(fpsr | fpcr));
return s | c;
}
} // namespace
ArmScarletNx32::ArmScarletNx32(std::shared_ptr<ScarletNxProcess> process, std::size_t core_index)
: ArmInterface{true}, m_process{std::move(process)}, m_core_index{core_index} {
m_rt.coproc = &Coprocessor;
m_cpu.parent = this;
m_cpu.cpu.mem = m_process->m_arena;
m_cpu.cpu.mem_size = 1ULL << 32;
m_cpu.cpu.rt = &m_rt;
m_fault_scope.memory = &m_process->m_process.GetMemory();
m_fault_scope.arena = m_process->m_arena;
m_fault_scope.arena_size = ScarletNxProcess::ArenaSize;
}
ArmScarletNx32::~ArmScarletNx32() {
snx_recompiled32_free(m_module);
}
int ArmScarletNx32::Svc(void* context, u32 imm, u64 number, const u64 arguments[6]) {
auto& self = *static_cast<ArmScarletNx32*>(context);
self.m_svc = imm;
self.m_in_svc = true;
return 1;
}
void ArmScarletNx32::Coprocessor(SnxArmCPU* cpu, u32 word) {
auto& self = *reinterpret_cast<Cpu32*>(cpu)->parent;
const u32 coproc = (word >> 8) & 0xF;
const u32 crm = word & 0xF;
if (coproc == 15 && (word & 0x0F100010) == 0x0E000010) {
// MCR p15
const u32 opc1 = (word >> 21) & 7;
const u32 crn = (word >> 16) & 0xF;
const u32 opc2 = (word >> 5) & 7;
if (opc1 == 0 && crn == 7 && crm == 5 && opc2 == 4) {
// CP15_FLUSH_PREFETCH_BUFFER
return;
}
if (opc1 == 0 && crn == 7 && crm == 10 && (opc2 == 4 || opc2 == 5)) {
// CP15_DATA_SYNC_BARRIER, CP15_DATA_MEMORY_BARRIER
std::atomic_thread_fence(std::memory_order_seq_cst);
return;
}
} else if (coproc == 15 && (word & 0x0FF00000) == 0x0C500000) {
// MRRC p15
const u32 opc1 = (word >> 4) & 0xF;
const u32 rt = (word >> 12) & 0xF;
const u32 rt2 = (word >> 16) & 0xF;
if (opc1 == 0 && crm == 14) {
// CNTPCT
const u64 ticks = self.m_process->m_system.CoreTiming().GetClockTicks();
cpu->r[rt] = u32(ticks);
cpu->r[rt2] = u32(ticks >> 32);
return;
}
}
// TPIDRURW and TPIDRURO, and undefined for anything else.
snx_arm_coproc_user(cpu, word);
ASSERT(cpu->exit == SNX_ARM_OK && "unhandled coproc");
}
HaltReason ArmScarletNx32::RunThread(Kernel::KThread* thread) {
if (m_interrupt.exchange(false)) {
return HaltReason::BreakLoop;
}
// The block cache only covers the code present when it was made.
const u64 generation = m_process->CodeGeneration();
if (!m_module || generation != m_code_generation) {
snx_recompiled32_free(m_module);
m_module = m_process->MakeModule32();
m_code_generation = generation;
}
LoadCpu();
m_in_svc = false;
m_fault_scope.last_fault = ~u64{};
SetScarletNxFaultScope(&m_fault_scope);
if (sigsetjmp(m_fault_scope.env, 1) != 0) {
SetScarletNxFaultScope(nullptr);
LOG_CRITICAL(Core_ARM, "invalid memory access at {:#x}",
m_fault_scope.fault_address);
SaveCpu();
return HaltReason::PrefetchAbort;
}
const int exit =
m_module ? snx_recompiled32_run(m_module, &m_cpu.cpu, &Svc, this) : SNX_ARM_BAD_ENTRY;
SetScarletNxFaultScope(nullptr);
SaveCpu();
return Halt(exit);
}
HaltReason ArmScarletNx32::Halt(int exit) {
if (m_in_svc) {
return HaltReason::SupervisorCall;
}
u32 const pc = m_cpu.cpu.r[15];
u32 const syndrome = m_cpu.cpu.syndrome;
switch (u32(exit)) {
case InterruptExit:
m_interrupt.store(false);
return HaltReason::BreakLoop;
case SNX_ARM_BREAKPOINT:
// As Dynarmic does without a debugger: report it and go on.
LOG_CRITICAL(Core_ARM, "BKPT #{:#x} at {:#x}", syndrome, pc);
return HaltReason::BreakLoop;
default:
LOG_CRITICAL(Core_ARM, "A32 exit {} at {:#x} (syndrome {:#x})", exit, pc, syndrome);
return HaltReason::PrefetchAbort;
}
}
HaltReason ArmScarletNx32::StepThread(Kernel::KThread* thread) {
// Never reached: the debugger selects Dynarmic.
LOG_ERROR(Core_ARM, "ScarletNX cannot single step");
return HaltReason::BreakLoop;
}
void ArmScarletNx32::LoadCpu() {
auto& cpu = m_cpu.cpu;
static_assert(sizeof(cpu.d) <= sizeof(m_ctx.v));
for (std::size_t i = 0; i < 16; i++)
cpu.r[i] = u32(m_ctx.r[i]);
cpu.cpsr = (m_ctx.pstate & ~ModeMask) | UserMode;
std::memcpy(cpu.d, m_ctx.v.data(), sizeof(cpu.d));
cpu.fpscr = FpsrFpcrToFpscr(m_ctx.fpsr, m_ctx.fpcr);
cpu.tpidrurw = u32(m_ctx.tpidr);
cpu.tpidruro = m_tpidrro;
cpu.exclusive_valid = 0;
}
void ArmScarletNx32::SaveCpu() {
const auto& cpu = m_cpu.cpu;
for (std::size_t i = 0; i < 16; i++)
m_ctx.r[i] = cpu.r[i];
m_ctx.fp = cpu.r[11];
m_ctx.sp = cpu.r[13];
m_ctx.lr = cpu.r[14];
m_ctx.pc = cpu.r[15];
m_ctx.pstate = cpu.cpsr;
std::memcpy(m_ctx.v.data(), cpu.d, sizeof(cpu.d));
const auto [fpsr, fpcr] = FpscrToFpsrFpcr(cpu.fpscr);
m_ctx.fpsr = fpsr;
m_ctx.fpcr = fpcr;
m_ctx.tpidr = cpu.tpidrurw;
}
void ArmScarletNx32::GetContext(Kernel::Svc::ThreadContext& ctx) const {
ctx = m_ctx;
}
void ArmScarletNx32::SetContext(const Kernel::Svc::ThreadContext& ctx) {
m_ctx = ctx;
}
void ArmScarletNx32::SetTpidrroEl0(u64 value) {
m_tpidrro = u32(value);
}
void ArmScarletNx32::GetSvcArguments(std::span<uint64_t, 8> args) const {
for (std::size_t i = 0; i < args.size(); i++)
args[i] = u32(m_ctx.r[i]);
}
void ArmScarletNx32::SetSvcArguments(std::span<const uint64_t, 8> args) {
for (std::size_t i = 0; i < args.size(); i++)
m_ctx.r[i] = u32(args[i]);
}
u32 ArmScarletNx32::GetSvcNumber() const {
return m_svc;
}
void ArmScarletNx32::SignalInterrupt(Kernel::KThread* thread) {
// The run loop checks the exit between blocks. A block's own exit takes precedence.
m_interrupt.store(true);
u32 expected = SNX_ARM_OK;
__atomic_compare_exchange_n(&m_cpu.cpu.exit, &expected, InterruptExit, false, __ATOMIC_SEQ_CST, __ATOMIC_RELAXED);
}
void ArmScarletNx32::ClearInstructionCache() {
m_process->ClearInstructionCache();
}
void ArmScarletNx32::InvalidateCacheRange(u64 addr, std::size_t size) {
m_process->InvalidateCacheRange(addr, size);
}
} // namespace Core
-81
View File
@@ -1,81 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <atomic>
#include <memory>
#include "core/arm/arm_interface.h"
#include "core/arm/scarletnx/scarletnx_api.h"
#include "core/arm/scarletnx/scarletnx_process.h"
namespace Core {
/// Runs A32 and Thumb code through ScarletNX's block compiler. Blocks return to the run loop, so
/// an SVC ends the run with r15 past it. The block cache is not thread-safe, so each core has its
/// own, rebuilt when the process' code changes.
class ArmScarletNx32 final : public ArmInterface {
public:
ArmScarletNx32(std::shared_ptr<ScarletNxProcess> process, std::size_t core_index);
~ArmScarletNx32() override;
Architecture GetArchitecture() const override {
return Architecture::AArch32;
}
HaltReason RunThread(Kernel::KThread* thread) override;
HaltReason StepThread(Kernel::KThread* thread) override;
void GetContext(Kernel::Svc::ThreadContext& ctx) const override;
void SetContext(const Kernel::Svc::ThreadContext& ctx) override;
void SetTpidrroEl0(u64 value) override;
void GetSvcArguments(std::span<uint64_t, 8> args) const override;
void SetSvcArguments(std::span<const uint64_t, 8> args) override;
u32 GetSvcNumber() const override;
void SignalInterrupt(Kernel::KThread* thread) override;
void ClearInstructionCache() override;
void InvalidateCacheRange(u64 addr, std::size_t size) override;
protected:
const Kernel::DebugWatchpoint* HaltedWatchpoint() const override {
return nullptr;
}
void RewindBreakpointInstruction() override {}
private:
// The CPU first, so the coprocessor hook can find its interface.
struct Cpu32 {
SnxArmCPU cpu;
ArmScarletNx32* parent;
};
static int Svc(void* context, u32 imm, u64 number, const u64 arguments[6]);
static void Coprocessor(SnxArmCPU* cpu, u32 word);
HaltReason Halt(int exit);
void LoadCpu();
void SaveCpu();
std::shared_ptr<ScarletNxProcess> m_process;
std::size_t m_core_index{};
snx_recompiled32* m_module{};
u64 m_code_generation{~u64{}};
// The context the kernel reads and writes; m_cpu holds it while guest code runs.
Kernel::Svc::ThreadContext m_ctx{};
u32 m_tpidrro{};
Cpu32 m_cpu{};
SnxArmRt m_rt{};
bool m_in_svc{};
u32 m_svc{};
std::atomic<bool> m_interrupt{};
ScarletNxFaultScope m_fault_scope{};
};
} // namespace Core
-261
View File
@@ -1,261 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <cstring>
#include <sys/mman.h>
#include "common/assert.h"
#include "common/literals.h"
#include "common/logging.h"
#include "core/arm/scarletnx/arm_scarletnx_64.h"
#include "core/hle/kernel/k_thread.h"
#include "core/hle/kernel/k_process.h"
namespace Core {
namespace {
using namespace Common::Literals;
namespace ctx = boost::context::detail;
// Reserved, not committed: guest calls nest on it, and recompiling a region runs on it too.
constexpr std::size_t StackSize = 32_MiB;
constexpr std::size_t GuardSize = 64_KiB;
bool SameContext(const Kernel::Svc::ThreadContext a, const Kernel::Svc::ThreadContext b, bool in_svc) {
// An SVC returns its results in x0-x7.
for (std::size_t i = in_svc ? 8 : 0; i < a.r.size(); i++)
if (a.r[i] != b.r[i])
return false;
return a.fp == b.fp && a.lr == b.lr && a.sp == b.sp && a.pc == b.pc
&& (a.pstate >> 28) == (b.pstate >> 28) && a.v == b.v && a.fpcr == b.fpcr
&& a.fpsr == b.fpsr && a.tpidr == b.tpidr;
}
} // namespace
const RtAPI* ArmScarletNx64::Runtime() {
// this incurs a cost each time we call Runtime() -- but we only ever call it once (ideally)
static const RtAPI rt = [] {
RtAPI api;
snx_runtime_defaults(&api);
api.svc = &Svc;
api.undef = &Undefined;
api.brk = &Breakpoint;
api.poll = &Poll;
api.miss = &Miss;
api.code_changed = &CodeChanged;
api.ticks = &ScarletNxTicks;
return api;
}();
return &rt;
}
ArmScarletNx64::ArmScarletNx64(std::shared_ptr<ScarletNxProcess> process, std::size_t core_index)
: ArmInterface{true}
, m_process{std::move(process)}, m_core_index{core_index}
{
// can't use virtualbuffer...
void* const stack = mmap(nullptr, StackSize, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
ASSERT_MSG(stack != MAP_FAILED, "cannot allocate a guest stack");
m_stack = static_cast<u8*>(stack);
mprotect(m_stack, GuardSize, PROT_NONE);
m_cpu.mem = m_process->m_arena;
m_cpu.rt = Runtime();
m_cpu.thread = this;
m_fault_scope.memory = &m_process->m_process.GetMemory();
m_fault_scope.arena = m_process->m_arena;
m_fault_scope.arena_size = ScarletNxProcess::ArenaSize;
}
ArmScarletNx64::~ArmScarletNx64() {
munmap(m_stack, StackSize);
}
void ArmScarletNx64::Svc(CPU* cpu, u32 imm) {
auto& self = From(cpu);
self.m_svc = imm;
self.Yield(HaltReason::SupervisorCall, true, true);
}
void ArmScarletNx64::Undefined(CPU* cpu, u64 pc, u32 word) {
// A reserved encoding rejected from inside an instruction, which then goes on.
if (cpu->pc != pc) {
LOG_ERROR(Core_ARM, "reserved encoding {:08X} at {:#x}", word, pc);
return;
}
LOG_CRITICAL(Core_ARM, "cannot execute {:08X} at {:#x}", word, pc);
From(cpu).Yield(HaltReason::PrefetchAbort, false, false);
UNREACHABLE();
}
void ArmScarletNx64::Breakpoint(CPU* cpu, u64 pc, u32 imm) {
// As Dynarmic does without a debugger: report it and go on.
LOG_CRITICAL(Core_ARM, "BRK #{:#x} at {:#x}", imm, pc);
cpu->pc = pc + 4;
}
void ArmScarletNx64::Poll(CPU* cpu) {
auto& self = From(cpu);
__atomic_store_n(&cpu->poll, 0, __ATOMIC_RELAXED);
if (self.m_interrupt.exchange(false))
self.Yield(HaltReason::BreakLoop, true, false);
}
void ArmScarletNx64::Miss(CPU* cpu, u64 pc) {
auto& process = *From(cpu).m_process;
const auto function = process.Lookup(pc);
if (!function) {
cpu->pc = pc;
Undefined(cpu, pc, 0);
return;
}
// The first region was recompiled by this lookup: dispatch through its table from now on.
if (snx_recompiled* const module = cpu->disp ? nullptr : process.FindModule(pc))
snx_recompiled_bind(module, cpu);
function(cpu, pc);
}
void ArmScarletNx64::CodeChanged(CPU* cpu, u64 addr) {
constexpr u64 ICacheLineSize = 64;
From(cpu).m_process->InvalidateCacheRange(addr & ~(ICacheLineSize - 1), ICacheLineSize);
}
void ArmScarletNx64::Entry(ctx::transfer_t transfer) {
auto* const self = static_cast<ArmScarletNx64*>(transfer.data);
self->m_host = transfer.fctx;
self->Execute();
}
void ArmScarletNx64::Execute() {
if (sigsetjmp(m_fault_scope.env, 1) != 0) {
LOG_CRITICAL(Core_ARM, "invalid memory access at {:#x}", m_fault_scope.fault_address);
Yield(HaltReason::PrefetchAbort, false, false);
UNREACHABLE();
}
// Every guest return to below the first frame comes back here.
while (true)
snx_lookup(&m_cpu, m_cpu.pc)(&m_cpu, m_cpu.pc);
}
void ArmScarletNx64::Yield(HaltReason reason, bool resumable, bool in_svc) {
m_halt = reason;
m_resumable = resumable;
m_suspended_in_svc = in_svc;
m_host = ctx::jump_fcontext(m_host, nullptr).fctx;
}
void ArmScarletNx64::ResetGuest() {
m_guest = ctx::make_fcontext(m_stack + StackSize, StackSize - GuardSize, &Entry);
if (snx_recompiled* const module = m_process->FindModule(m_cpu.pc))
snx_recompiled_bind(module, &m_cpu);
}
HaltReason ArmScarletNx64::RunThread(Kernel::KThread* thread) {
if (m_interrupt.exchange(false))
return HaltReason::BreakLoop;
bool const resume = m_resumable && m_suspended_thread_id == thread->GetThreadId() && SameContext(m_ctx, m_suspended_ctx, m_suspended_in_svc);
LoadCpu();
if (!resume)
ResetGuest();
m_resumable = false;
m_fault_scope.last_fault = ~u64{};
SetScarletNxFaultScope(&m_fault_scope);
m_guest = ctx::jump_fcontext(m_guest, this).fctx;
SetScarletNxFaultScope(nullptr);
SaveCpu();
if (m_resumable) {
m_suspended_thread_id = thread->GetThreadId();
m_suspended_ctx = m_ctx;
}
return m_halt;
}
HaltReason ArmScarletNx64::StepThread(Kernel::KThread* thread) {
// Never reached: the debugger selects Dynarmic.
LOG_ERROR(Core_ARM, "ScarletNX cannot single step");
return HaltReason::BreakLoop;
}
void ArmScarletNx64::LoadCpu() {
static_assert(sizeof(m_cpu.v) == sizeof(m_ctx.v));
for (std::size_t i = 0; i < m_ctx.r.size(); i++)
m_cpu.x[i] = m_ctx.r[i];
m_cpu.x[29] = m_ctx.fp;
m_cpu.x[30] = m_ctx.lr;
m_cpu.x[31] = m_ctx.sp;
m_cpu.pc = m_ctx.pc;
set_nzcv(&m_cpu, m_ctx.pstate >> 28);
std::memcpy(m_cpu.v, m_ctx.v.data(), sizeof(m_cpu.v));
m_cpu.fpcr = m_ctx.fpcr;
m_cpu.fpsr = m_ctx.fpsr;
m_cpu.tpidr = m_ctx.tpidr;
m_cpu.tpidrro = m_tpidrro;
m_cpu.excl_on = 0;
}
void ArmScarletNx64::SaveCpu() {
for (std::size_t i = 0; i < m_ctx.r.size(); i++) {
m_ctx.r[i] = m_cpu.x[i];
}
m_ctx.fp = m_cpu.x[29];
m_ctx.lr = m_cpu.x[30];
m_ctx.sp = m_cpu.x[31];
m_ctx.pc = m_cpu.pc;
m_ctx.pstate = get_nzcv(&m_cpu);
std::memcpy(m_ctx.v.data(), m_cpu.v, sizeof(m_cpu.v));
m_ctx.fpcr = m_cpu.fpcr;
m_ctx.fpsr = m_cpu.fpsr;
m_ctx.tpidr = m_cpu.tpidr;
}
void ArmScarletNx64::GetContext(Kernel::Svc::ThreadContext& ctx) const {
ctx = m_ctx;
}
void ArmScarletNx64::SetContext(const Kernel::Svc::ThreadContext& ctx) {
m_ctx = ctx;
}
void ArmScarletNx64::SetTpidrroEl0(u64 value) {
m_tpidrro = value;
}
void ArmScarletNx64::GetSvcArguments(std::span<uint64_t, 8> args) const {
for (std::size_t i = 0; i < args.size(); i++) {
args[i] = m_ctx.r[i];
}
}
void ArmScarletNx64::SetSvcArguments(std::span<const uint64_t, 8> args) {
for (std::size_t i = 0; i < args.size(); i++) {
m_ctx.r[i] = args[i];
}
}
u32 ArmScarletNx64::GetSvcNumber() const {
return m_svc;
}
void ArmScarletNx64::SignalInterrupt(Kernel::KThread* thread) {
// Generated code reads poll on backward branches and calls Poll, which yields.
m_interrupt.store(true);
__atomic_store_n(&m_cpu.poll, 1, __ATOMIC_RELAXED);
}
void ArmScarletNx64::ClearInstructionCache() {
m_process->ClearInstructionCache();
}
void ArmScarletNx64::InvalidateCacheRange(u64 addr, std::size_t size) {
m_process->InvalidateCacheRange(addr, size);
}
} // namespace Core
-100
View File
@@ -1,100 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <atomic>
#include <memory>
#include <boost/context/detail/fcontext.hpp>
#include "core/arm/arm_interface.h"
#include "core/arm/scarletnx/scarletnx_api.h"
#include "core/arm/scarletnx/scarletnx_process.h"
namespace Core {
/// Runs A64 code recompiled by ScarletNX.
///
/// A recompiled guest function is a host function that returns when the guest function returns,
/// so guest calls nest on the host stack and an SVC cannot return from the generated code. Guest
/// code therefore runs on a coroutine of this core, which the SVC and interrupt hooks suspend.
/// When the kernel next runs the same thread with the same context, the coroutine resumes;
/// otherwise its host frames are dropped and execution restarts at the new pc through dispatch.
class ArmScarletNx64 final : public ArmInterface {
public:
ArmScarletNx64(std::shared_ptr<ScarletNxProcess> process, std::size_t core_index);
~ArmScarletNx64() override;
Architecture GetArchitecture() const override {
return Architecture::AArch64;
}
HaltReason RunThread(Kernel::KThread* thread) override;
HaltReason StepThread(Kernel::KThread* thread) override;
void GetContext(Kernel::Svc::ThreadContext& ctx) const override;
void SetContext(const Kernel::Svc::ThreadContext& ctx) override;
void SetTpidrroEl0(u64 value) override;
void GetSvcArguments(std::span<uint64_t, 8> args) const override;
void SetSvcArguments(std::span<const uint64_t, 8> args) override;
u32 GetSvcNumber() const override;
void SignalInterrupt(Kernel::KThread* thread) override;
void ClearInstructionCache() override;
void InvalidateCacheRange(u64 addr, std::size_t size) override;
protected:
const Kernel::DebugWatchpoint* HaltedWatchpoint() const override {
return nullptr;
}
void RewindBreakpointInstruction() override {}
private:
static ArmScarletNx64& From(CPU* cpu) {
return *static_cast<ArmScarletNx64*>(cpu->thread);
}
static const RtAPI* Runtime();
// Runtime hooks called by the generated code.
static void Svc(CPU* cpu, u32 imm);
static void Undefined(CPU* cpu, u64 pc, u32 word);
static void Breakpoint(CPU* cpu, u64 pc, u32 imm);
static void Poll(CPU* cpu);
static void Miss(CPU* cpu, u64 pc);
static void CodeChanged(CPU* cpu, u64 addr);
static void Entry(boost::context::detail::transfer_t transfer);
[[noreturn]] void Execute();
void Yield(HaltReason reason, bool resumable, bool in_svc);
void ResetGuest();
void LoadCpu();
void SaveCpu();
std::shared_ptr<ScarletNxProcess> m_process;
std::size_t m_core_index{};
// The context the kernel reads and writes; m_cpu holds it while guest code runs.
Kernel::Svc::ThreadContext m_ctx{};
u64 m_tpidrro{};
CPU m_cpu{};
HaltReason m_halt{};
u32 m_svc{};
std::atomic<bool> m_interrupt{};
// The coroutine, and what it was suspended in, when it can be resumed.
boost::context::detail::fcontext_t m_guest{};
boost::context::detail::fcontext_t m_host{};
u8* m_stack{};
bool m_resumable{};
bool m_suspended_in_svc{};
u64 m_suspended_thread_id{};
Kernel::Svc::ThreadContext m_suspended_ctx{};
ScarletNxFaultScope m_fault_scope{};
};
} // namespace Core
-10
View File
@@ -1,10 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
// snx_recompiler.h includes cpu.h and arm.h before its own extern "C" block, so the whole
// header needs C linkage for the runtime library's functions to resolve.
extern "C" {
#include <scarletnx/snx_recompiler.h>
}
@@ -1,300 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <algorithm>
#include <mutex>
#include <signal.h>
#include "common/fs/path_util.h"
#include "common/logging.h"
#include "common/settings.h"
#include "core/arm/scarletnx/arm_scarletnx_32.h"
#include "core/arm/scarletnx/arm_scarletnx_64.h"
#include "core/arm/scarletnx/scarletnx_api.h"
#include "core/arm/scarletnx/scarletnx_process.h"
#include "core/core.h"
#include "core/core_timing.h"
#include "core/device_memory.h"
#include "core/hle/kernel/k_process.h"
#include "core/hle/kernel/kernel.h"
#include "core/memory.h"
namespace Core {
namespace {
// ScarletNX refuses dispatch tables spanning more than 1 GiB of guest code.
constexpr u64 MaxModuleSpan = 1ULL << 30;
constexpr u64 A32Limit = 1ULL << 32;
std::atomic<Timing::CoreTiming*> g_core_timing{};
// Needs be TLS because SNX expects like, more than 4 threads (host == guest threads)
// and well -- quite impervious to "emulated" threads... maybe one day this can be removed
thread_local ScarletNxFaultScope* g_fault_scope{};
static struct {
struct sigaction old_sigsegv;
struct sigaction old_sigbus;
std::once_flag installed;
} g_snx_data;
bool Overlaps(const ScarletNxProcess::CodeRange& range, u64 addr, u64 size) {
return addr < range.addr + range.size && range.addr < addr + size;
}
void ChainSignal(int sig, siginfo_t* info, void* raw_context) {
const struct sigaction& old = sig == SIGSEGV ? g_snx_data.old_sigsegv : g_snx_data.old_sigbus;
if (old.sa_flags & SA_SIGINFO) {
old.sa_sigaction(sig, info, raw_context);
} else if (old.sa_handler == SIG_DFL) {
// The faulting instruction runs again and takes the default action.
signal(sig, SIG_DFL);
} else if (old.sa_handler != SIG_IGN) {
old.sa_handler(sig);
}
}
void FaultHandler(int sig, siginfo_t* info, void* raw_context) {
ScarletNxFaultScope* const scope = g_fault_scope;
u8* const addr = static_cast<u8*>(info->si_addr);
if (!scope || addr < scope->arena || addr >= scope->arena + scope->arena_size) {
ChainSignal(sig, info, raw_context);
return;
}
// Pages the rasterizer protects fault on access: invalidate them and retry. A second fault at
// the same addr means retrying will not help.
const u64 guest_address = u64(addr - scope->arena);
if (guest_address != scope->last_fault) {
scope->last_fault = guest_address;
if (scope->memory->InvalidateNCE(guest_address & ~Memory::YUZU_PAGEMASK, Memory::YUZU_PAGESIZE)) {
return;
}
}
scope->fault_address = guest_address;
siglongjmp(scope->env, 1);
}
} // namespace
void InstallScarletNxFaultHandler() {
std::call_once(g_snx_data.installed, [] {
struct sigaction action {};
action.sa_flags = SA_SIGINFO;
action.sa_sigaction = &FaultHandler;
sigemptyset(&action.sa_mask);
sigaction(SIGSEGV, &action, &g_snx_data.old_sigsegv);
sigaction(SIGBUS, &action, &g_snx_data.old_sigbus);
});
}
void SetScarletNxFaultScope(ScarletNxFaultScope* scope) {
g_fault_scope = scope;
}
u64 ScarletNxTicks() {
return g_core_timing.load(std::memory_order_relaxed)->GetClockTicks();
}
std::unique_ptr<ArmInterface> MakeScarletNxInterface(std::shared_ptr<ScarletNxProcess> process, bool is_64bit, std::size_t core_index) {
if (is_64bit)
return std::make_unique<ArmScarletNx64>(std::move(process), core_index);
return std::make_unique<ArmScarletNx32>(std::move(process), core_index);
}
ScarletNxProcess::ScarletNxProcess(System& system, Kernel::KProcess& process)
: m_system{system}
, m_process{process}
, m_arena{system.DeviceMemory().buffer.VirtualBasePointer()}
{
InstallScarletNxFaultHandler();
g_core_timing.store(&system.CoreTiming(), std::memory_order_relaxed);
m_recompiler = snx_recompiler_new(SNX_RECOMPILER_FLAG_LLVM);
ASSERT(m_recompiler && "cannot create the recompiler");
// Linked modules are kept by a hash of their code, so later launches skip the recompile.
const auto cache = Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir) / "scarletnx";
if (snx_recompiler_set_cache(m_recompiler, Common::FS::PathToUTF8String(cache).c_str()) != 0)
LOG_WARNING(Core_ARM, "no code cache: {}", snx_recompiler_error(m_recompiler));
}
ScarletNxProcess::~ScarletNxProcess() {
for (auto& mod : m_modules)
snx_recompiled_free(mod.mod);
snx_recompiler_free(m_recompiler);
}
bool ScarletNxProcess::ShouldUse(System& system, const Kernel::KProcess& process) {
if (Settings::values.cpu_backend.GetValue() != Settings::CpuBackend::ScarletNx)
return false;
auto const f = [](auto& s) {
LOG_WARNING(Core_ARM, "snx: {}", s);
return false;
};
if (!process.IsApplication())
return f("no qlaunch yet");
if (!system.Kernel().IsMulticore())
return f("single core mode needs cycle counting");
if (system.DebuggerEnabled())
return f("the debugger needs stepping and watchpoints");
/// ScarletNX expects flat memory, so it will only work with fastmem
if (!Settings::IsFastmemEnabled() || !system.DeviceMemory().buffer.VirtualBasePointer())
return f("guest memory must be mapped by fastmem");
if (snx_recompiler_abi() != SNX_ABI)
return f("the library was built with a different ABI");
if (!snx_recompiler_has(SNX_RECOMPILER_FLAG_LLVM))
return f("LLVM cannot target this host");
return true;
}
std::vector<ScarletNxProcess::CodeRange> ScarletNxProcess::GetCodeRanges() const {
std::vector<CodeRange> r;
const auto& page_table = m_process.GetPageTable().GetBasePageTable();
u64 addr = GetInteger(page_table.GetAddressSpaceStart());
while (true) {
Kernel::KMemoryInfo info;
Kernel::Svc::PageInfo page_info;
if (page_table.QueryInfo(&info, &page_info, addr).IsError())
break;
if (True(info.GetPermission() & Kernel::KMemoryPermission::UserExecute)) {
if (!r.empty() && r.back().addr + r.back().size == info.GetAddress()) {
r.back().size += info.GetSize();
} else {
r.push_back({info.GetAddress(), info.GetSize()});
}
}
// The block past the addr space ends at 0.
if (info.GetEndAddress() <= addr)
break;
addr = info.GetEndAddress();
}
return r;
}
const ScarletNxProcess::Module* ScarletNxProcess::FindModuleLocked(u64 pc) const {
for (const auto& mod : m_modules) {
if (mod.retired || pc < mod.lo || pc >= mod.hi)
continue;
for (const auto& range : mod.ranges)
if (pc >= range.addr && pc < range.addr + range.size)
return &mod;
}
return nullptr;
}
snx_recompiled* ScarletNxProcess::RecompileRegionLocked(u64 pc) {
const auto r = GetCodeRanges();
const auto it = std::ranges::find_if(r, [pc](const CodeRange& range) {
return pc >= range.addr && pc < range.addr + range.size;
});
if (it == r.end())
return nullptr;
const auto is_covered = [this](const CodeRange& range) {
return std::ranges::any_of(m_modules, [&](const Module& mod) {
return !mod.retired && std::ranges::any_of(mod.ranges, [&](const CodeRange& cr) {
return Overlaps(cr, range.addr, range.size);
});
});
};
// Recompile as many neighbouring regions as one dispatch table can cover.
auto first = size_t(it - r.begin());
auto last = first;
const auto end_of = [&](size_t i) { return r[i].addr + r[i].size; };
while (first > 0 && !is_covered(r[first - 1]) && end_of(last) - r[first - 1].addr <= MaxModuleSpan)
--first;
while (last + 1 < r.size() && !is_covered(r[last + 1])
&& end_of(last + 1) - r[first].addr <= MaxModuleSpan)
++last;
Module mod{};
mod.ranges.assign(r.begin() + first, r.begin() + last + 1);
mod.lo = r[first].addr;
mod.hi = end_of(last);
std::vector<snx_code_range> code;
u64 code_size = 0;
for (const auto& range : mod.ranges) {
code.push_back({range.addr, m_arena + range.addr, range.size});
code_size += range.size;
}
LOG_INFO(Core_ARM, "recompiling {} KiB of code at {:#x}-{:#x}", code_size / 1024, mod.lo, mod.hi);
mod.mod = snx_recompile_ranges(m_recompiler, code.data(), code.size(), &pc, 1);
if (mod.mod) {
LOG_INFO(Core_ARM, "recompiled {} functions", snx_recompiled_functions(mod.mod));
} else {
// Kept, so the region is not recompiled again on every miss.
LOG_ERROR(Core_ARM, "recompilation failed: {}", snx_recompiler_error(m_recompiler));
}
m_modules.push_back(std::move(mod));
return m_modules.back().mod;
}
ScarletNxProcess::SnxGuestFn ScarletNxProcess::Lookup(u64 pc) {
snx_recompiled* mod;
{
std::scoped_lock lk{m_mutex};
const Module* const known = FindModuleLocked(pc);
mod = known ? known->mod : RecompileRegionLocked(pc);
}
if (!mod)
return nullptr;
// Waits for the mod's JIT when discovery missed the code at pc.
return snx_recompiled_lookup(mod, pc);
}
snx_recompiled* ScarletNxProcess::FindModule(u64 pc) {
std::scoped_lock lk{m_mutex};
const Module* const mod = FindModuleLocked(pc);
return mod ? mod->mod : nullptr;
}
snx_recompiled32* ScarletNxProcess::MakeModule32() {
std::scoped_lock lk{m_mutex};
std::vector<snx_code_range> code;
for (const auto& range : GetCodeRanges()) {
if (range.addr >= A32Limit) {
continue;
}
code.push_back({range.addr, nullptr, std::min(range.size, A32Limit - range.addr)});
}
if (code.empty())
return nullptr;
snx_recompiled32* const mod = snx_recompile32_ranges(m_recompiler, code.data(), code.size());
if (!mod)
LOG_ERROR(Core_ARM, "cannot prepare A32 code: {}", snx_recompiler_error(m_recompiler));
return mod;
}
void ScarletNxProcess::InvalidateCacheRange(u64 addr, u64 size) {
std::scoped_lock lk{m_mutex};
for (auto& mod : m_modules) {
if (mod.retired || !std::ranges::any_of(mod.ranges, [&](const CodeRange& range) {
return Overlaps(range, addr, size);
}))
continue;
// Running code diverts out of the retired functions, and the region is recompiled from
// the new code when it is next reached.
if (mod.mod)
snx_recompiled_invalidate(mod.mod, addr, size, nullptr, nullptr);
mod.retired = true;
}
m_code_generation.fetch_add(1, std::memory_order_release);
}
void ScarletNxProcess::ClearInstructionCache() {
std::scoped_lock lk{m_mutex};
for (auto& mod : m_modules) {
if (!mod.retired && mod.mod)
snx_recompiled_invalidate(mod.mod, mod.lo, mod.hi - mod.lo, nullptr, nullptr);
mod.retired = true;
}
m_code_generation.fetch_add(1, std::memory_order_release);
}
} // namespace Core
-104
View File
@@ -1,104 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <atomic>
#include <csetjmp>
#include <memory>
#include <mutex>
#include <vector>
#include "common/common_types.h"
struct CPU;
struct snx_recompiler;
struct snx_recompiled;
struct snx_recompiled32;
namespace Kernel {
class KProcess;
}
namespace Core::Memory {
class Memory;
}
namespace Core {
class ArmInterface;
class System;
class ScarletNxProcess {
public:
using SnxGuestFn = void (*)(CPU*, u64);
// DeviceMemory reserves 39 bits of address space for the fastmem arena.
static constexpr u64 ArenaSize = 1ULL << 39;
struct CodeRange {
u64 addr;
u64 size;
};
ScarletNxProcess(System& system, Kernel::KProcess& process);
~ScarletNxProcess();
static bool ShouldUse(System& system, const Kernel::KProcess& process);
SnxGuestFn Lookup(u64 pc);
snx_recompiled* FindModule(u64 pc);
snx_recompiled32* MakeModule32();
void InvalidateCacheRange(u64 addr, u64 size);
void ClearInstructionCache();
/// @brief general gate for when we are about to invalidate a given block aka. trigger regen
u64 CodeGeneration() const {
return m_code_generation.load(std::memory_order_acquire);
}
struct Module {
std::vector<CodeRange> ranges;
u64 lo;
u64 hi;
snx_recompiled* mod;
bool retired;
};
std::vector<CodeRange> GetCodeRanges() const;
const Module* FindModuleLocked(u64 pc) const;
snx_recompiled* RecompileRegionLocked(u64 pc);
std::vector<Module> m_modules;
std::atomic<u64> m_code_generation{};
// TODO: remove parent ref?
System& m_system;
Kernel::KProcess& m_process;
u8* m_arena{};
snx_recompiler* m_recompiler{};
// ScarletNX's recompiler is used by one thread at a time.
std::mutex m_mutex;
};
/// Recovers from host faults in guest memory on the thread running guest code: a fault on a
/// page the rasterizer protects is resolved and retried, any other jumps to env.
struct ScarletNxFaultScope {
sigjmp_buf env;
Memory::Memory* memory{};
u8* arena{};
u64 arena_size{};
u64 last_fault{~0ull};
u64 fault_address{};
};
void InstallScarletNxFaultHandler();
void SetScarletNxFaultScope(ScarletNxFaultScope* scope);
/// CNTPCT_EL0 of the emulated system.
u64 ScarletNxTicks();
std::unique_ptr<ArmInterface> MakeScarletNxInterface(std::shared_ptr<ScarletNxProcess> process, bool is_64bit, std::size_t core_index);
} // namespace Core
+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
+43 -3
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,
@@ -1415,8 +1448,9 @@ void ExternalContentProvider::ScanDirectory(const VirtualDir& dir) {
}
const auto extension = Common::ToLower(filename.substr(dot_pos + 1));
if (extension == "nsp") {
// .xcz is an XCI container, but its updates only work through the NSP/PFS0 path
// (DecompressNCZ), not ProcessXCI. So route .xcz here on purpose.
if (extension == "nsp" || extension == "nsz" || extension == "xcz") {
ProcessNSP(file);
} else if (extension == "xci") {
ProcessXCI(file);
@@ -1434,6 +1468,12 @@ void ExternalContentProvider::ProcessNSP(const VirtualFile& file) {
return;
}
// Reject solid NCZ (.nsz without -B): Eden only supports block-compressed NCZ.
if (!ContainerIsFullyBlockCompressed(nsp)) {
LOG_DEBUG(Service_FS, "Skipping solid-compressed container: {}", file->GetName());
return;
}
LOG_DEBUG(Service_FS, "Processing NSP file: {}", file->GetName());
AddExternalEntriesFromContainer(nsp, entries, versions, multi_version_entries);
}
+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,
-10
View File
@@ -23,9 +23,6 @@
#ifdef HAS_NCE
#include "core/arm/nce/arm_nce.h"
#endif
#ifdef HAS_SCARLETNX
#include "core/arm/scarletnx/scarletnx_process.h"
#endif
namespace Kernel {
@@ -1314,13 +1311,6 @@ void KProcess::InitializeInterfaces(KernelCore& kernel) {
for (size_t i = 0; i < Core::Hardware::NUM_CPU_CORES; i++)
m_arm_interfaces[i] = std::make_unique<Core::ArmNce>(kernel.System(), true, i);
} else
#endif
#ifdef HAS_SCARLETNX
if (Core::ScarletNxProcess::ShouldUse(kernel.System(), *this)) {
auto scarletnx = std::make_shared<Core::ScarletNxProcess>(kernel.System(), *this);
for (size_t i = 0; i < Core::Hardware::NUM_CPU_CORES; i++)
m_arm_interfaces[i] = Core::MakeScarletNxInterface(scarletnx, this->Is64Bit(), i);
} else
#endif
if (this->Is64Bit()) {
for (size_t i = 0; i < Core::Hardware::NUM_CPU_CORES; i++) {
+39 -9
View File
@@ -82,23 +82,27 @@ 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;
if (xci.GetStatus() == ResultStatus::Success) {
auto secure_nsp = xci.GetSecurePartitionNSP();
if (secure_nsp != nullptr && secure_nsp->GetStatus() == ResultStatus::Success) {
return secure_nsp;
}
}
auto secure_nsp = xci.GetSecurePartitionNSP();
if (secure_nsp == nullptr || secure_nsp->GetStatus() != ResultStatus::Success) {
return nullptr;
// PFS0-shaped .xcz (NSZ-style container) does not parse as XCI; fall back to NSP.
if (type == FileType::XCZ) {
auto nsp = std::make_shared<FileSys::NSP>(file, program_id, program_index);
return nsp->GetStatus() == ResultStatus::Success ? nsp : nullptr;
}
return secure_nsp;
return nullptr;
}
return nullptr;
@@ -144,7 +148,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 +185,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 +206,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:
@@ -326,12 +339,29 @@ static std::unique_ptr<AppLoader> GetFileLoader(Core::System& system, FileSys::V
system.GetContentProvider(), program_id,
program_index);
// NCZ-compressed XCI container. XCI-shaped .xcz goes to the XCI loader, while
// PFS0-shaped (NSZ-style) .xcz containers fall back to the NSP loader.
case FileType::XCZ: {
FileSys::XCI probe{file, program_id, program_index};
if (probe.GetStatus() == ResultStatus::Success &&
probe.GetSecurePartitionNSP() != nullptr) {
return std::make_unique<AppLoader_XCI>(std::move(file),
system.GetFileSystemController(),
system.GetContentProvider(), program_id,
program_index);
}
return std::make_unique<AppLoader_NSP>(std::move(file), system.GetFileSystemController(),
system.GetContentProvider(), program_id,
program_index);
}
// NX NAX (NintendoAesXts) file format.
case FileType::NAX:
return std::make_unique<AppLoader_NAX>(std::move(file));
// 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,
+11
View File
@@ -56,6 +56,17 @@ 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;
}
// PFS0-shaped .xcz containers are handled by this loader too; report XCZ so the game
// list does not show them as NSP. XCI-shaped .xcz is identified by AppLoader_XCI.
if (nsp_file->GetName().ends_with(".xcz")) {
return FileType::XCZ;
}
const FileSys::NSP nsp(nsp_file);
if (nsp.GetStatus() != ResultStatus::Success) {
+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;
@@ -449,12 +449,7 @@ std::unique_ptr<ComboboxTranslationMap> ComboboxEnumeration(QObject* parent) {
translations->insert({Settings::EnumMetadata<Settings::CpuBackend>::Index(),
{
PAIR(CpuBackend, Dynarmic, tr("Dynarmic")),
#ifdef HAS_NCE
PAIR(CpuBackend, Nce, tr("NCE")),
#endif
#ifdef HAS_SCARLETNX
PAIR(CpuBackend, ScarletNx, tr("ScarletNX")),
#endif
}});
translations->insert({Settings::EnumMetadata<Settings::FullscreenMode>::Index(),
{
+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.
+1 -1
View File
@@ -34,7 +34,7 @@ ConfigureCpu::ConfigureCpu(const Core::System& system_,
connect(backend_combobox, qOverload<int>(&QComboBox::currentIndexChanged), this,
&ConfigureCpu::UpdateGroup);
#if defined(HAS_NCE) || defined(HAS_SCARLETNX)
#ifdef HAS_NCE
ui->backend_group->setVisible(true);
#endif
}
+10 -7
View File
@@ -2797,9 +2797,9 @@ void MainWindow::IncrementInstallProgress() {
void MainWindow::OnMenuInstallToNAND() {
const QString file_filter =
tr("Installable Switch File (*.nca *.nsp *.xci);;Nintendo Content Archive "
"(*.nca);;Nintendo Submission Package (*.nsp);;NX Cartridge "
"Image (*.xci)");
tr("Installable Switch File (*.nca *.nsp *.nsz *.xci *.xcz);;Nintendo Content Archive "
"(*.nca);;Nintendo Submission Package (*.nsp *.nsz);;NX Cartridge "
"Image (*.xci *.xcz)");
QStringList filenames = QFileDialog::getOpenFileNames(
this, tr("Install Files"), QString::fromStdString(UISettings::values.roms_path),
@@ -2856,7 +2856,10 @@ void MainWindow::OnMenuInstallToNAND() {
QFuture<ContentManager::InstallResult> future;
ContentManager::InstallResult result;
if (file.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) {
if (file.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive) ||
file.endsWith(QStringLiteral("nsz"), Qt::CaseInsensitive) ||
file.endsWith(QStringLiteral("xci"), Qt::CaseInsensitive) ||
file.endsWith(QStringLiteral("xcz"), Qt::CaseInsensitive)) {
const auto progress_callback = [this](size_t size, size_t progress) {
emit UpdateInstallProgress();
if (install_progress->wasCanceled()) {
@@ -3661,12 +3664,12 @@ void MainWindow::OnToggleGraphicsAPI() {
api = Settings::RendererBackend::OpenGL_GLSL;
break;
case Settings::RendererBackend::OpenGL_GLSL:
api = Settings::RendererBackend::OpenGL_GLSL;
break;
case Settings::RendererBackend::OpenGL_SPIRV:
api = Settings::RendererBackend::OpenGL_GLASM;
break;
case Settings::RendererBackend::OpenGL_GLASM:
api = Settings::RendererBackend::OpenGL_SPIRV;
break;
case Settings::RendererBackend::OpenGL_SPIRV:
api = Settings::RendererBackend::Null;
break;
#else