Split 335 7/7 Synchronize CPU/HLE (#334)
This commit is contained in:
parent
720f7712ca
commit
f17045fefe
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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*/)
|
||||
|
|
|
|||
|
|
@ -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{};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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{};
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue