Compare commits

...

4 Commits

Author SHA1 Message Date
lizzie aa6178c0c9 [scarletnx] Add CPU backend
Signed-off-by: lizzie <lizzie@eden-emu.dev>
Assisted-by: Claude Opus 5.5 <claude@anthropic.com>
2026-10-25 17:53:53 +00:00
PavelBARABANOV eb88a40a8e [qt] fix graphics API toggle getting stuck on OpenGL_GLSL (#4577)
- [x] I have read and followed the [Contribution Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/CONTRIBUTING.md#code-contributions).
- [x] I have read and followed the [AI Policy](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/AI.md).
- [x] I have read and followed the [Coding Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/Coding.md) to the best of my ability.

-------------------

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4577
Reviewed-by: crueter <crueter@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
2026-10-25 16:06:46 +01:00
PavelBARABANOV 305f40100d Fix aarch and android builds (#4576)
- [x] I have read and followed the [Contribution Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/CONTRIBUTING.md#code-contributions).
- [x] I have read and followed the [AI Policy](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/AI.md).
- [x] I have read and followed the [Coding Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/Coding.md) to the best of my ability.

-------------------
credit: John and Crueter

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4576
Reviewed-by: crueter <crueter@eden-emu.dev>
2026-10-25 10:03:16 +01:00
John 989768aa46 Fix [core, gpu, threads] Multithreading refactor (#4254) (#4548)
Performance loss regression fixed in Link's Awakening, Castlevania, Super Mario RPG, etc

Tested by Mr. Knobs on Android and myself on Linux

Multicore ON may be fixed for some users using Intel CPUs.
Games include: FF Tactics, Shredder's Revenge, Unity Engine Games.

- [x] I have read and followed the [Contribution Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/CONTRIBUTING.md#code-contributions).
- [x] I have read and followed the [AI Policy](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/AI.md).
- [x] I have read and followed the [Coding Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/Coding.md) to the best of my ability.

-------------------

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4548
Reviewed-by: Maufeat <sahyno1996@gmail.com>
Reviewed-by: lizzie <lizzie@eden-emu.dev>
2026-10-19 15:58:04 +02:00
21 changed files with 1198 additions and 8 deletions
+10
View File
@@ -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()
+21
View File
@@ -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",
+3
View File
@@ -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
+8
View File
@@ -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)
+1 -1
View File
@@ -259,7 +259,7 @@ android {
externalNativeBuild {
cmake {
version = "3.31.6"
version = "3.31.5"
path = file("${edenDir}/CMakeLists.txt")
}
}
+3 -1
View File
@@ -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,
+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);
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);
+12
View File
@@ -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
@@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
/* SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project */
/* SPDX-License-Identifier: GPL-2.0-or-later */
@@ -5,9 +8,22 @@
#define __ASSEMBLY__
// Newer Linux UAPI headers check __ASSEMBLER__ instead of __ASSEMBLY__ to hide C declarations.
// Without it, asm-generic/signal.h redeclares sigset_t and clashes with glibc's. Only define it
// around these includes, since glibc headers also look at __ASSEMBLER__.
#ifndef __ASSEMBLER__
#define __ASSEMBLER__
#define EDEN_NCE_UNDEF_ASSEMBLER
#endif
#include <asm-generic/signal.h>
#include <asm-generic/unistd.h>
#ifdef EDEN_NCE_UNDEF_ASSEMBLER
#undef __ASSEMBLER__
#undef EDEN_NCE_UNDEF_ASSEMBLER
#endif
#define ReturnToRunCodeByExceptionLevelChangeSignal SIGUSR2
#define BreakFromRunCodeSignal SIGURG
#define GuestAccessFaultSignal SIGSEGV
+227
View File
@@ -0,0 +1,227 @@
// 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
@@ -0,0 +1,81 @@
// 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
@@ -0,0 +1,261 @@
// 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
@@ -0,0 +1,100 @@
// 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
@@ -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 <scarletnx/snx_recompiler.h>
}
@@ -0,0 +1,300 @@
// 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
@@ -0,0 +1,104 @@
// 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
+10
View File
@@ -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<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++) {
@@ -57,8 +57,21 @@ u32 HardwareComposer::ComposeLocked(f32* out_speed_scale, Display& display,
// Set default speed limit to 100%.
*out_speed_scale = 1.0f;
// If no layers are visible, skip the logic. Acquiring and holding buffers here would
// consume the producer's queue without ever presenting.
bool any_visible = false;
for (auto& layer : display.stack.layers) {
if (layer->visible) {
any_visible = true;
break;
}
}
if (!any_visible) {
return 1;
}
// Wait for the previous composite, bounding the number of in-flight composites.
nvdisp.WaitForComposite();
this->ReleaseFramebuffersLocked(display);
// Determine the number of vsync periods to wait before composing again.
std::optional<s32> swap_interval{};
@@ -152,6 +165,13 @@ u32 HardwareComposer::ComposeLocked(f32* out_speed_scale, Display& display,
// Advance by 1 frame (60 FPS compositing)
m_frame_number += 1;
// Release framebuffers once the frame has been handed to the compositor. This happens
// within the same vsync period as the acquire (release_frame_number is checked against
// the advanced counter), so the producer gets its buffers back immediately instead of
// losing a queue slot for a whole frame.
// TODO: support release fences; releasing before the composite has finished can tear.
this->ReleaseFramebuffersLocked(display);
return 1;
}
@@ -449,7 +449,12 @@ 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(),
{
+1 -1
View File
@@ -34,7 +34,7 @@ ConfigureCpu::ConfigureCpu(const Core::System& system_,
connect(backend_combobox, qOverload<int>(&QComboBox::currentIndexChanged), this,
&ConfigureCpu::UpdateGroup);
#ifdef HAS_NCE
#if defined(HAS_NCE) || defined(HAS_SCARLETNX)
ui->backend_group->setVisible(true);
#endif
}
+3 -3
View File
@@ -3661,12 +3661,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