Split 335 7/7 Synchronize CPU/HLE (#334)

This commit is contained in:
Bruno 2026-10-04 12:30:59 +02:00 committed by GitHub
parent 720f7712ca
commit f17045fefe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 857 additions and 49 deletions

View File

@ -58,6 +58,10 @@
<div class="left"><label for="UseMultiCore">Multicore CPU mode</label></div>
<div class="right"><input type="checkbox" id="UseMultiCore" tooltip="Uncheck to use single-core mode. Applied when the next game starts."></div>
</div>
<div class="form-row checkbox-row">
<div class="left"><label for="CpuHleSynchronization">CPU/HLE Synchronization</label></div>
<div class="right"><input type="checkbox" id="CpuHleSynchronization" tooltip="Synchronizes CPU progress and service replies. Applied when the next game starts. Requires multicore mode and a supported host."></div>
</div>
</div>
</Page>
</PageNav>

View File

@ -4,6 +4,7 @@
#include "cpu_settings.h"
#include <array>
#include <limits>
#include "yuzu_common/scope_exit.h"
#include <yuzu_common/logging/log.h>
#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<u32> 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<Dynarmic::A64::Jit> 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*/)

View File

@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <atomic>
#include "dynarmic/interface/A64/a64.h"
#include <yuzu_common/hardware_properties.h>
#include <nxemu-module-spec/cpu.h>
@ -59,4 +60,5 @@ private:
uint32_t m_svc;
IKernelProcess & m_process;
uint32_t m_coreIndex;
bool m_cpu_hle_synchronization{};
};

View File

@ -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,

View File

@ -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;

View File

@ -12,6 +12,7 @@
#include "dynarmic/common/cast_util.h"
#include "dynarmic/common/fp/fpcr.h"
#include "dynarmic/interface/exclusive_monitor.h"
#include <mcl/scope_exit.hpp>
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;

View File

@ -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;

View File

@ -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) {

View File

@ -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<std::uint32_t> 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.

View File

@ -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

View File

@ -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

View File

@ -3,6 +3,7 @@
#include <array>
#include <atomic>
#include <cstdlib>
#include <optional>
#include "yuzu_audio_core/audio_core.h"
@ -34,6 +35,7 @@
#include "yuzu_input_common/main.h"
#include "network/network.h"
#include <nxemu-module-spec/system_loader.h>
#include <nxemu-cpu/cpu_settings_identifiers.h>
#include <nxemu-video/video_settings_identifiers.h>
#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<std::mutex> 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<std::mutex> 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() {

View File

@ -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

View File

@ -5,8 +5,12 @@
#include <mutex>
#include <string>
#include <tuple>
#include "core/hle_execution_time.h"
#include "os_settings.h"
#ifdef _WIN32
#include <Windows.h>
#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<EventType> CreateEvent(std::string name, TimedCallback&& callback) {
return std::make_shared<EventType>(std::move(callback), std::move(name));
}
@ -57,12 +93,36 @@ void CoreTiming::ThreadEntry(CoreTiming& instance) {
instance.ThreadLoop();
}
void CoreTiming::Initialize(std::function<void()>&& on_thread_init_) {
void CoreTiming::Initialize(std::function<void()>&& 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<WindowsTimerWait>();
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<std::jthread>(ThreadEntry, std::ref(*this));
}
@ -71,12 +131,16 @@ void CoreTiming::Initialize(std::function<void()>&& 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<EventType>& 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<std::chrono::microseconds>(GetGlobalTimeNs());
if (is_multicore) [[likely]] {
if (fallback_time_offset_ns.load(std::memory_order_acquire))
return std::chrono::duration_cast<std::chrono::microseconds>(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<s64>(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

View File

@ -19,6 +19,7 @@
#include "yuzu_common/wall_clock.h"
#include <nxemu-module-spec/cpu.h>
#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<void()>&& 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<void()>&& 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<Common::WallClock> clock;
SharedCpuClock shared_clock;
std::atomic<bool> cpu_hle_native_clock{};
std::atomic<bool> cpu_hle_enabled{};
std::atomic<bool> cpu_hle_failed{};
std::atomic<s64> 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<WindowsTimerWait> windows_timer_wait;
s64 timer_resolution_ns{1'000'000};
#endif

View File

@ -2,6 +2,11 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <mutex>
#include <algorithm>
#include <limits>
#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<u64>(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<u64>(std::numeric_limits<s64>::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);

View File

@ -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<KThread*> m_thread_list;
std::mutex m_global_list_guard;
};
} // namespace Kernel

View File

@ -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);
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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{};

View File

@ -65,6 +65,18 @@ void NVDRV::ServiceError(HLERequestContext& ctx, NvResult result) {
rb.PushEnum(result);
}
void NVDRV::PrepareOutputBuffer(HLERequestContext& ctx, Common::ScratchBuffer<u8>& buffer,
size_t index) {
const auto size = ctx.GetWriteBufferSize(index);
if (ctx.ResponseBuffersDeferred() && size && !output_pool)
output_pool = std::make_shared<IpcResponseBufferPool>();
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<DeviceFD>();
@ -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<const u8>{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};

View File

@ -40,6 +40,7 @@ private:
void DumpGraphicsMemoryInfo(HLERequestContext& ctx);
void ServiceError(HLERequestContext& ctx, NvResult result);
void PrepareOutputBuffer(HLERequestContext& ctx, Common::ScratchBuffer<u8>& buffer, size_t index);
std::shared_ptr<Module> nvdrv;
@ -54,6 +55,7 @@ private:
NvCore::SessionId session_id{};
Common::ScratchBuffer<u8> output_buffer;
Common::ScratchBuffer<u8> inline_output_buffer;
std::shared_ptr<IpcResponseBufferPool> output_pool;
};
} // namespace Service::Nvidia

View File

@ -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<u64>(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<s64>(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();

View File

@ -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

View File

@ -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;
}

View File

@ -0,0 +1,109 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "core/hle_execution_time.h"
#include <limits>
#include <thread>
#include <system_error>
#include <chrono>
#include "core/thread_time_validation.h"
#if defined(_WIN32) && (defined(_M_X64) || defined(ARCHITECTURE_x86_64))
#include <Windows.h>
#undef CreateEvent
#include "yuzu_common/x64/cpu_detect.h"
#include "yuzu_common/x64/rdtsc.h"
#elif !defined(_WIN32)
#include <time.h>
#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<u64> 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<u64>::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<u64>::max)() - fraction) / 1'000'000'000)
return std::nullopt;
const auto cpu = seconds * 1'000'000'000 + fraction;
const auto wall = static_cast<u64>(std::chrono::duration_cast<std::chrono::nanoseconds>(
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<u64>(time.tv_sec) * 1'000'000'000 + static_cast<u64>(time.tv_nsec);
#else
return std::nullopt;
#endif
}
} // namespace Core::Timing

View File

@ -0,0 +1,53 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <optional>
#include <limits>
#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<u64> 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<u64> sample) {
collecting = true;
running = false;
valid = sample.has_value();
accumulated = 0;
Resume(sample);
}
void Suspend(std::optional<u64> sample) {
if (!collecting || !running) return;
valid &= sample.has_value() && *sample >= anchor;
if (valid) {
const auto elapsed = *sample - anchor;
valid = elapsed <= std::numeric_limits<u64>::max() - accumulated;
if (valid) accumulated += elapsed;
}
running = false;
}
void Resume(std::optional<u64> sample) {
if (!collecting || running) return;
valid &= sample.has_value();
if (sample) anchor = *sample;
running = true;
}
std::optional<u64> Finish(std::optional<u64> sample) {
Suspend(sample);
collecting = false;
return valid ? std::optional<u64>{accumulated} : std::nullopt;
}
bool IsCollecting() const { return collecting; }
private:
u64 anchor{}, accumulated{};
bool collecting{}, running{}, valid{};
};
} // namespace Core::Timing

