Use nanosleep for non-Windows platforms

This commit is contained in:
hwsmm
2024-10-18 21:49:30 +09:00
parent d56a920df9
commit e97e54f80f
4 changed files with 170 additions and 41 deletions

View File

@@ -0,0 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
namespace osu.Framework.Platform
{
public interface INativeSleep : IDisposable
{
bool Sleep(TimeSpan duration);
}
}

View File

@@ -0,0 +1,92 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Runtime.InteropServices;
namespace osu.Framework.Platform.Linux.Native
{
internal class UnixNativeSleep : INativeSleep
{
[StructLayout(LayoutKind.Sequential)]
public struct TimeSpec
{
public long Seconds;
public long NanoSeconds;
}
private delegate int NanoSleepDelegate(in TimeSpec duration, out TimeSpec rem);
private static readonly NanoSleepDelegate? nanosleep;
// Android and some platforms don't have version in lib name.
[DllImport("c", EntryPoint = "nanosleep", CallingConvention = CallingConvention.Cdecl, SetLastError = true)]
private static extern int nanosleep_c(in TimeSpec duration, out TimeSpec rem);
[DllImport("libc.so.6", EntryPoint = "nanosleep", CallingConvention = CallingConvention.Cdecl, SetLastError = true)]
private static extern int nanosleep_libc6(in TimeSpec duration, out TimeSpec rem);
static UnixNativeSleep()
{
TimeSpec test = new TimeSpec
{
Seconds = 0,
NanoSeconds = 1,
};
try
{
nanosleep_c(in test, out _);
nanosleep = nanosleep_c;
}
catch
{
}
if (nanosleep == null)
{
try
{
nanosleep_libc6(in test, out _);
nanosleep = nanosleep_libc6;
}
catch
{
}
}
// if nanosleep is null at this point, Thread.Sleep should be used.
}
private const int interrupt_error = 4;
public bool Sleep(TimeSpan duration)
{
if (nanosleep == null)
return false;
const int ns_per_second = 1000 * 1000 * 1000;
long ns = (long)duration.TotalNanoseconds;
TimeSpec timeSpec = new TimeSpec
{
Seconds = ns / ns_per_second,
NanoSeconds = ns % ns_per_second,
};
while (nanosleep(in timeSpec, out var remaining) == -1 && Marshal.GetLastPInvokeError() == interrupt_error)
{
// The pause can be interrupted by a signal that was delivered to the thread.
// Sleep again with remaining time if it happened.
timeSpec = remaining;
}
return true;
}
public void Dispose()
{
}
}
}

View File

@@ -0,0 +1,57 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
namespace osu.Framework.Platform.Windows.Native
{
internal class WindowsNativeSleep : INativeSleep
{
private IntPtr waitableTimer;
public WindowsNativeSleep()
{
createWaitableTimer();
}
private void createWaitableTimer()
{
try
{
// Attempt to use CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, only available since Windows 10, version 1803.
waitableTimer = Execution.CreateWaitableTimerEx(IntPtr.Zero, null,
Execution.CreateWaitableTimerFlags.CREATE_WAITABLE_TIMER_MANUAL_RESET | Execution.CreateWaitableTimerFlags.CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, Execution.TIMER_ALL_ACCESS);
if (waitableTimer == IntPtr.Zero)
{
// Fall back to a more supported version. This is still far more accurate than Thread.Sleep.
waitableTimer = Execution.CreateWaitableTimerEx(IntPtr.Zero, null, Execution.CreateWaitableTimerFlags.CREATE_WAITABLE_TIMER_MANUAL_RESET, Execution.TIMER_ALL_ACCESS);
}
}
catch
{
// Any kind of unexpected exception should fall back to Thread.Sleep.
}
}
public bool Sleep(TimeSpan duration)
{
if (waitableTimer == IntPtr.Zero) return false;
// Not sure if we want to fall back to Thread.Sleep on failure here, needs further investigation.
if (Execution.SetWaitableTimerEx(waitableTimer, Execution.CreateFileTime(duration), 0, null, default, IntPtr.Zero, 0))
{
Execution.WaitForSingleObject(waitableTimer, Execution.INFINITE);
return true;
}
return false;
}
public void Dispose()
{
if (waitableTimer != IntPtr.Zero)
Execution.CloseHandle(waitableTimer);
}
}
}

View File

@@ -3,8 +3,9 @@
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading;
using osu.Framework.Platform;
using osu.Framework.Platform.Linux.Native;
using osu.Framework.Platform.Windows.Native;
namespace osu.Framework.Timing
@@ -32,11 +33,14 @@ namespace osu.Framework.Timing
/// </summary>
public double TimeSlept { get; private set; }
private IntPtr waitableTimer;
private readonly INativeSleep nativeSleep;
internal ThrottledFrameClock()
{
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows) createWaitableTimer();
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
nativeSleep = new WindowsNativeSleep();
else
nativeSleep = new UnixNativeSleep();
}
public override void ProcessFrame()
@@ -91,7 +95,7 @@ namespace osu.Framework.Timing
TimeSpan timeSpan = TimeSpan.FromMilliseconds(milliseconds);
if (!waitWaitableTimer(timeSpan))
if (!nativeSleep.Sleep(timeSpan))
Thread.Sleep(timeSpan);
return (CurrentTime = SourceTime) - before;
@@ -99,43 +103,7 @@ namespace osu.Framework.Timing
public void Dispose()
{
if (waitableTimer != IntPtr.Zero)
Execution.CloseHandle(waitableTimer);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool waitWaitableTimer(TimeSpan timeSpan)
{
if (waitableTimer == IntPtr.Zero) return false;
// Not sure if we want to fall back to Thread.Sleep on failure here, needs further investigation.
if (Execution.SetWaitableTimerEx(waitableTimer, Execution.CreateFileTime(timeSpan), 0, null, default, IntPtr.Zero, 0))
{
Execution.WaitForSingleObject(waitableTimer, Execution.INFINITE);
return true;
}
return false;
}
private void createWaitableTimer()
{
try
{
// Attempt to use CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, only available since Windows 10, version 1803.
waitableTimer = Execution.CreateWaitableTimerEx(IntPtr.Zero, null,
Execution.CreateWaitableTimerFlags.CREATE_WAITABLE_TIMER_MANUAL_RESET | Execution.CreateWaitableTimerFlags.CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, Execution.TIMER_ALL_ACCESS);
if (waitableTimer == IntPtr.Zero)
{
// Fall back to a more supported version. This is still far more accurate than Thread.Sleep.
waitableTimer = Execution.CreateWaitableTimerEx(IntPtr.Zero, null, Execution.CreateWaitableTimerFlags.CREATE_WAITABLE_TIMER_MANUAL_RESET, Execution.TIMER_ALL_ACCESS);
}
}
catch
{
// Any kind of unexpected exception should fall back to Thread.Sleep.
}
nativeSleep.Dispose();
}
}
}