mirror of
https://git.ryujinx.app/ryubing/ryujinx.git
synced 2025-08-08 03:08:28 +02:00
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:
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();
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user