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