diff --git a/lang/english/html/system_config_general.html b/lang/english/html/system_config_general.html
index 35c41483..5876a96d 100644
--- a/lang/english/html/system_config_general.html
+++ b/lang/english/html/system_config_general.html
@@ -58,6 +58,10 @@
+
diff --git a/src/nxemu-cpu/arm_dynarmic_64.cpp b/src/nxemu-cpu/arm_dynarmic_64.cpp
index b66ce162..13993af9 100644
--- a/src/nxemu-cpu/arm_dynarmic_64.cpp
+++ b/src/nxemu-cpu/arm_dynarmic_64.cpp
@@ -4,6 +4,7 @@
#include "cpu_settings.h"
#include
#include
+#include "yuzu_common/scope_exit.h"
#include
#include "arm_dynarmic.h"
#include "arm_dynarmic_64.h"
@@ -65,6 +66,10 @@ public:
}
return m_memory.Read64(vaddr);
}
+ void OnCompilation(bool compiling) override
+ {
+ m_system.Timing().CpuCompilation(m_parent.m_coreIndex, compiling);
+ }
std::optional MemoryReadCode(uint64_t vaddr) override
{
if (!m_memory.IsValidVirtualAddressRange(vaddr, sizeof(u32)))
@@ -315,6 +320,7 @@ ArmDynarmic64::ArmDynarmic64(ICoreSystem & system, bool uses_wall_clock, IKernel
m_process(process),
m_coreIndex(core_index)
{
+ m_cpu_hle_synchronization = m_uses_wall_clock && m_system.Timing().CpuHleSynchronizationEnabled();
m_jit = MakeJit(process);
ScopedJitExecution::RegisterHandler();
}
@@ -365,6 +371,7 @@ std::shared_ptr ArmDynarmic64::MakeJit(IKernelProcess & proc
// Timing
config.wall_clock_cntpct = m_uses_wall_clock;
config.enable_cycle_counting = !m_uses_wall_clock;
+ config.notify_compilation = m_cpu_hle_synchronization;
// Code cache size
#if defined(_M_ARM64) || defined(ARCHITECTURE_arm64)
@@ -492,15 +499,30 @@ CpuHaltReason ArmDynarmic64::RunThread(IKernelThread * thread)
ScopedJitExecution sj(thread->GetOwnerProcess());
m_jit->ClearExclusiveState();
- return TranslateHaltReason(m_jit->Run());
+ const auto result = [&] {
+ if (m_cpu_hle_synchronization) m_system.Timing().BeginCpuRun(m_coreIndex);
+ SCOPE_EXIT {
+ if (m_cpu_hle_synchronization) m_system.Timing().EndCpuRun(m_coreIndex);
+ };
+ return m_jit->Run();
+ }();
+ return TranslateHaltReason(result);
}
+
CpuHaltReason ArmDynarmic64::StepThread(IKernelThread * thread)
{
ScopedJitExecution sj(thread->GetOwnerProcess());
m_jit->ClearExclusiveState();
- return TranslateHaltReason(m_jit->Step());
+ const auto result = [&] {
+ if (m_cpu_hle_synchronization) m_system.Timing().BeginCpuRun(m_coreIndex);
+ SCOPE_EXIT {
+ if (m_cpu_hle_synchronization) m_system.Timing().EndCpuRun(m_coreIndex);
+ };
+ return m_jit->Step();
+ }();
+ return TranslateHaltReason(result);
}
void ArmDynarmic64::LockThread(IKernelThread * /*thread*/)
diff --git a/src/nxemu-cpu/arm_dynarmic_64.h b/src/nxemu-cpu/arm_dynarmic_64.h
index 460df960..87c49d71 100644
--- a/src/nxemu-cpu/arm_dynarmic_64.h
+++ b/src/nxemu-cpu/arm_dynarmic_64.h
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
+#include
#include "dynarmic/interface/A64/a64.h"
#include
#include
@@ -59,4 +60,5 @@ private:
uint32_t m_svc;
IKernelProcess & m_process;
uint32_t m_coreIndex;
+ bool m_cpu_hle_synchronization{};
};
diff --git a/src/nxemu-cpu/dynarmic/backend/arm64/a64_address_space.cpp b/src/nxemu-cpu/dynarmic/backend/arm64/a64_address_space.cpp
index 9d82fb47..8f3a25e7 100644
--- a/src/nxemu-cpu/dynarmic/backend/arm64/a64_address_space.cpp
+++ b/src/nxemu-cpu/dynarmic/backend/arm64/a64_address_space.cpp
@@ -544,6 +544,10 @@ void A64AddressSpace::EmitPrelude() {
ProtectCodeMemory();
}
+void A64AddressSpace::NotifyCompilation(bool compiling) {
+ if (conf.notify_compilation) conf.callbacks->OnCompilation(compiling);
+}
+
EmitConfig A64AddressSpace::GetEmitConfig() {
return EmitConfig{
.optimizations = conf.unsafe_optimizations ? conf.optimizations : conf.optimizations & all_safe_optimizations,
diff --git a/src/nxemu-cpu/dynarmic/backend/arm64/a64_address_space.h b/src/nxemu-cpu/dynarmic/backend/arm64/a64_address_space.h
index 86eae4dd..d0836d6f 100644
--- a/src/nxemu-cpu/dynarmic/backend/arm64/a64_address_space.h
+++ b/src/nxemu-cpu/dynarmic/backend/arm64/a64_address_space.h
@@ -24,6 +24,7 @@ public:
protected:
friend class A64Core;
+ void NotifyCompilation(bool compiling) override;
void EmitPrelude();
EmitConfig GetEmitConfig() override;
void RegisterNewBasicBlock(const IR::Block& block, const EmittedBlockInfo& block_info) override;
diff --git a/src/nxemu-cpu/dynarmic/backend/arm64/address_space.cpp b/src/nxemu-cpu/dynarmic/backend/arm64/address_space.cpp
index 10df477d..cf0daa29 100644
--- a/src/nxemu-cpu/dynarmic/backend/arm64/address_space.cpp
+++ b/src/nxemu-cpu/dynarmic/backend/arm64/address_space.cpp
@@ -12,6 +12,7 @@
#include "dynarmic/common/cast_util.h"
#include "dynarmic/common/fp/fpcr.h"
#include "dynarmic/interface/exclusive_monitor.h"
+#include
namespace Dynarmic::Backend::Arm64 {
@@ -60,6 +61,8 @@ CodePtr AddressSpace::GetOrEmit(IR::LocationDescriptor descriptor) {
return block_entry;
}
+ NotifyCompilation(true);
+ SCOPE_EXIT { NotifyCompilation(false); };
IR::Block ir_block = GenerateIR(descriptor);
const EmittedBlockInfo block_info = Emit(std::move(ir_block));
return block_info.entry_point;
diff --git a/src/nxemu-cpu/dynarmic/backend/arm64/address_space.h b/src/nxemu-cpu/dynarmic/backend/arm64/address_space.h
index a5fe7513..732e8640 100644
--- a/src/nxemu-cpu/dynarmic/backend/arm64/address_space.h
+++ b/src/nxemu-cpu/dynarmic/backend/arm64/address_space.h
@@ -44,6 +44,8 @@ public:
void ClearCache();
protected:
+ // Called only after a cache miss, never on a cached block's execution path.
+ virtual void NotifyCompilation(bool) {}
virtual EmitConfig GetEmitConfig() = 0;
virtual void RegisterNewBasicBlock(const IR::Block& block, const EmittedBlockInfo& block_info) = 0;
diff --git a/src/nxemu-cpu/dynarmic/backend/x64/a64_interface.cpp b/src/nxemu-cpu/dynarmic/backend/x64/a64_interface.cpp
index fb36b111..a6787a75 100644
--- a/src/nxemu-cpu/dynarmic/backend/x64/a64_interface.cpp
+++ b/src/nxemu-cpu/dynarmic/backend/x64/a64_interface.cpp
@@ -265,6 +265,8 @@ private:
CodePtr GetBlock(IR::LocationDescriptor current_location) {
if (auto block = emitter.GetBasicBlock(current_location))
return block->entrypoint;
+ if (conf.notify_compilation) conf.callbacks->OnCompilation(true);
+ SCOPE_EXIT { if (conf.notify_compilation) conf.callbacks->OnCompilation(false); };
constexpr size_t MINIMUM_REMAINING_CODESIZE = 1 * 1024 * 1024;
if (block_of_code.SpaceRemaining() < MINIMUM_REMAINING_CODESIZE) {
@@ -298,7 +300,8 @@ private:
if (!conf.HasOptimization(OptimizationFlag::DisableVerification)) {
Optimization::VerificationPass(ir_block);
}
- return emitter.Emit(ir_block).entrypoint;
+ const auto entrypoint = emitter.Emit(ir_block).entrypoint;
+ return entrypoint;
}
void PerformRequestedCacheInvalidation(HaltReason hr) {
diff --git a/src/nxemu-cpu/dynarmic/interface/A64/config.h b/src/nxemu-cpu/dynarmic/interface/A64/config.h
index b10c65bc..c977529e 100644
--- a/src/nxemu-cpu/dynarmic/interface/A64/config.h
+++ b/src/nxemu-cpu/dynarmic/interface/A64/config.h
@@ -87,6 +87,8 @@ struct UserCallbacks {
// All reads through this callback are 4-byte aligned.
// Memory must be interpreted as little endian.
virtual std::optional MemoryReadCode(VAddr vaddr) { return MemoryRead32(vaddr); }
+ // Cold compilation must not advance the executing CPU timeline.
+ virtual void OnCompilation(bool) {}
// Reads through these callbacks may not be aligned.
virtual std::uint8_t MemoryRead8(VAddr vaddr) = 0;
@@ -284,6 +286,7 @@ struct UserConfig {
/// This option allows you to disable cycle counting. If this is set to false,
/// AddTicks and GetTicksRemaining are never called, and no cycle counting is done.
bool enable_cycle_counting = true;
+ bool notify_compilation = false;
// Minimum size is about 8MiB. Maximum size is about 128MiB (arm64 host) or 2GiB (x64 host).
// Maximum size is limited by the maximum length of a x86_64 / arm64 jump.
diff --git a/src/nxemu-module-spec/base.h b/src/nxemu-module-spec/base.h
index 38f192ea..0b33dbc1 100644
--- a/src/nxemu-module-spec/base.h
+++ b/src/nxemu-module-spec/base.h
@@ -27,8 +27,8 @@ enum
{
MODULE_LOADER_SPECS_VERSION = 0x0130,
MODULE_VIDEO_SPECS_VERSION = 0x0121,
- MODULE_CPU_SPECS_VERSION = 0x0118,
- MODULE_OPERATING_SYSTEM_SPECS_VERSION = 0x0120,
+ MODULE_CPU_SPECS_VERSION = 0x011d,
+ MODULE_OPERATING_SYSTEM_SPECS_VERSION = 0x0124,
};
enum MODULE_TYPE : uint16_t
diff --git a/src/nxemu-module-spec/cpu.h b/src/nxemu-module-spec/cpu.h
index e45f9106..14da082d 100644
--- a/src/nxemu-module-spec/cpu.h
+++ b/src/nxemu-module-spec/cpu.h
@@ -65,6 +65,10 @@ nxinterface ICoreTiming
virtual void AddTicks(uint64_t ticks) = 0;
virtual int64_t GetDowncount() const = 0;
virtual uint64_t GetClockTicks() const = 0;
+ virtual void BeginCpuRun(uint32_t core) = 0;
+ virtual void EndCpuRun(uint32_t core) = 0;
+ virtual bool CpuHleSynchronizationEnabled() const = 0;
+ virtual void CpuCompilation(uint32_t core, bool compiling) = 0;
};
nxinterface ICoreSystem
diff --git a/src/nxemu-os/core/core.cpp b/src/nxemu-os/core/core.cpp
index bdb2d3ee..033996aa 100644
--- a/src/nxemu-os/core/core.cpp
+++ b/src/nxemu-os/core/core.cpp
@@ -3,6 +3,7 @@
#include
#include
+#include
#include
#include "yuzu_audio_core/audio_core.h"
@@ -34,6 +35,7 @@
#include "yuzu_input_common/main.h"
#include "network/network.h"
#include
+#include
#include
#ifdef _WIN32
@@ -66,8 +68,20 @@ struct System::Impl {
}
void Initialize(System& system)
+ {
+ ConfigureRuntime(system);
+
+ // These objects outlive individual games and runtime setting changes.
+ frontend_applets.SetDefaultAppletsIfMissing();
+ input_subsystem->Initialize();
+ }
+
+ void ConfigureRuntime(System& system)
{
is_multicore = osSettings.use_multi_core;
+ cpu_hle_requested = osSettings.cpu_hle_synchronization;
+ cpu_hle_initialized_supported = CpuHleSupported();
+ cpu_hle_initialized_native_clock = CpuHleUsesNativeClock();
#ifdef _WIN32
const std::chrono::nanoseconds timer_resolution = Common::Windows::SetCurrentTimerResolutionToMaximum();
@@ -76,46 +90,54 @@ struct System::Impl {
#endif
core_timing.SetMulticore(is_multicore);
- core_timing.Initialize([&system]() { system.RegisterHostThread(); });
-
- // Create default implementations of applets if one is not provided.
- frontend_applets.SetDefaultAppletsIfMissing();
+ core_timing.Initialize([&system]() { system.RegisterHostThread(); },
+ cpu_hle_initialized_supported, cpu_hle_initialized_native_clock);
is_async_gpu = g_settings->GetBool(NXVideoSetting::UseAsynchronousGPUEmulation);
kernel.SetMulticore(is_multicore);
cpu_manager.SetMulticore(is_multicore);
cpu_manager.SetAsyncGpu(is_async_gpu);
- input_subsystem->Initialize();
}
- void ReinitializeIfNecessary(System & system)
+ void ReconfigureIfNecessary(System & system)
{
- const bool must_reinitialize = false;
- // is_multicore != osSettings.use_multi_core ||
- // extended_memory_layout != (Settings::values.memory_layout_mode.GetValue() !=
- // Settings::MemoryLayout::Memory_4Gb);
+ const bool must_reconfigure = is_multicore != osSettings.use_multi_core ||
+ cpu_hle_requested != osSettings.cpu_hle_synchronization ||
+ (cpu_hle_requested && cpu_hle_initialized_supported != CpuHleSupported()) ||
+ (cpu_hle_requested && cpu_hle_initialized_native_clock != CpuHleUsesNativeClock()) ||
+ core_timing.CpuHleSynchronizationFailed();
- if (!must_reinitialize)
+ if (!must_reconfigure)
{
return;
}
- LOG_DEBUG(Kernel, "Re-initializing");
+ LOG_DEBUG(Kernel, "Reconfiguring runtime timing and CPU settings");
- // is_multicore = osSettings.use_multi_core;
- // extended_memory_layout =
- // Settings::values.memory_layout_mode.GetValue() != Settings::MemoryLayout::Memory_4Gb;
+ ConfigureRuntime(system);
+ }
- Initialize(system);
+ bool CpuHleSupported() const
+ {
+ return cpu_hle_guest64;
+ }
+
+ bool CpuHleUsesNativeClock() const
+ {
+#if (defined(_M_ARM64) || defined(ARCHITECTURE_arm64) || defined(__aarch64__)) && (defined(__linux__) || defined(__ANDROID__))
+ return cpu_hle_guest64 && g_settings->GetBool(NXCpuSetting::NceEnabled);
+#else
+ return false;
+#endif
}
void Run()
{
std::unique_lock lk(suspend_guard);
- kernel.SuspendEmulation(false);
core_timing.SyncPause(false);
+ kernel.SuspendEmulation(false);
is_paused.store(false, std::memory_order_relaxed);
}
@@ -123,8 +145,8 @@ struct System::Impl {
{
std::unique_lock lk(suspend_guard);
- core_timing.SyncPause(true);
kernel.SuspendEmulation(true);
+ core_timing.SyncPause(true);
is_paused.store(true, std::memory_order_relaxed);
}
@@ -160,12 +182,14 @@ struct System::Impl {
return nvdec_active;
}
- void InitializeKernel(System & system, uint64_t titleID)
+ void InitializeKernel(System & system, uint64_t titleID, bool is_64_bit)
{
LOG_DEBUG(Core, "initialized OK");
- // Setting changes may require a full system reinitialization (e.g., disabling multicore).
- ReinitializeIfNecessary(system);
+ cpu_hle_guest64 = is_64_bit;
+ // Reconfigure timing before creating guest threads. Input factories and
+ // frontend applets remain the instances registered during OS startup.
+ ReconfigureIfNecessary(system);
kernel.Initialize();
cpu_manager.Initialize();
@@ -288,6 +312,9 @@ struct System::Impl {
Core::SpeedLimiter speed_limiter;
bool is_multicore{};
+ bool cpu_hle_requested{};
+ bool cpu_hle_guest64{true}, cpu_hle_initialized_supported{true};
+ bool cpu_hle_initialized_native_clock{};
bool is_async_gpu{};
::ExecuteProgramCallback execute_program_callback{};
@@ -332,9 +359,9 @@ const CpuManager & System::GetCpuManager() const
return impl->cpu_manager;
}
-void System::InitializeKernel(uint64_t titleID)
+void System::InitializeKernel(uint64_t titleID, bool is_64_bit)
{
- impl->InitializeKernel(*this, titleID);
+ impl->InitializeKernel(*this, titleID, is_64_bit);
}
void System::Initialize() {
diff --git a/src/nxemu-os/core/core.h b/src/nxemu-os/core/core.h
index 2ef2d61c..e6d60778 100644
--- a/src/nxemu-os/core/core.h
+++ b/src/nxemu-os/core/core.h
@@ -153,7 +153,7 @@ public:
System(System &&) = delete;
System & operator=(System &&) = delete;
- void InitializeKernel(uint64_t titleID);
+ void InitializeKernel(uint64_t titleID, bool is_64_bit = true);
/**
* Initializes the system
diff --git a/src/nxemu-os/core/core_timing.cpp b/src/nxemu-os/core/core_timing.cpp
index 81cba29b..949c3d25 100644
--- a/src/nxemu-os/core/core_timing.cpp
+++ b/src/nxemu-os/core/core_timing.cpp
@@ -5,8 +5,12 @@
#include
#include
#include
+#include "core/hle_execution_time.h"
+#include "os_settings.h"
#ifdef _WIN32
+#include
+#undef CreateEvent
#include "yuzu_common/windows/timer_resolution.h"
#endif
@@ -16,11 +20,43 @@
#include "core/core_timing.h"
#include "core/hardware_properties.h"
+#include "yuzu_common/logging/log.h"
namespace Core::Timing {
constexpr s64 MAX_SLICE_LENGTH = 10000;
+#ifdef _WIN32
+// Short HLE completion deadlines must not keep HostTiming on a CPU while
+// waiting. Both an earlier event and shutdown interrupt this one-shot wait.
+class CoreTiming::WindowsTimerWait {
+public:
+ WindowsTimerWait() {
+ timer = CreateWaitableTimerExW(nullptr, nullptr, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION,
+ TIMER_MODIFY_STATE | SYNCHRONIZE);
+ wake = CreateEventW(nullptr, FALSE, FALSE, nullptr);
+ }
+ ~WindowsTimerWait() {
+ if (timer) CloseHandle(timer);
+ if (wake) CloseHandle(wake);
+ }
+ bool IsValid() const { return timer && wake; }
+ void Notify() { SetEvent(wake); }
+ bool WaitForNotification() { return WaitForSingleObject(wake, INFINITE) == WAIT_OBJECT_0; }
+ bool Wait(s64 ns) {
+ // Round up to the API's 100 ns units. The caller always rechecks the
+ // emulated clock; a host timeout alone never makes an event due.
+ LARGE_INTEGER due{.QuadPart = -(ns / 100 + (ns % 100 != 0))};
+ if (!SetWaitableTimerEx(timer, &due, 0, nullptr, nullptr, nullptr, 0)) return false;
+ const HANDLE handles[]{wake, timer};
+ return WaitForMultipleObjects(2, handles, FALSE, INFINITE) != WAIT_FAILED;
+ }
+private:
+ HANDLE timer{}, wake{};
+};
+#endif
+
+
std::shared_ptr CreateEvent(std::string name, TimedCallback&& callback) {
return std::make_shared(std::move(callback), std::move(name));
}
@@ -57,12 +93,36 @@ void CoreTiming::ThreadEntry(CoreTiming& instance) {
instance.ThreadLoop();
}
-void CoreTiming::Initialize(std::function&& on_thread_init_) {
+void CoreTiming::Initialize(std::function&& on_thread_init_, bool allow_cpu_hle,
+ bool native_cpu_clock) {
Reset();
+ cpu_hle_native_clock.store(native_cpu_clock, std::memory_order_relaxed);
on_thread_init = std::move(on_thread_init_);
event_fifo_id = 0;
shutting_down = false;
cpu_ticks = 0;
+ fallback_time_offset_ns.store(0, std::memory_order_relaxed);
+ cpu_hle_failed.store(false, std::memory_order_relaxed);
+ bool supported = false;
+ bool counter_supported = false;
+ if (osSettings.cpu_hle_synchronization && allow_cpu_hle && is_multicore)
+ counter_supported = ReadNativeThreadTimeNs().has_value();
+ supported = osSettings.cpu_hle_synchronization && allow_cpu_hle && is_multicore && counter_supported;
+#ifdef _WIN32
+ if (supported) {
+ windows_timer_wait = std::make_unique();
+ if (!windows_timer_wait->IsValid()) {
+ windows_timer_wait.reset();
+ supported = false;
+ LOG_WARNING(Core, "High-resolution timer unavailable; using normal timing");
+ }
+ }
+#endif
+ cpu_hle_enabled.store(osSettings.cpu_hle_synchronization && supported, std::memory_order_release);
+ shared_clock.Reset(clock.get());
+ if (osSettings.cpu_hle_synchronization && !supported)
+ LOG_WARNING(Core, "CPU/HLE synchronization unavailable (multicore={}, guest64_backend={}, thread_counter={}); using normal timing", is_multicore, allow_cpu_hle, counter_supported);
+ LOG_INFO(Core, "CPU/HLE synchronization: {} (multicore={}, guest64_backend={}, native_cpu_clock={})", CpuHleSynchronizationEnabled(), is_multicore, allow_cpu_hle, native_cpu_clock);
if (is_multicore) {
timer_thread = std::make_unique(ThreadEntry, std::ref(*this));
}
@@ -71,12 +131,16 @@ void CoreTiming::Initialize(std::function&& on_thread_init_) {
void CoreTiming::ClearPendingEvents() {
std::scoped_lock lock{advance_lock, basic_lock};
event_queue.clear();
- event.Set();
+ NotifyEvent();
}
void CoreTiming::Pause(bool is_paused) {
+ if (UsesSharedCpuClock()) shared_clock.Pause(is_paused);
paused = is_paused;
pause_event.Set();
+#ifdef _WIN32
+ if (windows_timer_wait) windows_timer_wait->Notify();
+#endif
if (!is_paused) {
pause_end_time = GetGlobalTimeNs().count();
@@ -93,7 +157,7 @@ void CoreTiming::SyncPause(bool is_paused) {
if (!is_paused) {
pause_event.Set();
}
- event.Set();
+ NotifyEvent();
while (paused_set != is_paused)
;
}
@@ -122,7 +186,7 @@ void CoreTiming::ScheduleEvent(std::chrono::nanoseconds ns_into_future,
(*h).handle = h;
}
- event.Set();
+ NotifyEvent();
}
void CoreTiming::ScheduleLoopingEvent(std::chrono::nanoseconds start_time,
@@ -138,7 +202,7 @@ void CoreTiming::ScheduleLoopingEvent(std::chrono::nanoseconds start_time,
(*h).handle = h;
}
- event.Set();
+ NotifyEvent();
}
void CoreTiming::UnscheduleEvent(const std::shared_ptr& event_type,
@@ -181,14 +245,20 @@ void CoreTiming::ResetTicks() {
}
u64 CoreTiming::GetClockTicks() const {
+ if (UsesSharedCpuClock()) return Common::WallClock::NSToCNTPCT(shared_clock.Now());
if (is_multicore) [[likely]] {
+ if (fallback_time_offset_ns.load(std::memory_order_acquire))
+ return Common::WallClock::NSToCNTPCT(GetGlobalTimeNs().count());
return clock->GetCNTPCT();
}
return Common::WallClock::CPUTickToCNTPCT(cpu_ticks);
}
u64 CoreTiming::GetGPUTicks() const {
+ if (UsesSharedCpuClock()) return Common::WallClock::NSToGPUTick(shared_clock.Now());
if (is_multicore) [[likely]] {
+ if (fallback_time_offset_ns.load(std::memory_order_acquire))
+ return Common::WallClock::NSToGPUTick(GetGlobalTimeNs().count());
return clock->GetGPUTick();
}
return Common::WallClock::CPUTickToGPUTick(cpu_ticks);
@@ -266,7 +336,30 @@ void CoreTiming::ThreadLoop() {
#ifdef _WIN32
while (!paused && !event.IsSet() && wait_time > 0) {
wait_time = *next_time - GetGlobalTimeNs().count();
- if (wait_time >= timer_resolution_ns) {
+ if (wait_time <= 0) break;
+ if (CpuHleSynchronizationEnabled() && windows_timer_wait) {
+ // Serialize the snapshot with compilation-end notification
+ // so resuming a frozen CPU cannot miss this wakeup.
+ bool blocked;
+ {
+ std::scoped_lock lock{compilation_wait_guard};
+ blocked = UsesSharedCpuClock() && shared_clock.CompilationBlocks(*next_time);
+ waiting_for_compilation = blocked;
+ }
+ bool ok;
+ if (blocked) {
+ ok = windows_timer_wait->WaitForNotification();
+ } else {
+ ok = windows_timer_wait->Wait(wait_time);
+ }
+ {
+ std::scoped_lock lock{compilation_wait_guard};
+ waiting_for_compilation = false;
+ }
+ if (!ok) {
+ DisableCpuHleSynchronization("high-resolution timer wait failed");
+ }
+ } else if (wait_time >= timer_resolution_ns) {
Common::Windows::SleepForOneTick();
} else {
#if defined(_M_X64) || defined(ARCHITECTURE_x86_64)
@@ -281,7 +374,21 @@ void CoreTiming::ThreadLoop() {
event.Reset();
}
#else
- event.WaitFor(std::chrono::nanoseconds(wait_time));
+ // Use the same frozen-clock predicate and wake handshake as
+ // Windows. A deadline cannot expire while a participant is
+ // compiling; wait for a transition instead of polling it.
+ bool blocked = false;
+ if (UsesSharedCpuClock()) {
+ std::scoped_lock lock{compilation_wait_guard};
+ blocked = shared_clock.CompilationBlocks(*next_time);
+ waiting_for_compilation = blocked;
+ }
+ if (blocked) event.Wait();
+ else event.WaitFor(std::chrono::nanoseconds(wait_time));
+ if (blocked) {
+ std::scoped_lock lock{compilation_wait_guard};
+ waiting_for_compilation = false;
+ }
#endif
}
} else {
@@ -302,23 +409,31 @@ void CoreTiming::Reset() {
paused = true;
shutting_down = true;
pause_event.Set();
- event.Set();
+ NotifyEvent();
if (timer_thread) {
timer_thread->join();
}
timer_thread.reset();
+#ifdef _WIN32
+ windows_timer_wait.reset();
+#endif
has_started = false;
}
std::chrono::nanoseconds CoreTiming::GetGlobalTimeNs() const {
+ if (UsesSharedCpuClock()) return std::chrono::nanoseconds{shared_clock.Now()};
if (is_multicore) [[likely]] {
- return clock->GetTimeNS();
+ const auto offset = fallback_time_offset_ns.load(std::memory_order_acquire);
+ return clock->GetTimeNS() + std::chrono::nanoseconds{offset};
}
return std::chrono::nanoseconds{Common::WallClock::CPUTickToNS(cpu_ticks)};
}
std::chrono::microseconds CoreTiming::GetGlobalTimeUs() const {
+ if (UsesSharedCpuClock()) return std::chrono::duration_cast(GetGlobalTimeNs());
if (is_multicore) [[likely]] {
+ if (fallback_time_offset_ns.load(std::memory_order_acquire))
+ return std::chrono::duration_cast(GetGlobalTimeNs());
return clock->GetTimeUS();
}
return std::chrono::microseconds{Common::WallClock::CPUTickToUS(cpu_ticks)};
@@ -330,4 +445,56 @@ void CoreTiming::SetTimerResolutionNs(std::chrono::nanoseconds ns) {
}
#endif
+bool CoreTiming::CpuHleSynchronizationEnabled() const {
+ return cpu_hle_enabled.load(std::memory_order_acquire);
+}
+
+bool CoreTiming::UsesSharedCpuClock() const {
+ return !cpu_hle_native_clock.load(std::memory_order_relaxed) && CpuHleSynchronizationEnabled();
+}
+
+void CoreTiming::DisableCpuHleSynchronization(const char* reason) {
+ std::scoped_lock lock{cpu_hle_transition_guard};
+ if (!CpuHleSynchronizationEnabled()) return;
+ // NCE already uses the native time domain; fallback must not offset its
+ // ticks away from the counter read directly by guest instructions.
+ if (!cpu_hle_native_clock.load(std::memory_order_relaxed)) {
+ shared_clock.Pause(true);
+ const auto now = static_cast(shared_clock.Now());
+ fallback_time_offset_ns.store(now - clock->GetTimeNS().count(), std::memory_order_release);
+ }
+ cpu_hle_enabled.store(false, std::memory_order_release);
+ cpu_hle_failed.store(true, std::memory_order_release);
+ NotifyEvent();
+ LOG_ERROR(Core, "CPU/HLE synchronization disabled: {}; normal timing resumed for this session", reason);
+}
+
+void CoreTiming::NotifyEvent() {
+ event.Set();
+#ifdef _WIN32
+ if (windows_timer_wait) windows_timer_wait->Notify();
+#endif
+}
+
+void CoreTiming::CpuCompilation(uint32_t core, bool compiling) {
+ if (!UsesSharedCpuClock()) return;
+ shared_clock.Compile(core, compiling);
+ if (!compiling) {
+ std::scoped_lock lock{compilation_wait_guard};
+ if (waiting_for_compilation) NotifyEvent();
+ }
+}
+
+void CoreTiming::SetCpuRunnableMask(u32 mask) {
+ if (UsesSharedCpuClock()) shared_clock.SetRunnableMask(mask);
+}
+
+void CoreTiming::BeginCpuRun(uint32_t core) {
+ if (UsesSharedCpuClock()) shared_clock.Begin(core);
+}
+
+void CoreTiming::EndCpuRun(uint32_t core) {
+ if (UsesSharedCpuClock()) shared_clock.End(core);
+}
+
} // namespace Core::Timing
diff --git a/src/nxemu-os/core/core_timing.h b/src/nxemu-os/core/core_timing.h
index a9203fd0..a70a28ac 100644
--- a/src/nxemu-os/core/core_timing.h
+++ b/src/nxemu-os/core/core_timing.h
@@ -19,6 +19,7 @@
#include "yuzu_common/wall_clock.h"
#include
+#include "core/shared_cpu_clock.h"
namespace Core::Timing {
@@ -72,7 +73,10 @@ public:
/// CoreTiming begins at the boundary of timing slice -1. An initial call to Advance() is
/// required to end slice - 1 and start slice 0 before the first cycle of code is executed.
- void Initialize(std::function&& on_thread_init_);
+ // NCE reads the native ARM counter directly; its HLE deadlines must use
+ // that same time domain rather than the JIT-adjusted shared CPU clock.
+ void Initialize(std::function&& on_thread_init_, bool allow_cpu_hle = true,
+ bool native_cpu_clock = false);
/// Clear all pending events. This should ONLY be done on exit.
void ClearPendingEvents();
@@ -126,6 +130,13 @@ public:
/// Returns the current CNTPCT tick value.
uint64_t GetClockTicks() const override;
+ void BeginCpuRun(uint32_t core) override;
+ void EndCpuRun(uint32_t core) override;
+ bool CpuHleSynchronizationEnabled() const override;
+ void CpuCompilation(uint32_t core, bool compiling) override;
+ void SetCpuRunnableMask(u32 mask);
+ void DisableCpuHleSynchronization(const char* reason = "execution-time measurement failed");
+ bool CpuHleSynchronizationFailed() const { return cpu_hle_failed.load(std::memory_order_acquire); }
/// Returns the current GPU tick value.
u64 GetGPUTicks() const;
@@ -148,14 +159,26 @@ private:
static void ThreadEntry(CoreTiming& instance);
void ThreadLoop();
+ void NotifyEvent();
void Reset();
+ bool UsesSharedCpuClock() const;
std::unique_ptr clock;
+ SharedCpuClock shared_clock;
+ std::atomic cpu_hle_native_clock{};
+ std::atomic cpu_hle_enabled{};
+ std::atomic cpu_hle_failed{};
+ std::atomic fallback_time_offset_ns{};
+ std::mutex cpu_hle_transition_guard;
+ std::mutex compilation_wait_guard;
+ bool waiting_for_compilation{};
s64 global_timer = 0;
#ifdef _WIN32
+ class WindowsTimerWait;
+ std::unique_ptr windows_timer_wait;
s64 timer_resolution_ns{1'000'000};
#endif
diff --git a/src/nxemu-os/core/hle/kernel/global_scheduler_context.cpp b/src/nxemu-os/core/hle/kernel/global_scheduler_context.cpp
index faa8bc0b..bcd86e8e 100644
--- a/src/nxemu-os/core/hle/kernel/global_scheduler_context.cpp
+++ b/src/nxemu-os/core/hle/kernel/global_scheduler_context.cpp
@@ -2,6 +2,11 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include
+#include
+#include
+#include "core/hle_execution_time.h"
+#include "core/core_timing.h"
+
#include "yuzu_common/yuzu_assert.h"
#include "core/core.h"
@@ -10,13 +15,48 @@
#include "core/hle/kernel/kernel.h"
#include "core/hle/kernel/physical_core.h"
+
namespace Kernel {
GlobalSchedulerContext::GlobalSchedulerContext(KernelCore& kernel)
- : m_kernel{kernel}, m_scheduler_lock{kernel} {}
+ : m_kernel{kernel}, m_scheduler_lock{kernel} {
+
+}
GlobalSchedulerContext::~GlobalSchedulerContext() = default;
+GlobalSchedulerContext::HleWork GlobalSchedulerContext::BeginHle() {
+ auto& timing = m_kernel.System().CoreTiming();
+ if (!timing.CpuHleSynchronizationEnabled()) return {};
+ KScopedSchedulerLock lock{m_kernel};
+ auto& thread = GetCurrentThread(m_kernel);
+ // A nested service call is already charged to its enclosing handler.
+ if (thread.hle_execution.IsCollecting()) return {};
+ const auto start = std::max(thread.hle_available_time,
+ static_cast(timing.GetGlobalTimeNs().count()));
+ ASSERT(!thread.hle_execution.IsCollecting());
+ thread.hle_execution.Begin(Core::Timing::ReadNativeThreadTimeNs());
+ return {start, &thread};
+}
+
+u64 GlobalSchedulerContext::EndHle(const HleWork& work) {
+ if (!work.executing_thread) return 0;
+ const auto elapsed = work.executing_thread->hle_execution.Finish(Core::Timing::ReadNativeThreadTimeNs());
+ KScopedSchedulerLock lock{m_kernel};
+ auto& timing = m_kernel.System().CoreTiming();
+ if (!timing.CpuHleSynchronizationEnabled()) return 0;
+ if (!elapsed) {
+ // A missing CPU-time sample cannot become free work or blocked wall time.
+ timing.DisableCpuHleSynchronization();
+ return 0;
+ }
+ constexpr auto limit = static_cast(std::numeric_limits::max());
+ const auto start = std::min(work.virtual_start, limit);
+ auto& available = work.executing_thread->hle_available_time;
+ available = std::max(available, start + std::min(*elapsed, limit - start));
+ return available;
+}
+
void GlobalSchedulerContext::AddThread(KThread* thread) {
std::scoped_lock lock{m_global_list_guard};
m_thread_list.push_back(thread);
diff --git a/src/nxemu-os/core/hle/kernel/global_scheduler_context.h b/src/nxemu-os/core/hle/kernel/global_scheduler_context.h
index ad2a85b1..8ac96965 100644
--- a/src/nxemu-os/core/hle/kernel/global_scheduler_context.h
+++ b/src/nxemu-os/core/hle/kernel/global_scheduler_context.h
@@ -62,6 +62,13 @@ public:
void UnregisterDummyThreadForWakeup(KThread* thread);
void RegisterDummyThreadForWakeup(KThread* thread);
void WakeupWaitingDummyThreads();
+ struct HleWork {
+ u64 virtual_start{};
+ KThread* executing_thread{};
+ };
+ HleWork BeginHle();
+ u64 EndHle(const HleWork& work);
+
LockType& SchedulerLock() {
return m_scheduler_lock;
@@ -83,6 +90,7 @@ private:
/// Lists all thread ids that aren't deleted/etc.
std::vector m_thread_list;
std::mutex m_global_list_guard;
+
};
} // namespace Kernel
diff --git a/src/nxemu-os/core/hle/kernel/k_scheduler.cpp b/src/nxemu-os/core/hle/kernel/k_scheduler.cpp
index 014bf91f..df17ca92 100644
--- a/src/nxemu-os/core/hle/kernel/k_scheduler.cpp
+++ b/src/nxemu-os/core/hle/kernel/k_scheduler.cpp
@@ -135,6 +135,7 @@ void KScheduler::PreemptSingleCore()
auto * thread = GetCurrentThreadPointer(m_kernel);
auto & previous_scheduler = m_kernel.Scheduler(thread->GetCurrentCore());
+ thread->SuspendHleExecution();
previous_scheduler.Unload(thread);
Common::Fiber::YieldTo(thread->GetHostContext(), *m_switch_fiber);
@@ -372,6 +373,16 @@ u64 KScheduler::UpdateHighestPriorityThreadsImpl(KernelCore & kernel)
idle_cores &= ~(1ULL << core_id);
}
+ if (kernel.System().CoreTiming().CpuHleSynchronizationEnabled()) {
+ u32 runnable_mask{};
+ for (u32 core = 0; core < Hardware::NUM_CPU_CORES; ++core) {
+ const auto* thread = top_threads[core];
+ if (thread && thread->GetOwnerKProcess() && thread->GetOwnerKProcess()->Is64Bit())
+ runnable_mask |= 1U << core;
+ }
+ kernel.System().CoreTiming().SetCpuRunnableMask(runnable_mask);
+ }
+
// HACK: any waiting dummy threads can wake up now.
kernel.GlobalSchedulerContext().WakeupWaitingDummyThreads();
@@ -482,6 +493,7 @@ void KScheduler::ScheduleImpl()
m_switch_cur_thread = cur_thread;
m_switch_highest_priority_thread = highest_priority_thread;
m_switch_from_schedule = true;
+ cur_thread->SuspendHleExecution();
Common::Fiber::YieldTo(cur_thread->m_host_context, *m_switch_fiber);
// Returning from ScheduleImpl occurs after this thread has been scheduled again.
@@ -569,6 +581,7 @@ void KScheduler::ScheduleImplFiber()
Reload(highest_priority_thread);
// Reload the host thread.
+ highest_priority_thread->ResumeHleExecution();
Common::Fiber::YieldTo(m_switch_fiber, *highest_priority_thread->m_host_context);
}
diff --git a/src/nxemu-os/core/hle/kernel/k_thread.h b/src/nxemu-os/core/hle/kernel/k_thread.h
index fba073ec..83947439 100644
--- a/src/nxemu-os/core/hle/kernel/k_thread.h
+++ b/src/nxemu-os/core/hle/kernel/k_thread.h
@@ -29,6 +29,7 @@
#include "core/hle/kernel/svc_common.h"
#include "core/hle/kernel/svc_types.h"
#include "core/hle/result.h"
+#include "core/hle_execution_time.h"
namespace Common
{
@@ -144,6 +145,20 @@ private:
public:
static constexpr s32 DefaultThreadPriority = 44;
+ // Owned by this fiber, including suspension and migration between host threads.
+ Core::Timing::HleExecutionTime hle_execution;
+ // Serialized by the scheduler lock; dies with the actor instead of leaving
+ // an entry indexed by a monotonically increasing thread id.
+ u64 hle_available_time{};
+ void SuspendHleExecution() {
+ if (hle_execution.IsCollecting())
+ hle_execution.Suspend(Core::Timing::ReadNativeThreadTimeNs());
+ }
+ void ResumeHleExecution() {
+ if (hle_execution.IsCollecting())
+ hle_execution.Resume(Core::Timing::ReadNativeThreadTimeNs());
+ }
+
static constexpr s32 IdleThreadPriority = Svc::LowestThreadPriority + 1;
static constexpr s32 DummyThreadPriority = Svc::LowestThreadPriority + 2;
diff --git a/src/nxemu-os/core/hle/service/hle_ipc.cpp b/src/nxemu-os/core/hle/service/hle_ipc.cpp
index 0b98c2cf..0bfca69d 100644
--- a/src/nxemu-os/core/hle/service/hle_ipc.cpp
+++ b/src/nxemu-os/core/hle/service/hle_ipc.cpp
@@ -27,6 +27,7 @@
#include "core/hle/service/ipc_helpers.h"
#include "core/memory.h"
#include "core/core.h"
+#include "core/core_timing.h"
namespace Service {
@@ -139,6 +140,7 @@ HLERequestContext::HLERequestContext(Kernel::KernelCore& kernel_, Core::Memory::
thread->Open();
server_session->Open();
session_request->Open();
+ defer_response_buffers = kernel.System().CoreTiming().CpuHleSynchronizationEnabled();
cmd_buf[0] = 0;
}
diff --git a/src/nxemu-os/core/hle/service/hle_ipc.h b/src/nxemu-os/core/hle/service/hle_ipc.h
index 6b72e410..76705db0 100644
--- a/src/nxemu-os/core/hle/service/hle_ipc.h
+++ b/src/nxemu-os/core/hle/service/hle_ipc.h
@@ -406,6 +406,8 @@ public:
void SetIsDeferred(bool is_deferred_ = true) {
is_deferred = is_deferred_;
}
+ void SetCompletionTime(u64 value) { completion_time = value; }
+ u64 GetCompletionTime() const { return completion_time; }
private:
friend class IPC::ResponseBuilder;
@@ -420,6 +422,7 @@ private:
Kernel::KThread* thread{};
Kernel::KSessionRequest* session_request{};
u64 command_buffer_address{};
+ u64 completion_time{};
bool defer_response_buffers{};
mutable IpcResponseWrites response_writes;
mutable bool response_write_failed{};
diff --git a/src/nxemu-os/core/hle/service/nvdrv/nvdrv_interface.cpp b/src/nxemu-os/core/hle/service/nvdrv/nvdrv_interface.cpp
index a1fb07b9..a4d99f66 100644
--- a/src/nxemu-os/core/hle/service/nvdrv/nvdrv_interface.cpp
+++ b/src/nxemu-os/core/hle/service/nvdrv/nvdrv_interface.cpp
@@ -65,6 +65,18 @@ void NVDRV::ServiceError(HLERequestContext& ctx, NvResult result) {
rb.PushEnum(result);
}
+void NVDRV::PrepareOutputBuffer(HLERequestContext& ctx, Common::ScratchBuffer& buffer,
+ size_t index) {
+ const auto size = ctx.GetWriteBufferSize(index);
+ if (ctx.ResponseBuffersDeferred() && size && !output_pool)
+ output_pool = std::make_shared();
+ if (ctx.ResponseBuffersDeferred() && size > buffer.capacity()) {
+ buffer = output_pool->Acquire(size);
+ } else {
+ buffer.resize_destructive(size);
+ }
+}
+
void NVDRV::Ioctl1(HLERequestContext& ctx) {
IPC::RequestParser rp{ctx};
const auto fd = rp.Pop();
@@ -88,7 +100,7 @@ void NVDRV::Ioctl1(HLERequestContext& ctx) {
}
// Deferred requests keep their first input snapshot and their original deadline.
- output_buffer.resize_destructive(ctx.GetWriteBufferSize(0));
+ PrepareOutputBuffer(ctx, output_buffer, 0);
const auto input_buffer = pending == deferred_ioctls.end()
? ctx.ReadBuffer(0)
: std::span{pending->second.input};
@@ -110,7 +122,7 @@ void NVDRV::Ioctl1(HLERequestContext& ctx) {
}
if (command.is_out != 0) {
- ctx.WriteBuffer(std::move(output_buffer));
+ ctx.WriteBufferPooled(std::move(output_buffer), 0, output_pool);
}
IPC::ResponseBuilder rb{ctx, 3};
@@ -132,12 +144,12 @@ void NVDRV::Ioctl2(HLERequestContext& ctx) {
const auto input_buffer = ctx.ReadBuffer(0);
const auto input_inlined_buffer = ctx.ReadBuffer(1);
- output_buffer.resize_destructive(ctx.GetWriteBufferSize(0));
+ PrepareOutputBuffer(ctx, output_buffer, 0);
const auto nv_result =
nvdrv->Ioctl2(fd, command, input_buffer, input_inlined_buffer, output_buffer);
if (command.is_out != 0) {
- ctx.WriteBuffer(std::move(output_buffer));
+ ctx.WriteBufferPooled(std::move(output_buffer), 0, output_pool);
}
IPC::ResponseBuilder rb{ctx, 3};
@@ -158,14 +170,14 @@ void NVDRV::Ioctl3(HLERequestContext& ctx) {
}
const auto input_buffer = ctx.ReadBuffer(0);
- output_buffer.resize_destructive(ctx.GetWriteBufferSize(0));
- inline_output_buffer.resize_destructive(ctx.GetWriteBufferSize(1));
+ PrepareOutputBuffer(ctx, output_buffer, 0);
+ PrepareOutputBuffer(ctx, inline_output_buffer, 1);
const auto nv_result =
nvdrv->Ioctl3(fd, command, input_buffer, output_buffer, inline_output_buffer);
if (command.is_out != 0) {
- ctx.WriteBuffer(std::move(output_buffer), 0);
- ctx.WriteBuffer(std::move(inline_output_buffer), 1);
+ ctx.WriteBufferPooled(std::move(output_buffer), 0, output_pool);
+ ctx.WriteBufferPooled(std::move(inline_output_buffer), 1, output_pool);
}
IPC::ResponseBuilder rb{ctx, 3};
diff --git a/src/nxemu-os/core/hle/service/nvdrv/nvdrv_interface.h b/src/nxemu-os/core/hle/service/nvdrv/nvdrv_interface.h
index 9b0f2c8e..8f5dc582 100644
--- a/src/nxemu-os/core/hle/service/nvdrv/nvdrv_interface.h
+++ b/src/nxemu-os/core/hle/service/nvdrv/nvdrv_interface.h
@@ -40,6 +40,7 @@ private:
void DumpGraphicsMemoryInfo(HLERequestContext& ctx);
void ServiceError(HLERequestContext& ctx, NvResult result);
+ void PrepareOutputBuffer(HLERequestContext& ctx, Common::ScratchBuffer& buffer, size_t index);
std::shared_ptr nvdrv;
@@ -54,6 +55,7 @@ private:
NvCore::SessionId session_id{};
Common::ScratchBuffer output_buffer;
Common::ScratchBuffer inline_output_buffer;
+ std::shared_ptr output_pool;
};
} // namespace Service::Nvidia
diff --git a/src/nxemu-os/core/hle/service/server_manager.cpp b/src/nxemu-os/core/hle/service/server_manager.cpp
index dba2865c..b3ede57c 100644
--- a/src/nxemu-os/core/hle/service/server_manager.cpp
+++ b/src/nxemu-os/core/hle/service/server_manager.cpp
@@ -4,9 +4,11 @@
#include "yuzu_common/scope_exit.h"
#include "core/core.h"
+#include "core/core_timing.h"
#include "core/hle/kernel/k_client_port.h"
#include "core/hle/kernel/k_client_session.h"
#include "core/hle/kernel/k_event.h"
+#include "core/hle/kernel/global_scheduler_context.h"
#include "core/hle/kernel/k_object_name.h"
#include "core/hle/kernel/k_port.h"
#include "core/hle/kernel/k_server_port.h"
@@ -78,6 +80,10 @@ ServerManager::ServerManager(Core::System& system) : m_system{system}, m_selecti
// Register event.
Kernel::KEvent::Register(system.Kernel(), m_wakeup_event);
+ m_shutdown_event = Kernel::KEvent::Create(system.Kernel());
+ m_shutdown_event->Initialize(nullptr);
+ Kernel::KEvent::Register(system.Kernel(), m_shutdown_event);
+
// Link to holder.
m_wakeup_holder.emplace(std::addressof(m_wakeup_event->GetReadableEvent()));
m_wakeup_holder->LinkToMultiWait(std::addressof(m_deferred_list));
@@ -86,6 +92,7 @@ ServerManager::ServerManager(Core::System& system) : m_system{system}, m_selecti
ServerManager::~ServerManager() {
// Signal stop.
m_stop_source.request_stop();
+ m_shutdown_event->Signal();
m_wakeup_event->Signal();
// Wait for processing to stop.
@@ -112,6 +119,9 @@ ServerManager::~ServerManager() {
m_wakeup_event->GetReadableEvent().Close();
m_wakeup_event->Close();
+ m_shutdown_event->GetReadableEvent().Close();
+ m_shutdown_event->Close();
+
if (m_deferral_event) {
m_deferral_event->GetReadableEvent().Close();
// Write event is owned by ServiceManager
@@ -403,6 +413,22 @@ Result ServerManager::CompleteSyncRequest(Session* session) {
R_SUCCEED();
}
+ if (m_system.CoreTiming().CpuHleSynchronizationEnabled()) {
+ // Retain the request/context until completion. The reply transaction
+ // handles synchronous clients, asynchronous events and session closure.
+ const auto deadline = session->GetContext()->GetCompletionTime();
+ while (!m_stop_source.stop_requested() &&
+ m_system.CoreTiming().CpuHleSynchronizationEnabled() && deadline >
+ static_cast(m_system.CoreTiming().GetGlobalTimeNs().count())) {
+ s32 index{};
+ Kernel::KSynchronizationObject* objects[]{std::addressof(m_shutdown_event->GetReadableEvent())};
+ const auto wait = Kernel::KSynchronizationObject::Wait(m_system.Kernel(),
+ std::addressof(index), objects, 1, static_cast(deadline));
+ if (wait == Kernel::ResultTerminationRequested) R_SUCCEED();
+ ASSERT(wait == ResultSuccess || wait == Kernel::ResultTimedOut || wait == Kernel::ResultCancelled);
+ }
+ R_SUCCEED_IF(m_stop_source.stop_requested());
+ }
// Send the reply.
res = server_session->SendReplyHLE(*session->GetContext());
session->GetContext().reset();
diff --git a/src/nxemu-os/core/hle/service/server_manager.h b/src/nxemu-os/core/hle/service/server_manager.h
index b6aff866..7178aa64 100644
--- a/src/nxemu-os/core/hle/service/server_manager.h
+++ b/src/nxemu-os/core/hle/service/server_manager.h
@@ -75,6 +75,9 @@ private:
// Events
Kernel::KEvent* m_wakeup_event{};
+ // Manual-reset, signaled only at shutdown. Completion waits must never
+ // consume or clear the selection queue's notification.
+ Kernel::KEvent* m_shutdown_event{};
Kernel::KEvent* m_deferral_event{};
// Deferred wait list
diff --git a/src/nxemu-os/core/hle/service/service.cpp b/src/nxemu-os/core/hle/service/service.cpp
index c79898ef..4cfe1f83 100644
--- a/src/nxemu-os/core/hle/service/service.cpp
+++ b/src/nxemu-os/core/hle/service/service.cpp
@@ -10,6 +10,7 @@
#include "core/hle/ipc.h"
#include "core/hle/kernel/kernel.h"
#include "core/hle/kernel/k_thread.h"
+#include "core/hle/kernel/global_scheduler_context.h"
#include "core/hle/service/ipc_helpers.h"
#include "core/hle/service/service.h"
#include "core/hle/service/sm/sm.h"
@@ -116,6 +117,12 @@ Result ServiceFrameworkBase::HandleSyncRequest(Kernel::KServerSession& session,
HLERequestContext& ctx) {
const auto guard = LockService();
+ ctx.SetCompletionTime(0);
+ const auto work = system.Kernel().GlobalSchedulerContext().BeginHle();
+ const auto finish_work = [&] {
+ ctx.SetCompletionTime(system.Kernel().GlobalSchedulerContext().EndHle(work));
+ };
+ auto complete_work = SCOPE_GUARD { finish_work(); };
Result result = ResultSuccess;
switch (ctx.GetCommandType()) {
@@ -146,6 +153,8 @@ Result ServiceFrameworkBase::HandleSyncRequest(Kernel::KServerSession& session,
break;
}
+ finish_work();
+ complete_work.Cancel();
return result;
}
diff --git a/src/nxemu-os/core/hle_execution_time.cpp b/src/nxemu-os/core/hle_execution_time.cpp
new file mode 100644
index 00000000..0be3d0f6
--- /dev/null
+++ b/src/nxemu-os/core/hle_execution_time.cpp
@@ -0,0 +1,109 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include "core/hle_execution_time.h"
+#include
+#include
+#include
+#include
+#include "core/thread_time_validation.h"
+#if defined(_WIN32) && (defined(_M_X64) || defined(ARCHITECTURE_x86_64))
+#include
+#undef CreateEvent
+#include "yuzu_common/x64/cpu_detect.h"
+#include "yuzu_common/x64/rdtsc.h"
+#elif !defined(_WIN32)
+#include
+#endif
+
+namespace Core::Timing {
+#if defined(_WIN32) && (defined(_M_X64) || defined(ARCHITECTURE_x86_64))
+namespace {
+// QueryThreadCycleTime does not promise a portable conversion to time. Accept
+// its TSC conversion only after checking the counter domain on every logical
+// processor on which a migrated emulation thread could execute.
+bool HasCompatibleThreadCounter(u64 frequency) {
+ GROUP_AFFINITY previous{};
+ if (!GetThreadGroupAffinity(GetCurrentThread(), &previous)) return false;
+ bool compatible = true;
+ for (WORD group = 0; group < GetActiveProcessorGroupCount() && compatible; ++group) {
+ for (DWORD index = 0; index < GetActiveProcessorCount(group) && compatible; ++index) {
+ GROUP_AFFINITY affinity{};
+ affinity.Group = group;
+ affinity.Mask = KAFFINITY{1} << index;
+ if (!SetThreadGroupAffinity(GetCurrentThread(), &affinity, nullptr)) {
+ compatible = false;
+ break;
+ }
+ bool matched = false;
+ // TSC includes interrupts/descheduling; the thread counter does not.
+ // A 0.5 ms window could reject a compatible host during startup.
+ // Use a longer sample and bounded retries without relaxing the
+ // counter-domain tolerance. This runs only during initialization.
+ for (int attempt = 0; attempt < 8 && !matched; ++attempt) {
+ ULONG64 first{}, last{};
+ if (!QueryThreadCycleTime(GetCurrentThread(), &first)) break;
+ const auto start = Common::X64::FencedRDTSC();
+ // Short busy sample. Descheduling can only make the comparison
+ // too small; retry rather than accepting a different rate.
+ u64 end;
+ do { end = Common::X64::FencedRDTSC(); } while (end - start < frequency / 500);
+ if (!QueryThreadCycleTime(GetCurrentThread(), &last) || last < first) break;
+ const auto elapsed = end - start;
+ const auto cycles = last - first;
+ matched = elapsed && cycles >= elapsed - elapsed / 20 &&
+ cycles <= elapsed + elapsed / 20;
+ }
+ compatible = matched;
+ }
+ }
+ const bool restored = SetThreadGroupAffinity(GetCurrentThread(), &previous, nullptr) != 0;
+ return compatible && restored;
+}
+} // namespace
+#endif
+
+std::optional ReadNativeThreadTimeNs() {
+#if defined(_WIN32) && (defined(_M_X64) || defined(ARCHITECTURE_x86_64))
+ // Convert only on hosts whose thread counter was checked against the
+ // invariant TSC on every logical processor. Turbo CPU and QPC frequencies
+ // are not interchangeable with this validated counter domain.
+ static const u64 frequency = [] {
+ const auto& caps = Common::GetCPUCaps();
+ if (!caps.invariant_tsc) return u64{};
+ // CPUID may expose a ratio without its crystal frequency, or omit leaf
+ // 0x15 entirely. Calibrate the invariant TSC once before services start.
+ const auto rate = caps.tsc_frequency ? caps.tsc_frequency : Common::X64::EstimateRDTSCFrequency();
+ if (!rate) return u64{};
+ // Probe affinity on a short-lived worker: even a failed restoration
+ // must not alter the application thread on an unsupported host.
+ bool compatible{};
+ try {
+ std::thread probe{[&] { compatible = HasCompatibleThreadCounter(rate); }};
+ probe.join();
+ } catch (const std::system_error&) {
+ return u64{};
+ }
+ return compatible ? rate : u64{};
+ }();
+ if (!frequency || frequency > (std::numeric_limits::max)() / 1'000'000'000)
+ return std::nullopt;
+ ULONG64 ticks{};
+ if (!QueryThreadCycleTime(GetCurrentThread(), &ticks)) return std::nullopt;
+ const auto fraction = ticks % frequency * 1'000'000'000 / frequency;
+ const auto seconds = ticks / frequency;
+ if (seconds > ((std::numeric_limits::max)() - fraction) / 1'000'000'000)
+ return std::nullopt;
+ const auto cpu = seconds * 1'000'000'000 + fraction;
+ const auto wall = static_cast(std::chrono::duration_cast(
+ std::chrono::steady_clock::now().time_since_epoch()).count());
+ thread_local ThreadTimeValidation validation;
+ if (!validation.Observe(cpu, wall)) return std::nullopt;
+ return cpu;
+#elif defined(CLOCK_THREAD_CPUTIME_ID)
+ timespec time{};
+ if (clock_gettime(CLOCK_THREAD_CPUTIME_ID, &time) != 0) return std::nullopt;
+ return static_cast(time.tv_sec) * 1'000'000'000 + static_cast(time.tv_nsec);
+#else
+ return std::nullopt;
+#endif
+}
+} // namespace Core::Timing
diff --git a/src/nxemu-os/core/hle_execution_time.h b/src/nxemu-os/core/hle_execution_time.h
new file mode 100644
index 00000000..f0d2fa26
--- /dev/null
+++ b/src/nxemu-os/core/hle_execution_time.h
@@ -0,0 +1,53 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#pragma once
+
+#include
+#include
+#include "yuzu_common/common_types.h"
+
+namespace Core::Timing {
+
+// Native CPU time, not wall time or calibrated guest cycles. A missing sample
+// must not silently become wall time (which would charge waits as CPU work).
+std::optional ReadNativeThreadTimeNs();
+
+// One handler's logical execution across guest fiber switches. Each resumed
+// segment starts a new native-thread sample, including after a core migration.
+class HleExecutionTime {
+public:
+ void Begin(std::optional sample) {
+ collecting = true;
+ running = false;
+ valid = sample.has_value();
+ accumulated = 0;
+ Resume(sample);
+ }
+ void Suspend(std::optional sample) {
+ if (!collecting || !running) return;
+ valid &= sample.has_value() && *sample >= anchor;
+ if (valid) {
+ const auto elapsed = *sample - anchor;
+ valid = elapsed <= std::numeric_limits::max() - accumulated;
+ if (valid) accumulated += elapsed;
+ }
+ running = false;
+ }
+ void Resume(std::optional sample) {
+ if (!collecting || running) return;
+ valid &= sample.has_value();
+ if (sample) anchor = *sample;
+ running = true;
+ }
+ std::optional Finish(std::optional sample) {
+ Suspend(sample);
+ collecting = false;
+ return valid ? std::optional{accumulated} : std::nullopt;
+ }
+ bool IsCollecting() const { return collecting; }
+
+private:
+ u64 anchor{}, accumulated{};
+ bool collecting{}, running{}, valid{};
+};
+
+} // namespace Core::Timing
diff --git a/src/nxemu-os/core/shared_cpu_clock.h b/src/nxemu-os/core/shared_cpu_clock.h
new file mode 100644
index 00000000..aca67c5d
--- /dev/null
+++ b/src/nxemu-os/core/shared_cpu_clock.h
@@ -0,0 +1,199 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include "yuzu_common/common_types.h"
+#include "yuzu_common/hardware_properties.h"
+#include "yuzu_common/wall_clock.h"
+
+namespace Core::Timing {
+
+// Events follow the least advanced participating CPU. Scheduler-ready cores
+// remain participants across SVCs and fiber switches; cold JIT compilation is
+// excluded. Idle time follows the host, and pause freezes every participant.
+// This orders timed events without serializing CPU execution or guest memory.
+class SharedCpuClock {
+ struct alignas(64) Slot {
+ std::atomic base_ns{}, anchor{};
+ std::atomic compiling{};
+ };
+public:
+ void Reset(const Common::WallClock* clock) {
+ std::scoped_lock lock{writers};
+ host_clock = clock;
+ for (auto& slot : slots) {
+ slot.base_ns.store(0);
+ slot.anchor.store(0);
+ slot.compiling.store(false);
+ }
+ published.store(0);
+ active.store(0);
+ running.store(0);
+ runnable.store(0);
+ idle_base.store(0);
+ idle_anchor.store(HostNow());
+ pace_base.store(0);
+ pace_anchor.store(HostNow());
+ paused.store(true);
+ sequence.store(0);
+ }
+
+ u64 Now() const {
+ const auto version = sequence.load(std::memory_order_acquire);
+ if ((version & 1) || paused.load(std::memory_order_acquire))
+ return published.load(std::memory_order_acquire);
+ const auto host = HostNow();
+ const u32 mask = active.load(std::memory_order_acquire);
+ u64 candidate = std::numeric_limits::max();
+ if (!mask) {
+ candidate = idle_base.load(std::memory_order_relaxed) +
+ host - idle_anchor.load(std::memory_order_relaxed);
+ } else {
+ for (u32 core = 0; core < slots.size(); ++core) {
+ if (!(mask & (1U << core))) continue;
+ const auto& slot = slots[core];
+ candidate = std::min(candidate, slot.base_ns.load(std::memory_order_relaxed) +
+ (slot.compiling.load(std::memory_order_relaxed) ? 0 :
+ host - slot.anchor.load(std::memory_order_relaxed)));
+ }
+ }
+ candidate = std::min(candidate, pace_base.load(std::memory_order_relaxed) +
+ host - pace_anchor.load(std::memory_order_relaxed));
+ std::atomic_thread_fence(std::memory_order_acquire);
+ if (sequence.load(std::memory_order_acquire) != version)
+ return published.load(std::memory_order_acquire);
+ auto previous = published.load(std::memory_order_relaxed);
+ while (previous < candidate && !published.compare_exchange_weak(
+ previous, candidate, std::memory_order_release, std::memory_order_relaxed)) {}
+ return std::max(previous, candidate);
+ }
+
+ // A frozen participant cannot reach this deadline until compilation ends.
+ // Take a consistent snapshot; a racing transition simply retries normally.
+ bool CompilationBlocks(u64 deadline) const {
+ const auto version = sequence.load(std::memory_order_acquire);
+ if (version & 1) return false;
+ const auto mask = active.load(std::memory_order_acquire);
+ bool blocked{};
+ for (u32 core = 0; core < slots.size(); ++core) {
+ if ((mask & (1U << core)) && slots[core].compiling.load(std::memory_order_relaxed) &&
+ slots[core].base_ns.load(std::memory_order_relaxed) < deadline) blocked = true;
+ }
+ std::atomic_thread_fence(std::memory_order_acquire);
+ return blocked && sequence.load(std::memory_order_acquire) == version;
+ }
+
+ void Begin(u32 core) {
+ std::scoped_lock lock{writers};
+ const auto now = Now();
+ sequence.fetch_add(1, std::memory_order_acq_rel);
+ // Pair relaxed payload stores with the reader's acquire fence, so a
+ // reader observing a new field also observes the odd sequence.
+ std::atomic_thread_fence(std::memory_order_release);
+ auto& slot = slots.at(core);
+ const auto host = HostNow();
+ const bool participates = active.load(std::memory_order_relaxed) & (1U << core);
+ const auto own = !participates ? now : slot.base_ns.load(std::memory_order_relaxed) +
+ (!paused.load(std::memory_order_relaxed) && !slot.compiling.load(std::memory_order_relaxed) ?
+ host - slot.anchor.load(std::memory_order_relaxed) : 0);
+ slot.base_ns.store(std::max(now, own), std::memory_order_relaxed);
+ slot.anchor.store(host, std::memory_order_relaxed);
+ slot.compiling.store(false, std::memory_order_relaxed);
+ running.fetch_or(1U << core, std::memory_order_relaxed);
+ active.fetch_or(1U << core, std::memory_order_relaxed);
+ sequence.fetch_add(1, std::memory_order_release);
+ }
+
+ void End(u32 core) {
+ std::scoped_lock lock{writers};
+ const auto now = Now();
+ sequence.fetch_add(1, std::memory_order_acq_rel);
+ std::atomic_thread_fence(std::memory_order_release);
+ auto& slot = slots.at(core);
+ const auto host = HostNow();
+ Accumulate(slot, host);
+ const auto runs = running.load(std::memory_order_relaxed) & ~(1U << core);
+ running.store(runs, std::memory_order_relaxed);
+ active.store(runs | runnable.load(std::memory_order_relaxed), std::memory_order_relaxed);
+ idle_base.store(now, std::memory_order_relaxed);
+ idle_anchor.store(host, std::memory_order_relaxed);
+ sequence.fetch_add(1, std::memory_order_release);
+ }
+
+ void Pause(bool value) {
+ std::scoped_lock lock{writers};
+ const auto now = Now();
+ const auto host = HostNow();
+ sequence.fetch_add(1, std::memory_order_acq_rel);
+ std::atomic_thread_fence(std::memory_order_release);
+ idle_base.store(now, std::memory_order_relaxed);
+ idle_anchor.store(host, std::memory_order_relaxed);
+ pace_base.store(now, std::memory_order_relaxed);
+ pace_anchor.store(host, std::memory_order_relaxed);
+ for (u32 core = 0; core < slots.size(); ++core) {
+ auto& slot = slots[core];
+ if (active.load(std::memory_order_relaxed) & (1U << core)) Accumulate(slot, host);
+ slot.base_ns.store(std::max(now, slot.base_ns.load(std::memory_order_relaxed)), std::memory_order_relaxed);
+ slot.anchor.store(host, std::memory_order_relaxed);
+ }
+ paused.store(value, std::memory_order_relaxed);
+ sequence.fetch_add(1, std::memory_order_release);
+ }
+
+ // Apply one scheduler snapshot after migrations. A CPU leaves only after
+ // its JIT run has also returned; a newly ready CPU starts at common time.
+ void SetRunnableMask(u32 mask) {
+ if (runnable.load(std::memory_order_relaxed) == mask) return;
+ std::scoped_lock lock{writers};
+ if (runnable.load(std::memory_order_relaxed) == mask) return;
+ const auto now = Now();
+ const auto host = HostNow();
+ sequence.fetch_add(1, std::memory_order_acq_rel);
+ std::atomic_thread_fence(std::memory_order_release);
+ const auto previous = active.load(std::memory_order_relaxed);
+ for (u32 core = 0; core < slots.size(); ++core) {
+ if (!(mask & (1U << core)) || (previous & (1U << core))) continue;
+ auto& slot = slots[core];
+ slot.base_ns.store(now, std::memory_order_relaxed);
+ slot.anchor.store(host, std::memory_order_relaxed);
+ slot.compiling.store(false, std::memory_order_relaxed);
+ }
+ runnable.store(mask, std::memory_order_relaxed);
+ active.store(mask | running.load(std::memory_order_relaxed), std::memory_order_relaxed);
+ idle_base.store(now, std::memory_order_relaxed);
+ idle_anchor.store(host, std::memory_order_relaxed);
+ sequence.fetch_add(1, std::memory_order_release);
+ }
+
+ void Compile(u32 core, bool value) {
+ std::scoped_lock lock{writers};
+ if (!(running.load(std::memory_order_relaxed) & (1U << core))) return;
+ Now();
+ sequence.fetch_add(1, std::memory_order_acq_rel);
+ std::atomic_thread_fence(std::memory_order_release);
+ auto& slot = slots.at(core);
+ const auto host = HostNow();
+ Accumulate(slot, host);
+ slot.compiling.store(value, std::memory_order_relaxed);
+ sequence.fetch_add(1, std::memory_order_release);
+ }
+private:
+ void Accumulate(Slot& slot, u64 host) {
+ if (!paused.load(std::memory_order_relaxed) && !slot.compiling.load(std::memory_order_relaxed))
+ slot.base_ns.fetch_add(host - slot.anchor.load(std::memory_order_relaxed), std::memory_order_relaxed);
+ slot.anchor.store(host, std::memory_order_relaxed);
+ }
+ u64 HostNow() const { return host_clock->GetTimeNS().count(); }
+ std::array slots{};
+ mutable std::mutex writers;
+ std::atomic sequence{}, idle_base{}, idle_anchor{}, pace_base{}, pace_anchor{};
+ std::atomic active{}, running{}, runnable{};
+ std::atomic paused{true};
+ mutable std::atomic published{};
+ const Common::WallClock* host_clock{}; // Stable while CPU/timer threads run.
+};
+} // namespace Core::Timing
diff --git a/src/nxemu-os/core/thread_time_validation.h b/src/nxemu-os/core/thread_time_validation.h
new file mode 100644
index 00000000..ce1f83ef
--- /dev/null
+++ b/src/nxemu-os/core/thread_time_validation.h
@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#pragma once
+
+#include "yuzu_common/common_types.h"
+
+namespace Core::Timing {
+
+// One instance per native thread, not per guest fiber. Reject counter reversal
+// or CPU work exceeding elapsed host time. This upper bound does not prove
+// conversion accuracy or detect undercounting; startup calibration is retained.
+class ThreadTimeValidation {
+public:
+ bool Observe(u64 cpu, u64 wall) {
+ if (failed) return false;
+ if (initialized) {
+ if (cpu < previous_cpu || wall < previous_wall) return Fail();
+ const auto work = cpu - previous_cpu;
+ const auto elapsed = wall - previous_wall;
+ // QPC and the CPU counter are sampled separately. Leave absolute
+ // noise room; tiny handlers must not fail due to sampling overhead.
+ if (work > elapsed && work - elapsed > elapsed / 10 + 50'000) return Fail();
+ }
+ previous_cpu = cpu;
+ previous_wall = wall;
+ initialized = true;
+ return true;
+ }
+private:
+ bool Fail() { failed = true; return false; }
+ u64 previous_cpu{}, previous_wall{};
+ bool initialized{}, failed{};
+};
+} // namespace Core::Timing
diff --git a/src/nxemu-os/nxemu-os.vcxproj b/src/nxemu-os/nxemu-os.vcxproj
index 67a1ac13..c8708dfe 100644
--- a/src/nxemu-os/nxemu-os.vcxproj
+++ b/src/nxemu-os/nxemu-os.vcxproj
@@ -270,6 +270,7 @@
+
@@ -530,6 +531,8 @@
+
+
diff --git a/src/nxemu-os/nxemu-os.vcxproj.filters b/src/nxemu-os/nxemu-os.vcxproj.filters
index d0372b76..4ab9da06 100644
--- a/src/nxemu-os/nxemu-os.vcxproj.filters
+++ b/src/nxemu-os/nxemu-os.vcxproj.filters
@@ -558,6 +558,12 @@
Header Files\core
+
+ Header Files\core
+
+
+ Header Files\core
+
Header Files\core
@@ -2164,6 +2170,9 @@
Source Files\core
+
+ Source Files\core
+
Source Files\core
diff --git a/src/nxemu-os/os_manager.cpp b/src/nxemu-os/os_manager.cpp
index f55d76a8..b85054a2 100644
--- a/src/nxemu-os/os_manager.cpp
+++ b/src/nxemu-os/os_manager.cpp
@@ -253,7 +253,7 @@ bool OSManager::CreateApplicationProcess(uint64_t codeSize, const IProgramMetada
{
return false;
}
- m_coreSystem.InitializeKernel(metaData.GetTitleID());
+ m_coreSystem.InitializeKernel(metaData.GetTitleID(), metaData.Is64BitProgram());
Kernel::KernelCore & kernel = m_coreSystem.Kernel();
m_applicationProcess = Kernel::KProcess::Create(kernel);
if (m_applicationProcess == nullptr)
diff --git a/src/nxemu-os/os_settings.cpp b/src/nxemu-os/os_settings.cpp
index 55cffde7..acda9be7 100644
--- a/src/nxemu-os/os_settings.cpp
+++ b/src/nxemu-os/os_settings.cpp
@@ -482,6 +482,7 @@ static OsSetting settings[] = {
{NXOsSetting::AudioMuted, "audio\\muted", &osSettings.audio_muted, false},
{NXOsSetting::SpeedLimit, "system\\speed_limit", &osSettings.speed_limit, 100, 0, 9999},
{NXOsSetting::UseMultiCore, "system\\use_multi_core", &osSettings.use_multi_core, true},
+ {NXOsSetting::CpuHleSynchronization, "system\\cpu_hle_synchronization", &osSettings.cpu_hle_synchronization, false},
{NXOsSetting::UseSpeedLimit, nullptr, &osSettings.use_speed_limit, true},
{NXOsSetting::MemoryLayout, "system\\memory_layout_mode", &osSettings.memory_layout_mode, MemoryLayout::Memory_4Gb},
{NXOsSetting::LanguageIndex, "system\\language_index", &osSettings.language_index, Language::EnglishAmerican},
diff --git a/src/nxemu-os/os_settings.h b/src/nxemu-os/os_settings.h
index 79e61824..a9b0a671 100644
--- a/src/nxemu-os/os_settings.h
+++ b/src/nxemu-os/os_settings.h
@@ -57,6 +57,7 @@ struct OSSettings
s32 current_user;
DockedMode use_docked_mode;
bool use_multi_core;
+ bool cpu_hle_synchronization;
bool use_speed_limit;
u16 speed_limit;
diff --git a/src/nxemu-os/os_settings_identifiers.h b/src/nxemu-os/os_settings_identifiers.h
index 2af1bf18..15a6ed44 100644
--- a/src/nxemu-os/os_settings_identifiers.h
+++ b/src/nxemu-os/os_settings_identifiers.h
@@ -9,6 +9,7 @@ namespace NXOsSetting
constexpr const char * AudioVolume = "nxos:AudioVolume";
constexpr const char * AudioMuted = "nxos:AudioMuted";
constexpr const char * SpeedLimit = "nxos:SpeedLimit";
+ constexpr const char * CpuHleSynchronization = "nxos:CpuHleSynchronization";
constexpr const char * UseMultiCore = "nxos:UseMultiCore";
constexpr const char * UseSpeedLimit = "nxos:UseSpeedLimit";
constexpr const char * DockedMode = "nxos:DockedMode";
diff --git a/src/nxemu/user_interface/settings/system_config_logging.cpp b/src/nxemu/user_interface/settings/system_config_logging.cpp
index c80d280f..698be3cc 100644
--- a/src/nxemu/user_interface/settings/system_config_logging.cpp
+++ b/src/nxemu/user_interface/settings/system_config_logging.cpp
@@ -9,6 +9,7 @@ namespace
static ConfigSetting loggingSettings[] = {
ConfigSetting(ConfigSetting::CheckBox, "ShowLogConsole", true, NXCoreSetting::ShowLogConsole),
ConfigSetting(ConfigSetting::CheckBox, "UseMultiCore", false, NXOsSetting::UseMultiCore),
+ ConfigSetting(ConfigSetting::CheckBox, "CpuHleSynchronization", false, NXOsSetting::CpuHleSynchronization),
ConfigSetting(ConfigSetting::InputText, "LogFilter", true, NXCoreSetting::LogFilter),
};
}