From f17045fefe1484d12829ac6db5eed7067cf202d9 Mon Sep 17 00:00:00 2001 From: Bruno <56844494+fab918@users.noreply.github.com> Date: Sun, 4 Oct 2026 12:30:59 +0200 Subject: [PATCH] Split 335 7/7 Synchronize CPU/HLE (#334) --- lang/english/html/system_config_general.html | 4 + src/nxemu-cpu/arm_dynarmic_64.cpp | 26 ++- src/nxemu-cpu/arm_dynarmic_64.h | 2 + .../backend/arm64/a64_address_space.cpp | 4 + .../backend/arm64/a64_address_space.h | 1 + .../dynarmic/backend/arm64/address_space.cpp | 3 + .../dynarmic/backend/arm64/address_space.h | 2 + .../dynarmic/backend/x64/a64_interface.cpp | 5 +- src/nxemu-cpu/dynarmic/interface/A64/config.h | 3 + src/nxemu-module-spec/base.h | 4 +- src/nxemu-module-spec/cpu.h | 4 + src/nxemu-os/core/core.cpp | 73 +++++-- src/nxemu-os/core/core.h | 2 +- src/nxemu-os/core/core_timing.cpp | 185 +++++++++++++++- src/nxemu-os/core/core_timing.h | 25 ++- .../hle/kernel/global_scheduler_context.cpp | 42 +++- .../hle/kernel/global_scheduler_context.h | 8 + src/nxemu-os/core/hle/kernel/k_scheduler.cpp | 13 ++ src/nxemu-os/core/hle/kernel/k_thread.h | 15 ++ src/nxemu-os/core/hle/service/hle_ipc.cpp | 2 + src/nxemu-os/core/hle/service/hle_ipc.h | 3 + .../hle/service/nvdrv/nvdrv_interface.cpp | 28 ++- .../core/hle/service/nvdrv/nvdrv_interface.h | 2 + .../core/hle/service/server_manager.cpp | 26 +++ .../core/hle/service/server_manager.h | 3 + src/nxemu-os/core/hle/service/service.cpp | 9 + src/nxemu-os/core/hle_execution_time.cpp | 109 ++++++++++ src/nxemu-os/core/hle_execution_time.h | 53 +++++ src/nxemu-os/core/shared_cpu_clock.h | 199 ++++++++++++++++++ src/nxemu-os/core/thread_time_validation.h | 33 +++ src/nxemu-os/nxemu-os.vcxproj | 3 + src/nxemu-os/nxemu-os.vcxproj.filters | 9 + src/nxemu-os/os_manager.cpp | 2 +- src/nxemu-os/os_settings.cpp | 1 + src/nxemu-os/os_settings.h | 1 + src/nxemu-os/os_settings_identifiers.h | 1 + .../settings/system_config_logging.cpp | 1 + 37 files changed, 857 insertions(+), 49 deletions(-) create mode 100644 src/nxemu-os/core/hle_execution_time.cpp create mode 100644 src/nxemu-os/core/hle_execution_time.h create mode 100644 src/nxemu-os/core/shared_cpu_clock.h create mode 100644 src/nxemu-os/core/thread_time_validation.h 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), }; }