diff --git a/CMakeLists.txt b/CMakeLists.txt index f647204a4b..fcd3fa19ae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -304,6 +304,16 @@ 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() diff --git a/cpmfile.json b/cpmfile.json index 9cac812b7a..a4d8f0b0e3 100644 --- a/cpmfile.json +++ b/cpmfile.json @@ -243,6 +243,27 @@ "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", diff --git a/docs/Options.md b/docs/Options.md index ece6bdeba6..03527d3464 100644 --- a/docs/Options.md +++ b/docs/Options.md @@ -78,6 +78,9 @@ 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 diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index cb1deceb96..70b9fc5434 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -249,6 +249,14 @@ 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) diff --git a/src/common/settings.h b/src/common/settings.h index d1f891607b..fad3b0ca78 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -256,7 +256,9 @@ struct Values { CpuBackend::Dynarmic, #endif CpuBackend::Dynarmic, -#ifdef HAS_NCE +#if defined(HAS_SCARLETNX) + CpuBackend::ScarletNx, +#elif defined(HAS_NCE) CpuBackend::Nce, #else CpuBackend::Dynarmic, diff --git a/src/common/settings_enums.h b/src/common/settings_enums.h index eaf3ec541d..1d7c465254 100644 --- a/src/common/settings_enums.h +++ b/src/common/settings_enums.h @@ -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); +ENUM(CpuBackend, Dynarmic, Nce, ScarletNx); ENUM(CpuAccuracy, Auto, Accurate, Unsafe, Paranoid, Debugging); ENUM(CpuClock, Normal, Boost, Overclock) ENUM(MemoryLayout, Memory_4Gb, Memory_6Gb, Memory_8Gb, Memory_10Gb, Memory_12Gb); diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 31f9931d3a..dce6bfa787 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1248,6 +1248,18 @@ 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 diff --git a/src/core/arm/scarletnx/arm_scarletnx_32.cpp b/src/core/arm/scarletnx/arm_scarletnx_32.cpp new file mode 100644 index 0000000000..b4a01d7499 --- /dev/null +++ b/src/core/arm/scarletnx/arm_scarletnx_32.cpp @@ -0,0 +1,227 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include + +#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 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 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(context); + self.m_svc = imm; + self.m_in_svc = true; + return 1; +} + +void ArmScarletNx32::Coprocessor(SnxArmCPU* cpu, u32 word) { + auto& self = *reinterpret_cast(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 args) const { + for (std::size_t i = 0; i < args.size(); i++) + args[i] = u32(m_ctx.r[i]); +} + +void ArmScarletNx32::SetSvcArguments(std::span 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 diff --git a/src/core/arm/scarletnx/arm_scarletnx_32.h b/src/core/arm/scarletnx/arm_scarletnx_32.h new file mode 100644 index 0000000000..ff82bef6ae --- /dev/null +++ b/src/core/arm/scarletnx/arm_scarletnx_32.h @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include + +#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 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 args) const override; + void SetSvcArguments(std::span 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 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 m_interrupt{}; + + ScarletNxFaultScope m_fault_scope{}; +}; + +} // namespace Core diff --git a/src/core/arm/scarletnx/arm_scarletnx_64.cpp b/src/core/arm/scarletnx/arm_scarletnx_64.cpp new file mode 100644 index 0000000000..56c48e2959 --- /dev/null +++ b/src/core/arm/scarletnx/arm_scarletnx_64.cpp @@ -0,0 +1,261 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include + +#include + +#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 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(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(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 args) const { + for (std::size_t i = 0; i < args.size(); i++) { + args[i] = m_ctx.r[i]; + } +} + +void ArmScarletNx64::SetSvcArguments(std::span 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 diff --git a/src/core/arm/scarletnx/arm_scarletnx_64.h b/src/core/arm/scarletnx/arm_scarletnx_64.h new file mode 100644 index 0000000000..322d072b03 --- /dev/null +++ b/src/core/arm/scarletnx/arm_scarletnx_64.h @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include + +#include + +#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 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 args) const override; + void SetSvcArguments(std::span 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(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 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 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 diff --git a/src/core/arm/scarletnx/scarletnx_api.h b/src/core/arm/scarletnx/scarletnx_api.h new file mode 100644 index 0000000000..234e59f350 --- /dev/null +++ b/src/core/arm/scarletnx/scarletnx_api.h @@ -0,0 +1,10 @@ +// 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 +} diff --git a/src/core/arm/scarletnx/scarletnx_process.cpp b/src/core/arm/scarletnx/scarletnx_process.cpp new file mode 100644 index 0000000000..cd7697ed2a --- /dev/null +++ b/src/core/arm/scarletnx/scarletnx_process.cpp @@ -0,0 +1,300 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include + +#include + +#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 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(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 MakeScarletNxInterface(std::shared_ptr process, bool is_64bit, std::size_t core_index) { + if (is_64bit) + return std::make_unique(std::move(process), core_index); + return std::make_unique(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::GetCodeRanges() const { + std::vector 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 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 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 diff --git a/src/core/arm/scarletnx/scarletnx_process.h b/src/core/arm/scarletnx/scarletnx_process.h new file mode 100644 index 0000000000..cb08a2ae2b --- /dev/null +++ b/src/core/arm/scarletnx/scarletnx_process.h @@ -0,0 +1,104 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include +#include + +#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 ranges; + u64 lo; + u64 hi; + snx_recompiled* mod; + bool retired; + }; + + std::vector GetCodeRanges() const; + const Module* FindModuleLocked(u64 pc) const; + snx_recompiled* RecompileRegionLocked(u64 pc); + + std::vector m_modules; + std::atomic 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 MakeScarletNxInterface(std::shared_ptr process, bool is_64bit, std::size_t core_index); + +} // namespace Core diff --git a/src/core/hle/kernel/k_process.cpp b/src/core/hle/kernel/k_process.cpp index 27d1b1bfc4..942bc9f52e 100644 --- a/src/core/hle/kernel/k_process.cpp +++ b/src/core/hle/kernel/k_process.cpp @@ -23,6 +23,9 @@ #ifdef HAS_NCE #include "core/arm/nce/arm_nce.h" #endif +#ifdef HAS_SCARLETNX +#include "core/arm/scarletnx/scarletnx_process.h" +#endif namespace Kernel { @@ -1311,6 +1314,13 @@ void KProcess::InitializeInterfaces(KernelCore& kernel) { for (size_t i = 0; i < Core::Hardware::NUM_CPU_CORES; i++) m_arm_interfaces[i] = std::make_unique(kernel.System(), true, i); } else +#endif +#ifdef HAS_SCARLETNX + if (Core::ScarletNxProcess::ShouldUse(kernel.System(), *this)) { + auto scarletnx = std::make_shared(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++) { diff --git a/src/qt_common/config/shared_translation.cpp b/src/qt_common/config/shared_translation.cpp index 09e64b98f0..e43fe8037a 100644 --- a/src/qt_common/config/shared_translation.cpp +++ b/src/qt_common/config/shared_translation.cpp @@ -449,7 +449,12 @@ std::unique_ptr ComboboxEnumeration(QObject* parent) { translations->insert({Settings::EnumMetadata::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::Index(), { diff --git a/src/yuzu/configuration/configure_cpu.cpp b/src/yuzu/configuration/configure_cpu.cpp index 8c7b15e4e0..3834816bab 100644 --- a/src/yuzu/configuration/configure_cpu.cpp +++ b/src/yuzu/configuration/configure_cpu.cpp @@ -34,7 +34,7 @@ ConfigureCpu::ConfigureCpu(const Core::System& system_, connect(backend_combobox, qOverload(&QComboBox::currentIndexChanged), this, &ConfigureCpu::UpdateGroup); -#ifdef HAS_NCE +#if defined(HAS_NCE) || defined(HAS_SCARLETNX) ui->backend_group->setVisible(true); #endif }