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.
This commit is contained in:
parent
21cd4c0c00
commit
1be668e68a
10 changed files with 1000 additions and 49 deletions
38
src/Ryujinx.Common/PreciseSleep/IPreciseSleepEvent.cs
Normal file
38
src/Ryujinx.Common/PreciseSleep/IPreciseSleepEvent.cs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Ryujinx.Common.PreciseSleep
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An event which works similarly to an AutoResetEvent, but is backed by a
|
||||||
|
/// more precise timer that allows waits of less than a millisecond.
|
||||||
|
/// </summary>
|
||||||
|
public interface IPreciseSleepEvent : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Adjust a timepoint to better fit the host clock.
|
||||||
|
/// When no adjustment is made, the input timepoint will be returned.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="timePoint">Timepoint to adjust</param>
|
||||||
|
/// <param name="timeoutNs">Requested timeout in nanoseconds</param>
|
||||||
|
/// <returns>Adjusted timepoint</returns>
|
||||||
|
long AdjustTimePoint(long timePoint, long timeoutNs);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sleep until a timepoint, or a signal is received.
|
||||||
|
/// Given no signal, may wake considerably before, or slightly after the timeout.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="timePoint">Timepoint to sleep until</param>
|
||||||
|
/// <returns>True if signalled or waited, false if a wait could not be performed</returns>
|
||||||
|
bool SleepUntil(long timePoint);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sleep until a signal is received.
|
||||||
|
/// </summary>
|
||||||
|
void Sleep();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Signal the event, waking any sleeping thread or the next attempted sleep.
|
||||||
|
/// </summary>
|
||||||
|
void Signal();
|
||||||
|
}
|
||||||
|
}
|
160
src/Ryujinx.Common/PreciseSleep/Nanosleep.cs
Normal file
160
src/Ryujinx.Common/PreciseSleep/Nanosleep.cs
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Runtime.Versioning;
|
||||||
|
|
||||||
|
namespace Ryujinx.Common.PreciseSleep
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Access to Linux/MacOS nanosleep, with platform specific bias to improve precision.
|
||||||
|
/// </summary>
|
||||||
|
[SupportedOSPlatform("macos")]
|
||||||
|
[SupportedOSPlatform("linux")]
|
||||||
|
[SupportedOSPlatform("android")]
|
||||||
|
[SupportedOSPlatform("ios")]
|
||||||
|
internal static partial class Nanosleep
|
||||||
|
{
|
||||||
|
private const long LinuxBaseNanosleepBias = 50000; // 0.05ms
|
||||||
|
|
||||||
|
// Penalty for max allowed sleep duration
|
||||||
|
private const long LinuxNanosleepAccuracyPenaltyThreshold = 200000; // 0.2ms
|
||||||
|
private const long LinuxNanosleepAccuracyPenalty = 30000; // 0.03ms
|
||||||
|
|
||||||
|
// Penalty for base sleep duration
|
||||||
|
private const long LinuxNanosleepBasePenaltyThreshold = 500000; // 0.5ms
|
||||||
|
private const long LinuxNanosleepBasePenalty = 30000; // 0.03ms
|
||||||
|
private const long LinuxNanosleepPenaltyPerMillisecond = 18000; // 0.018ms
|
||||||
|
private const long LinuxNanosleepPenaltyCap = 18000; // 0.018ms
|
||||||
|
|
||||||
|
private const long LinuxStrictBiasOffset = 150_000; // 0.15ms
|
||||||
|
|
||||||
|
// Nanosleep duration is biased depending on the requested timeout on MacOS.
|
||||||
|
// These match the results when measuring on an M1 processor at AboveNormal priority.
|
||||||
|
private const long MacosBaseNanosleepBias = 5000; // 0.005ms
|
||||||
|
private const long MacosBiasPerMillisecond = 140000; // 0.14ms
|
||||||
|
private const long MacosBiasMaxNanoseconds = 20_000_000; // 20ms
|
||||||
|
private const long MacosStrictBiasOffset = 150_000; // 0.15ms
|
||||||
|
|
||||||
|
public static long Bias { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get bias for a given nanosecond timeout.
|
||||||
|
/// Some platforms calculate their bias differently, this method can be used to counteract it.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="timeoutNs">Nanosecond timeout</param>
|
||||||
|
/// <returns>Bias in nanoseconds</returns>
|
||||||
|
public static long GetBias(long timeoutNs)
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS())
|
||||||
|
{
|
||||||
|
long biasNs = Math.Min(timeoutNs, MacosBiasMaxNanoseconds);
|
||||||
|
return MacosBaseNanosleepBias + biasNs * MacosBiasPerMillisecond / 1_000_000;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
long bias = LinuxBaseNanosleepBias;
|
||||||
|
|
||||||
|
if (timeoutNs > LinuxNanosleepBasePenaltyThreshold)
|
||||||
|
{
|
||||||
|
long penalty = (timeoutNs - LinuxNanosleepBasePenaltyThreshold) * LinuxNanosleepPenaltyPerMillisecond / 1_000_000;
|
||||||
|
bias += LinuxNanosleepBasePenalty + Math.Min(LinuxNanosleepPenaltyCap, penalty);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bias;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a stricter bias for a given nanosecond timeout,
|
||||||
|
/// which can improve the chances the sleep completes before the timeout.
|
||||||
|
/// Some platforms calculate their bias differently, this method can be used to counteract it.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="timeoutNs">Nanosecond timeout</param>
|
||||||
|
/// <returns>Strict bias in nanoseconds</returns>
|
||||||
|
public static long GetStrictBias(long timeoutNs)
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS())
|
||||||
|
{
|
||||||
|
return GetBias(timeoutNs) + MacosStrictBiasOffset;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
long bias = GetBias(timeoutNs) + LinuxStrictBiasOffset;
|
||||||
|
|
||||||
|
if (timeoutNs > LinuxNanosleepAccuracyPenaltyThreshold)
|
||||||
|
{
|
||||||
|
bias += LinuxNanosleepAccuracyPenalty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bias;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Nanosleep()
|
||||||
|
{
|
||||||
|
Bias = GetBias(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct Timespec
|
||||||
|
{
|
||||||
|
public long tv_sec; // Seconds
|
||||||
|
public long tv_nsec; // Nanoseconds
|
||||||
|
}
|
||||||
|
|
||||||
|
[LibraryImport("libc", SetLastError = true)]
|
||||||
|
private static partial int nanosleep(ref Timespec req, ref Timespec rem);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a timeout in nanoseconds to a timespec for nanosleep.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="nanoseconds">Timeout in nanoseconds</param>
|
||||||
|
/// <returns>Timespec for nanosleep</returns>
|
||||||
|
private static Timespec GetTimespecFromNanoseconds(ulong nanoseconds)
|
||||||
|
{
|
||||||
|
return new Timespec
|
||||||
|
{
|
||||||
|
tv_sec = (long)(nanoseconds / 1_000_000_000),
|
||||||
|
tv_nsec = (long)(nanoseconds % 1_000_000_000)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sleep for approximately a given time period in nanoseconds.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="nanoseconds">Time to sleep for in nanoseconds</param>
|
||||||
|
public static void Sleep(long nanoseconds)
|
||||||
|
{
|
||||||
|
nanoseconds -= GetBias(nanoseconds);
|
||||||
|
|
||||||
|
if (nanoseconds >= 0)
|
||||||
|
{
|
||||||
|
Timespec req = GetTimespecFromNanoseconds((ulong)nanoseconds);
|
||||||
|
Timespec rem = new();
|
||||||
|
|
||||||
|
nanosleep(ref req, ref rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sleep for at most a given time period in nanoseconds.
|
||||||
|
/// Uses a stricter bias to wake before the requested duration.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Due to OS scheduling behaviour, this timeframe may still be missed.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="nanoseconds">Maximum allowed time for sleep</param>
|
||||||
|
public static void SleepAtMost(long nanoseconds)
|
||||||
|
{
|
||||||
|
// Stricter bias to ensure we wake before the timepoint.
|
||||||
|
nanoseconds -= GetStrictBias(nanoseconds);
|
||||||
|
|
||||||
|
if (nanoseconds >= 0)
|
||||||
|
{
|
||||||
|
Timespec req = GetTimespecFromNanoseconds((ulong)nanoseconds);
|
||||||
|
Timespec rem = new();
|
||||||
|
|
||||||
|
nanosleep(ref req, ref rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
84
src/Ryujinx.Common/PreciseSleep/NanosleepEvent.cs
Normal file
84
src/Ryujinx.Common/PreciseSleep/NanosleepEvent.cs
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
using System;
|
||||||
|
using System.Runtime.Versioning;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace Ryujinx.Common.PreciseSleep
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A precise sleep event for linux and macos that uses nanosleep for more precise timeouts.
|
||||||
|
/// </summary>
|
||||||
|
[SupportedOSPlatform("macos")]
|
||||||
|
[SupportedOSPlatform("linux")]
|
||||||
|
[SupportedOSPlatform("android")]
|
||||||
|
[SupportedOSPlatform("ios")]
|
||||||
|
internal class NanosleepEvent : IPreciseSleepEvent
|
||||||
|
{
|
||||||
|
private readonly AutoResetEvent _waitEvent = new(false);
|
||||||
|
private readonly NanosleepPool _pool;
|
||||||
|
|
||||||
|
public NanosleepEvent()
|
||||||
|
{
|
||||||
|
_pool = new NanosleepPool(_waitEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long AdjustTimePoint(long timePoint, long timeoutNs)
|
||||||
|
{
|
||||||
|
// No adjustment
|
||||||
|
return timePoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SleepUntil(long timePoint)
|
||||||
|
{
|
||||||
|
long now = PerformanceCounter.ElapsedTicks;
|
||||||
|
long delta = (timePoint - now);
|
||||||
|
long ms = Math.Min(delta / PerformanceCounter.TicksPerMillisecond, int.MaxValue);
|
||||||
|
long ns = (delta * 1_000_000) / PerformanceCounter.TicksPerMillisecond;
|
||||||
|
|
||||||
|
if (ms > 0)
|
||||||
|
{
|
||||||
|
_waitEvent.WaitOne((int)ms);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (ns - Nanosleep.Bias > 0)
|
||||||
|
{
|
||||||
|
// Don't bother starting a sleep if there's already a signal active.
|
||||||
|
if (_waitEvent.WaitOne(0))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The 1ms wait will be interrupted by the nanosleep timeout if it completes.
|
||||||
|
if (!_pool.SleepAndSignal(ns, timePoint))
|
||||||
|
{
|
||||||
|
// Too many threads on the pool.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_waitEvent.WaitOne(1);
|
||||||
|
_pool.IgnoreSignal();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Sleep()
|
||||||
|
{
|
||||||
|
_waitEvent.WaitOne();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Signal()
|
||||||
|
{
|
||||||
|
_waitEvent.Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
|
||||||
|
_pool.Dispose();
|
||||||
|
_waitEvent.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
228
src/Ryujinx.Common/PreciseSleep/NanosleepPool.cs
Normal file
228
src/Ryujinx.Common/PreciseSleep/NanosleepPool.cs
Normal file
|
@ -0,0 +1,228 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Runtime.Versioning;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace Ryujinx.Common.PreciseSleep
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A pool of threads used to allow "interruptable" nanosleep for a single target event.
|
||||||
|
/// </summary>
|
||||||
|
[SupportedOSPlatform("macos")]
|
||||||
|
[SupportedOSPlatform("linux")]
|
||||||
|
[SupportedOSPlatform("android")]
|
||||||
|
[SupportedOSPlatform("ios")]
|
||||||
|
internal class NanosleepPool : IDisposable
|
||||||
|
{
|
||||||
|
public const int MaxThreads = 8;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A thread that nanosleeps and may signal an event on wake.
|
||||||
|
/// When a thread is assigned a nanosleep to perform, it also gets a signal ID.
|
||||||
|
/// The pool's target event is only signalled if this ID matches the latest dispatched one.
|
||||||
|
/// </summary>
|
||||||
|
private class NanosleepThread : IDisposable
|
||||||
|
{
|
||||||
|
private static readonly long _timePointEpsilon;
|
||||||
|
|
||||||
|
static NanosleepThread()
|
||||||
|
{
|
||||||
|
_timePointEpsilon = PerformanceCounter.TicksPerMillisecond / 100; // 0.01ms
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly Thread _thread;
|
||||||
|
private readonly NanosleepPool _parent;
|
||||||
|
private readonly AutoResetEvent _newWaitEvent;
|
||||||
|
private bool _running = true;
|
||||||
|
|
||||||
|
private long _signalId;
|
||||||
|
private long _nanoseconds;
|
||||||
|
private long _timePoint;
|
||||||
|
|
||||||
|
public long SignalId => _signalId;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new NanosleepThread for a parent pool, with a specified thread ID.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="parent">Parent NanosleepPool</param>
|
||||||
|
/// <param name="id">Thread ID</param>
|
||||||
|
public NanosleepThread(NanosleepPool parent, int id)
|
||||||
|
{
|
||||||
|
_parent = parent;
|
||||||
|
_newWaitEvent = new(false);
|
||||||
|
|
||||||
|
_thread = new Thread(Loop)
|
||||||
|
{
|
||||||
|
Name = $"Common.Nanosleep.{id}",
|
||||||
|
Priority = ThreadPriority.AboveNormal,
|
||||||
|
IsBackground = true
|
||||||
|
};
|
||||||
|
|
||||||
|
_thread.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service requests to perform a nanosleep, signal parent pool when complete.
|
||||||
|
/// </summary>
|
||||||
|
private void Loop()
|
||||||
|
{
|
||||||
|
_newWaitEvent.WaitOne();
|
||||||
|
|
||||||
|
while (_running)
|
||||||
|
{
|
||||||
|
Nanosleep.Sleep(_nanoseconds);
|
||||||
|
|
||||||
|
_parent.Signal(this);
|
||||||
|
_newWaitEvent.WaitOne();
|
||||||
|
}
|
||||||
|
|
||||||
|
_newWaitEvent.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Assign a nanosleep for this thread to perform, then signal at the end.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="nanoseconds">Nanoseconds to sleep</param>
|
||||||
|
/// <param name="signalId">Signal ID</param>
|
||||||
|
/// <param name="timePoint">Target timepoint</param>
|
||||||
|
public void SleepAndSignal(long nanoseconds, long signalId, long timePoint)
|
||||||
|
{
|
||||||
|
_signalId = signalId;
|
||||||
|
_nanoseconds = nanoseconds;
|
||||||
|
_timePoint = timePoint;
|
||||||
|
_newWaitEvent.Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resurrect an active nanosleep's signal if its target timepoint is a close enough match.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="signalId">New signal id to assign the nanosleep</param>
|
||||||
|
/// <param name="timePoint">Target timepoint</param>
|
||||||
|
/// <returns>True if resurrected, false otherwise</returns>
|
||||||
|
public bool Resurrect(long signalId, long timePoint)
|
||||||
|
{
|
||||||
|
if (Math.Abs(timePoint - _timePoint) < _timePointEpsilon)
|
||||||
|
{
|
||||||
|
_signalId = signalId;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dispose the NanosleepThread, interrupting its worker loop.
|
||||||
|
/// </summary>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_running)
|
||||||
|
{
|
||||||
|
_running = false;
|
||||||
|
_newWaitEvent.Set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly object _lock = new();
|
||||||
|
private readonly List<NanosleepThread> _threads = new();
|
||||||
|
private readonly List<NanosleepThread> _active = new();
|
||||||
|
private readonly Stack<NanosleepThread> _free = new();
|
||||||
|
private readonly AutoResetEvent _signalTarget;
|
||||||
|
|
||||||
|
private long _signalId;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new NanosleepPool with a target event to signal when a nanosleep completes.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="signalTarget">Event to signal when nanosleeps complete</param>
|
||||||
|
public NanosleepPool(AutoResetEvent signalTarget)
|
||||||
|
{
|
||||||
|
_signalTarget = signalTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Signal the target event (if the source sleep has not been superseded)
|
||||||
|
/// and free the nanosleep thread.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="thread">Nanosleep thread that completed</param>
|
||||||
|
private void Signal(NanosleepThread thread)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_active.Remove(thread);
|
||||||
|
_free.Push(thread);
|
||||||
|
|
||||||
|
if (thread.SignalId == _signalId)
|
||||||
|
{
|
||||||
|
_signalTarget.Set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sleep for the given number of nanoseconds and signal the target event.
|
||||||
|
/// This does not block the caller thread.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="nanoseconds">Nanoseconds to sleep</param>
|
||||||
|
/// <param name="timePoint">Target timepoint</param>
|
||||||
|
/// <returns>True if the signal will be set, false otherwise</returns>
|
||||||
|
public bool SleepAndSignal(long nanoseconds, long timePoint)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_signalId++;
|
||||||
|
|
||||||
|
// Check active sleeps, if any line up with the requested timepoint then resurrect that nanosleep.
|
||||||
|
foreach (NanosleepThread existing in _active)
|
||||||
|
{
|
||||||
|
if (existing.Resurrect(_signalId, timePoint))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_free.TryPop(out NanosleepThread thread))
|
||||||
|
{
|
||||||
|
if (_threads.Count >= MaxThreads)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
thread = new NanosleepThread(this, _threads.Count);
|
||||||
|
|
||||||
|
_threads.Add(thread);
|
||||||
|
}
|
||||||
|
|
||||||
|
_active.Add(thread);
|
||||||
|
|
||||||
|
thread.SleepAndSignal(nanoseconds, _signalId, timePoint);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ignore the latest nanosleep.
|
||||||
|
/// </summary>
|
||||||
|
public void IgnoreSignal()
|
||||||
|
{
|
||||||
|
_signalId++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dispose the NanosleepPool, disposing all of its active threads.
|
||||||
|
/// </summary>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
|
||||||
|
foreach (NanosleepThread thread in _threads)
|
||||||
|
{
|
||||||
|
thread.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_threads.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
104
src/Ryujinx.Common/PreciseSleep/PreciseSleepHelper.cs
Normal file
104
src/Ryujinx.Common/PreciseSleep/PreciseSleepHelper.cs
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
using Ryujinx.Common.SystemInterop;
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace Ryujinx.Common.PreciseSleep
|
||||||
|
{
|
||||||
|
public static class PreciseSleepHelper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Create a precise sleep event for the current platform.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A precise sleep event</returns>
|
||||||
|
public static IPreciseSleepEvent CreateEvent()
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() || OperatingSystem.IsIOS() || OperatingSystem.IsAndroid())
|
||||||
|
{
|
||||||
|
return new NanosleepEvent();
|
||||||
|
}
|
||||||
|
else if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
return new WindowsSleepEvent();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new SleepEvent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sleeps up to the closest point to the timepoint that the OS reasonably allows.
|
||||||
|
/// The provided event is used by the timer to wake the current thread, and should not be signalled from any other source.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="evt">Event used to wake this thread</param>
|
||||||
|
/// <param name="timePoint">Target timepoint in host ticks</param>
|
||||||
|
public static void SleepUntilTimePoint(EventWaitHandle evt, long timePoint)
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
WindowsGranularTimer.Instance.SleepUntilTimePointWithoutExternalSignal(evt, timePoint);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Events might oversleep by a little, depending on OS.
|
||||||
|
// We don't want to miss the timepoint, so bias the wait to be lower.
|
||||||
|
// Nanosleep can possibly handle it better, too.
|
||||||
|
long accuracyBias = PerformanceCounter.TicksPerMillisecond / 2;
|
||||||
|
long now = PerformanceCounter.ElapsedTicks + accuracyBias;
|
||||||
|
long ms = Math.Min((timePoint - now) / PerformanceCounter.TicksPerMillisecond, int.MaxValue);
|
||||||
|
|
||||||
|
if (ms > 0)
|
||||||
|
{
|
||||||
|
evt.WaitOne((int)ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() || OperatingSystem.IsIOS() || OperatingSystem.IsAndroid())
|
||||||
|
{
|
||||||
|
// Do a nanosleep.
|
||||||
|
now = PerformanceCounter.ElapsedTicks;
|
||||||
|
long ns = ((timePoint - now) * 1_000_000) / PerformanceCounter.TicksPerMillisecond;
|
||||||
|
|
||||||
|
Nanosleep.SleepAtMost(ns);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Spinwait until the given timepoint. If wakeSignal is or becomes 1, return early.
|
||||||
|
/// Thread is allowed to yield.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="timePoint">Target timepoint in host ticks</param>
|
||||||
|
/// <param name="wakeSignal">Returns early if this is set to 1</param>
|
||||||
|
public static void SpinWaitUntilTimePoint(long timePoint, ref long wakeSignal)
|
||||||
|
{
|
||||||
|
SpinWait spinWait = new();
|
||||||
|
|
||||||
|
while (Interlocked.Read(ref wakeSignal) != 1 && PerformanceCounter.ElapsedTicks < timePoint)
|
||||||
|
{
|
||||||
|
// Our time is close - don't let SpinWait go off and potentially Thread.Sleep().
|
||||||
|
if (spinWait.NextSpinWillYield)
|
||||||
|
{
|
||||||
|
Thread.Yield();
|
||||||
|
|
||||||
|
spinWait.Reset();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
spinWait.SpinOnce();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Spinwait until the given timepoint, with no opportunity to wake early.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="timePoint">Target timepoint in host ticks</param>
|
||||||
|
public static void SpinWaitUntilTimePoint(long timePoint)
|
||||||
|
{
|
||||||
|
while (PerformanceCounter.ElapsedTicks < timePoint)
|
||||||
|
{
|
||||||
|
Thread.SpinWait(5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
51
src/Ryujinx.Common/PreciseSleep/SleepEvent.cs
Normal file
51
src/Ryujinx.Common/PreciseSleep/SleepEvent.cs
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace Ryujinx.Common.PreciseSleep
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A cross-platform precise sleep event that has millisecond granularity.
|
||||||
|
/// </summary>
|
||||||
|
internal class SleepEvent : IPreciseSleepEvent
|
||||||
|
{
|
||||||
|
private readonly AutoResetEvent _waitEvent = new(false);
|
||||||
|
|
||||||
|
public long AdjustTimePoint(long timePoint, long timeoutNs)
|
||||||
|
{
|
||||||
|
// No adjustment
|
||||||
|
return timePoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SleepUntil(long timePoint)
|
||||||
|
{
|
||||||
|
long now = PerformanceCounter.ElapsedTicks;
|
||||||
|
long ms = Math.Min((timePoint - now) / PerformanceCounter.TicksPerMillisecond, int.MaxValue);
|
||||||
|
|
||||||
|
if (ms > 0)
|
||||||
|
{
|
||||||
|
_waitEvent.WaitOne((int)ms);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Sleep()
|
||||||
|
{
|
||||||
|
_waitEvent.WaitOne();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Signal()
|
||||||
|
{
|
||||||
|
_waitEvent.Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
|
||||||
|
_waitEvent.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
220
src/Ryujinx.Common/PreciseSleep/WindowsGranularTimer.cs
Normal file
220
src/Ryujinx.Common/PreciseSleep/WindowsGranularTimer.cs
Normal file
|
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Timer that attempts to align with the hardware timer interrupt,
|
||||||
|
/// and can alert listeners on ticks.
|
||||||
|
/// </summary>
|
||||||
|
[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<WaitingObject> _waitingObjects = new();
|
||||||
|
|
||||||
|
private WindowsGranularTimer()
|
||||||
|
{
|
||||||
|
_timerThread = new Thread(Loop)
|
||||||
|
{
|
||||||
|
IsBackground = true,
|
||||||
|
Name = "Common.WindowsTimer",
|
||||||
|
Priority = ThreadPriority.Highest
|
||||||
|
};
|
||||||
|
|
||||||
|
_timerThread.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Measure and initialize the timer's target granularity.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Main loop for the timer thread. Wakes every clock tick and signals any listeners,
|
||||||
|
/// as well as keeping track of clock alignment.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sleep until a timepoint.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="evt">Reset event to use to be awoken by the clock tick, or an external signal</param>
|
||||||
|
/// <param name="timePoint">Target timepoint</param>
|
||||||
|
/// <returns>True if waited or signalled, false otherwise</returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sleep until a timepoint, but don't expect any external signals.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Saves some effort compared to the sleep that expects to be signalled.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="evt">Reset event to use to be awoken by the clock tick</param>
|
||||||
|
/// <param name="timePoint">Target timepoint</param>
|
||||||
|
/// <returns>True if waited, false otherwise</returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the two nearest clock ticks for a given timepoint.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="timePoint">Target timepoint</param>
|
||||||
|
/// <returns>The nearest clock ticks before and after the given timepoint</returns>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
92
src/Ryujinx.Common/PreciseSleep/WindowsSleepEvent.cs
Normal file
92
src/Ryujinx.Common/PreciseSleep/WindowsSleepEvent.cs
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
using Ryujinx.Common.SystemInterop;
|
||||||
|
using System;
|
||||||
|
using System.Runtime.Versioning;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace Ryujinx.Common.PreciseSleep
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A precise sleep event that uses Windows specific methods to increase clock resolution beyond 1ms,
|
||||||
|
/// use the clock's phase for more precise waits, and potentially align timepoints with it.
|
||||||
|
/// </summary>
|
||||||
|
[SupportedOSPlatform("windows")]
|
||||||
|
internal class WindowsSleepEvent : IPreciseSleepEvent
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The clock can drift a bit, so add this to encourage the clock to still wait if the next tick is forecasted slightly before it.
|
||||||
|
/// </summary>
|
||||||
|
private const long ErrorBias = 50000;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Allowed to be 0.05ms away from the clock granularity to reduce precision.
|
||||||
|
/// </summary>
|
||||||
|
private const long ClockAlignedBias = 50000;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The fraction of clock granularity above the timepoint that will align it down to the lower timepoint.
|
||||||
|
/// Currently set to the lower 1/4, so for 0.5ms granularity: 0.1ms would be rounded down, 0.2 ms would be rounded up.
|
||||||
|
/// </summary>
|
||||||
|
private const long ReverseTimePointFraction = 4;
|
||||||
|
|
||||||
|
private readonly AutoResetEvent _waitEvent = new(false);
|
||||||
|
private readonly WindowsGranularTimer _timer = WindowsGranularTimer.Instance;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set to true to disable timepoint realignment.
|
||||||
|
/// </summary>
|
||||||
|
public bool Precise { get; set; } = false;
|
||||||
|
|
||||||
|
public long AdjustTimePoint(long timePoint, long timeoutNs)
|
||||||
|
{
|
||||||
|
if (Precise || timePoint == long.MaxValue)
|
||||||
|
{
|
||||||
|
return timePoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Does the timeout align with the host clock?
|
||||||
|
|
||||||
|
long granularity = _timer.GranularityNs;
|
||||||
|
long misalignment = timeoutNs % granularity;
|
||||||
|
|
||||||
|
if ((misalignment < ClockAlignedBias || misalignment > granularity - ClockAlignedBias) && timeoutNs > ClockAlignedBias)
|
||||||
|
{
|
||||||
|
// Inaccurate sleep for 0.5ms increments, typically.
|
||||||
|
|
||||||
|
(long low, long high) = _timer.ReturnNearestTicks(timePoint);
|
||||||
|
|
||||||
|
if (timePoint - low < _timer.GranularityTicks / ReverseTimePointFraction)
|
||||||
|
{
|
||||||
|
timePoint = low;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
timePoint = high;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return timePoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SleepUntil(long timePoint)
|
||||||
|
{
|
||||||
|
return _timer.SleepUntilTimePoint(_waitEvent, timePoint + (ErrorBias * PerformanceCounter.TicksPerMillisecond) / 1_000_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Sleep()
|
||||||
|
{
|
||||||
|
_waitEvent.WaitOne();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Signal()
|
||||||
|
{
|
||||||
|
_waitEvent.Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
|
||||||
|
_waitEvent.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
using Ryujinx.Common;
|
using Ryujinx.Common;
|
||||||
|
using Ryujinx.Common.PreciseSleep;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
@ -23,7 +24,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Common
|
||||||
|
|
||||||
private readonly KernelContext _context;
|
private readonly KernelContext _context;
|
||||||
private readonly List<WaitingObject> _waitingObjects;
|
private readonly List<WaitingObject> _waitingObjects;
|
||||||
private AutoResetEvent _waitEvent;
|
private IPreciseSleepEvent _waitEvent;
|
||||||
private bool _keepRunning;
|
private bool _keepRunning;
|
||||||
private long _enforceWakeupFromSpinWait;
|
private long _enforceWakeupFromSpinWait;
|
||||||
|
|
||||||
|
@ -54,6 +55,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Common
|
||||||
timePoint = long.MaxValue;
|
timePoint = long.MaxValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
timePoint = _waitEvent.AdjustTimePoint(timePoint, timeout);
|
||||||
|
|
||||||
lock (_context.CriticalSection.Lock)
|
lock (_context.CriticalSection.Lock)
|
||||||
{
|
{
|
||||||
_waitingObjects.Add(new WaitingObject(schedulerObj, timePoint));
|
_waitingObjects.Add(new WaitingObject(schedulerObj, timePoint));
|
||||||
|
@ -64,7 +67,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Common
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_waitEvent.Set();
|
_waitEvent.Signal();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UnscheduleFutureInvocation(IKFutureSchedulerObject schedulerObj)
|
public void UnscheduleFutureInvocation(IKFutureSchedulerObject schedulerObj)
|
||||||
|
@ -83,10 +86,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Common
|
||||||
|
|
||||||
private void WaitAndCheckScheduledObjects()
|
private void WaitAndCheckScheduledObjects()
|
||||||
{
|
{
|
||||||
SpinWait spinWait = new();
|
|
||||||
WaitingObject next;
|
WaitingObject next;
|
||||||
|
|
||||||
using (_waitEvent = new AutoResetEvent(false))
|
using (_waitEvent = PreciseSleepHelper.CreateEvent())
|
||||||
{
|
{
|
||||||
while (_keepRunning)
|
while (_keepRunning)
|
||||||
{
|
{
|
||||||
|
@ -103,30 +105,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Common
|
||||||
|
|
||||||
if (next.TimePoint > timePoint)
|
if (next.TimePoint > timePoint)
|
||||||
{
|
{
|
||||||
long ms = Math.Min((next.TimePoint - timePoint) / PerformanceCounter.TicksPerMillisecond, int.MaxValue);
|
if (!_waitEvent.SleepUntil(next.TimePoint))
|
||||||
|
|
||||||
if (ms > 0)
|
|
||||||
{
|
{
|
||||||
_waitEvent.WaitOne((int)ms);
|
PreciseSleepHelper.SpinWaitUntilTimePoint(next.TimePoint, ref _enforceWakeupFromSpinWait);
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
while (Interlocked.Read(ref _enforceWakeupFromSpinWait) != 1 && PerformanceCounter.ElapsedTicks < next.TimePoint)
|
|
||||||
{
|
|
||||||
// Our time is close - don't let SpinWait go off and potentially Thread.Sleep().
|
|
||||||
if (spinWait.NextSpinWillYield)
|
|
||||||
{
|
|
||||||
Thread.Yield();
|
|
||||||
|
|
||||||
spinWait.Reset();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
spinWait.SpinOnce();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
spinWait.Reset();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,7 +126,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Common
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_waitEvent.WaitOne();
|
_waitEvent.Sleep();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -212,7 +193,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Common
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_keepRunning = false;
|
_keepRunning = false;
|
||||||
_waitEvent?.Set();
|
_waitEvent?.Signal();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
using Ryujinx.Common.Configuration;
|
using Ryujinx.Common;
|
||||||
|
using Ryujinx.Common.Configuration;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
|
using Ryujinx.Common.PreciseSleep;
|
||||||
using Ryujinx.Graphics.GAL;
|
using Ryujinx.Graphics.GAL;
|
||||||
using Ryujinx.Graphics.Gpu;
|
using Ryujinx.Graphics.Gpu;
|
||||||
using Ryujinx.HLE.HOS.Services.Nv.NvDrvServices.NvMap;
|
using Ryujinx.HLE.HOS.Services.Nv.NvDrvServices.NvMap;
|
||||||
|
@ -23,9 +25,7 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger
|
||||||
|
|
||||||
private readonly Thread _composerThread;
|
private readonly Thread _composerThread;
|
||||||
|
|
||||||
private readonly Stopwatch _chrono;
|
private readonly AutoResetEvent _event = new(false);
|
||||||
|
|
||||||
private readonly ManualResetEvent _event = new(false);
|
|
||||||
private readonly AutoResetEvent _nextFrameEvent = new(true);
|
private readonly AutoResetEvent _nextFrameEvent = new(true);
|
||||||
private long _ticks;
|
private long _ticks;
|
||||||
private long _ticksPerFrame;
|
private long _ticksPerFrame;
|
||||||
|
@ -64,11 +64,9 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger
|
||||||
_composerThread = new Thread(HandleComposition)
|
_composerThread = new Thread(HandleComposition)
|
||||||
{
|
{
|
||||||
Name = "SurfaceFlinger.Composer",
|
Name = "SurfaceFlinger.Composer",
|
||||||
|
Priority = ThreadPriority.AboveNormal
|
||||||
};
|
};
|
||||||
|
|
||||||
_chrono = new Stopwatch();
|
|
||||||
_chrono.Start();
|
|
||||||
|
|
||||||
_ticks = 0;
|
_ticks = 0;
|
||||||
_spinTicks = Stopwatch.Frequency / 500;
|
_spinTicks = Stopwatch.Frequency / 500;
|
||||||
_1msTicks = Stopwatch.Frequency / 1000;
|
_1msTicks = Stopwatch.Frequency / 1000;
|
||||||
|
@ -299,11 +297,11 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger
|
||||||
{
|
{
|
||||||
_isRunning = true;
|
_isRunning = true;
|
||||||
|
|
||||||
long lastTicks = _chrono.ElapsedTicks;
|
long lastTicks = PerformanceCounter.ElapsedTicks;
|
||||||
|
|
||||||
while (_isRunning)
|
while (_isRunning)
|
||||||
{
|
{
|
||||||
long ticks = _chrono.ElapsedTicks;
|
long ticks = PerformanceCounter.ElapsedTicks;
|
||||||
|
|
||||||
if (_swapInterval == 0)
|
if (_swapInterval == 0)
|
||||||
{
|
{
|
||||||
|
@ -336,21 +334,16 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sleep if possible. If the time til the next frame is too low, spin wait instead.
|
// Sleep if possible. If the time til the next frame is too low, spin wait instead.
|
||||||
long diff = _ticksPerFrame - (_ticks + _chrono.ElapsedTicks - ticks);
|
long diff = _ticksPerFrame - (_ticks + PerformanceCounter.ElapsedTicks - ticks);
|
||||||
if (diff > 0)
|
if (diff > 0)
|
||||||
{
|
{
|
||||||
|
PreciseSleepHelper.SleepUntilTimePoint(_event, PerformanceCounter.ElapsedTicks + diff);
|
||||||
|
|
||||||
|
diff = _ticksPerFrame - (_ticks + PerformanceCounter.ElapsedTicks - ticks);
|
||||||
|
|
||||||
if (diff < _spinTicks)
|
if (diff < _spinTicks)
|
||||||
{
|
{
|
||||||
do
|
PreciseSleepHelper.SpinWaitUntilTimePoint(PerformanceCounter.ElapsedTicks + diff);
|
||||||
{
|
|
||||||
// SpinWait is a little more HT/SMT friendly than aggressively updating/checking ticks.
|
|
||||||
// The value of 5 still gives us quite a bit of precision (~0.0003ms variance at worst) while waiting a reasonable amount of time.
|
|
||||||
Thread.SpinWait(5);
|
|
||||||
|
|
||||||
ticks = _chrono.ElapsedTicks;
|
|
||||||
_ticks += ticks - lastTicks;
|
|
||||||
lastTicks = ticks;
|
|
||||||
} while (_ticks < _ticksPerFrame);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
Reference in a new issue