From 1be668e68a1937f2af239e2707ab914286018892 Mon Sep 17 00:00:00 2001 From: riperiperi Date: Thu, 30 Nov 2023 10:39:42 -0800 Subject: HLE: Add OS-specific precise sleep methods to reduce spinwaiting (#5948) * feat: add nanosleep for linux and macos * Add Windows 0.5ms sleep - Imprecise waits for longer waits with clock alignment - 1/4 the spin time on vsync timer * Remove old experiment * Fix event leak * Tweaking for MacOS * Linux tweaks, nanosleep vsync improvement * Fix overbias * Cleanup * Fix realignment * Add some docs and some cleanup NanosleepPool needs more, Nanosleep has some benchmark code that needs removed. * Rename "Microsleep" to "PreciseSleep" Might have been confused with "microseconds", which no measurement is performed in. * Remove nanosleep measurement * Remove unused debug logging * Nanosleep Pool Documentation * More cleanup * Whitespace * Formatting * Address Feedback * Allow SleepUntilTimePoint to take EventWaitHandle * Remove `_chrono` stopwatch in SurfaceFlinger * Move spinwaiting logic to PreciseSleepHelper Technically, these achieve different things, but having them here makes them easier to reuse or tune. --- .../PreciseSleep/WindowsGranularTimer.cs | 220 +++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 src/Ryujinx.Common/PreciseSleep/WindowsGranularTimer.cs (limited to 'src/Ryujinx.Common/PreciseSleep/WindowsGranularTimer.cs') diff --git a/src/Ryujinx.Common/PreciseSleep/WindowsGranularTimer.cs b/src/Ryujinx.Common/PreciseSleep/WindowsGranularTimer.cs new file mode 100644 index 00000000..a0de1634 --- /dev/null +++ b/src/Ryujinx.Common/PreciseSleep/WindowsGranularTimer.cs @@ -0,0 +1,220 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Threading; + +namespace Ryujinx.Common.SystemInterop +{ + /// + /// Timer that attempts to align with the hardware timer interrupt, + /// and can alert listeners on ticks. + /// + [SupportedOSPlatform("windows")] + internal partial class WindowsGranularTimer + { + private const int MinimumGranularity = 5000; + + private static readonly WindowsGranularTimer _instance = new(); + public static WindowsGranularTimer Instance => _instance; + + private readonly struct WaitingObject + { + public readonly long Id; + public readonly EventWaitHandle Signal; + public readonly long TimePoint; + + public WaitingObject(long id, EventWaitHandle signal, long timePoint) + { + Id = id; + Signal = signal; + TimePoint = timePoint; + } + } + + [LibraryImport("ntdll.dll", SetLastError = true)] + private static partial int NtSetTimerResolution(int DesiredResolution, [MarshalAs(UnmanagedType.Bool)] bool SetResolution, out int CurrentResolution); + + [LibraryImport("ntdll.dll", SetLastError = true)] + private static partial int NtQueryTimerResolution(out int MaximumResolution, out int MinimumResolution, out int CurrentResolution); + + [LibraryImport("ntdll.dll", SetLastError = true)] + private static partial uint NtDelayExecution([MarshalAs(UnmanagedType.Bool)] bool Alertable, ref long DelayInterval); + + public long GranularityNs => _granularityNs; + public long GranularityTicks => _granularityTicks; + + private readonly Thread _timerThread; + private long _granularityNs = MinimumGranularity * 100L; + private long _granularityTicks; + private long _lastTicks = PerformanceCounter.ElapsedTicks; + private long _lastId; + + private readonly object _lock = new(); + private readonly List _waitingObjects = new(); + + private WindowsGranularTimer() + { + _timerThread = new Thread(Loop) + { + IsBackground = true, + Name = "Common.WindowsTimer", + Priority = ThreadPriority.Highest + }; + + _timerThread.Start(); + } + + /// + /// Measure and initialize the timer's target granularity. + /// + private void Initialize() + { + NtQueryTimerResolution(out _, out int min, out int curr); + + if (min > 0) + { + min = Math.Max(min, MinimumGranularity); + + _granularityNs = min * 100L; + NtSetTimerResolution(min, true, out _); + } + else + { + _granularityNs = curr * 100L; + } + + _granularityTicks = (_granularityNs * PerformanceCounter.TicksPerMillisecond) / 1_000_000; + } + + /// + /// Main loop for the timer thread. Wakes every clock tick and signals any listeners, + /// as well as keeping track of clock alignment. + /// + private void Loop() + { + Initialize(); + while (true) + { + long delayInterval = -1; // Next tick + NtSetTimerResolution((int)(_granularityNs / 100), true, out _); + NtDelayExecution(false, ref delayInterval); + + long newTicks = PerformanceCounter.ElapsedTicks; + long nextTicks = newTicks + _granularityTicks; + + lock (_lock) + { + for (int i = 0; i < _waitingObjects.Count; i++) + { + if (nextTicks > _waitingObjects[i].TimePoint) + { + // The next clock tick will be after the timepoint, we need to signal now. + _waitingObjects[i].Signal.Set(); + + _waitingObjects.RemoveAt(i--); + } + } + + _lastTicks = newTicks; + } + } + } + + /// + /// Sleep until a timepoint. + /// + /// Reset event to use to be awoken by the clock tick, or an external signal + /// Target timepoint + /// True if waited or signalled, false otherwise + public bool SleepUntilTimePoint(AutoResetEvent evt, long timePoint) + { + if (evt.WaitOne(0)) + { + return true; + } + + long id; + + lock (_lock) + { + // Return immediately if the next tick is after the requested timepoint. + long nextTicks = _lastTicks + _granularityTicks; + + if (nextTicks > timePoint) + { + return false; + } + + id = ++_lastId; + + _waitingObjects.Add(new WaitingObject(id, evt, timePoint)); + } + + evt.WaitOne(); + + lock (_lock) + { + for (int i = 0; i < _waitingObjects.Count; i++) + { + if (id == _waitingObjects[i].Id) + { + _waitingObjects.RemoveAt(i--); + break; + } + } + } + + return true; + } + + /// + /// Sleep until a timepoint, but don't expect any external signals. + /// + /// + /// Saves some effort compared to the sleep that expects to be signalled. + /// + /// Reset event to use to be awoken by the clock tick + /// Target timepoint + /// True if waited, false otherwise + public bool SleepUntilTimePointWithoutExternalSignal(EventWaitHandle evt, long timePoint) + { + long id; + + lock (_lock) + { + // Return immediately if the next tick is after the requested timepoint. + long nextTicks = _lastTicks + _granularityTicks; + + if (nextTicks > timePoint) + { + return false; + } + + id = ++_lastId; + + _waitingObjects.Add(new WaitingObject(id, evt, timePoint)); + } + + evt.WaitOne(); + + return true; + } + + /// + /// Returns the two nearest clock ticks for a given timepoint. + /// + /// Target timepoint + /// The nearest clock ticks before and after the given timepoint + public (long, long) ReturnNearestTicks(long timePoint) + { + long last = _lastTicks; + long delta = timePoint - last; + + long lowTicks = delta / _granularityTicks; + long highTicks = (delta + _granularityTicks - 1) / _granularityTicks; + + return (last + lowTicks * _granularityTicks, last + highTicks * _granularityTicks); + } + } +} -- cgit v1.2.3