aboutsummaryrefslogtreecommitdiff
path: root/src/Ryujinx.Common/PreciseSleep/WindowsGranularTimer.cs
blob: 3bf092704b38da345882a7a595050ba5d9d515ce (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
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);
        }
    }
}