aboutsummaryrefslogtreecommitdiff
path: root/src/Ryujinx.Common/PreciseSleep/Nanosleep.cs
blob: 67f067ae2c434c1404f1201e7e0a0e5cdf351c52 (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
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);
            }
        }
    }
}