using System; using System.Runtime.InteropServices; using System.Runtime.Versioning; namespace Ryujinx.Common.PreciseSleep { /// /// Access to Linux/MacOS nanosleep, with platform specific bias to improve precision. /// [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; } /// /// Get bias for a given nanosecond timeout. /// Some platforms calculate their bias differently, this method can be used to counteract it. /// /// Nanosecond timeout /// Bias in nanoseconds 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; } } /// /// 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. /// /// Nanosecond timeout /// Strict bias in nanoseconds 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); /// /// Convert a timeout in nanoseconds to a timespec for nanosleep. /// /// Timeout in nanoseconds /// Timespec for nanosleep 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) }; } /// /// Sleep for approximately a given time period in nanoseconds. /// /// Time to sleep for in nanoseconds 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); } } /// /// Sleep for at most a given time period in nanoseconds. /// Uses a stricter bias to wake before the requested duration. /// /// /// Due to OS scheduling behaviour, this timeframe may still be missed. /// /// Maximum allowed time for sleep 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); } } } }