View File

@ -0,0 +1,199 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <algorithm>
#include <array>
#include <atomic>
#include <limits>
#include <mutex>
#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<u64> base_ns{}, anchor{};
std::atomic<bool> 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<u64>::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<Slot, Hardware::NUM_CPU_CORES> slots{};
mutable std::mutex writers;
std::atomic<u64> sequence{}, idle_base{}, idle_anchor{}, pace_base{}, pace_anchor{};
std::atomic<u32> active{}, running{}, runnable{};
std::atomic<bool> paused{true};
mutable std::atomic<u64> published{};
const Common::WallClock* host_clock{}; // Stable while CPU/timer threads run.
};
} // namespace Core::Timing

View File

@ -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

View File

@ -270,6 +270,7 @@
<ClCompile Include="emu_thread.cpp" />
<ClCompile Include="nxemu-os.cpp" />
<ClCompile Include="core\core_timing.cpp" />
<ClCompile Include="core\hle_execution_time.cpp" />
<ClCompile Include="core\device_memory.cpp" />
<ClCompile Include="core\cpu_manager.cpp" />
<ClCompile Include="core\memory.cpp" />
@ -530,6 +531,8 @@
<ClInclude Include="core\constants.h" />
<ClInclude Include="core\core.h" />
<ClInclude Include="core\core_timing.h" />
<ClInclude Include="core\shared_cpu_clock.h" />
<ClInclude Include="core\hle_execution_time.h" />
<ClInclude Include="core\cpu_manager.h" />
<ClInclude Include="core\debugger\debugger.h" />
<ClInclude Include="core\device_memory.h" />

View File

@ -558,6 +558,12 @@
<ClInclude Include="core\core_timing.h">
<Filter>Header Files\core</Filter>
</ClInclude>
<ClInclude Include="core\shared_cpu_clock.h">
<Filter>Header Files\core</Filter>
</ClInclude>
<ClInclude Include="core\hle_execution_time.h">
<Filter>Header Files\core</Filter>
</ClInclude>
<ClInclude Include="core\device_memory.h">
<Filter>Header Files\core</Filter>
</ClInclude>
@ -2164,6 +2170,9 @@
<ClCompile Include="core\core_timing.cpp">
<Filter>Source Files\core</Filter>
</ClCompile>
<ClCompile Include="core\hle_execution_time.cpp">
<Filter>Source Files\core</Filter>
</ClCompile>
<ClCompile Include="core\device_memory.cpp">
<Filter>Source Files\core</Filter>
</ClCompile>

View File

@ -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)

View File

@ -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},

View File

@ -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;

View File

@ -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";

View File

@ -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),
};
}