Skip to content

Commit

Permalink
Start work on TimeWindow type
Browse files Browse the repository at this point in the history
  • Loading branch information
Perksey committed Aug 18, 2023
1 parent 8b21c93 commit 1a7a5c7
Show file tree
Hide file tree
Showing 3 changed files with 213 additions and 34 deletions.
73 changes: 46 additions & 27 deletions src/Core/Silk.NET.Core/Miscellaneous/BreakneckSleep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#if NET7_0_OR_GREATER
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Runtime.Intrinsics.Arm;
Expand Down Expand Up @@ -39,6 +40,22 @@ public enum AccuracyMode
BusyLoopOnly
}

[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static double TrustFactor(AccuracyMode mode) => mode switch
{
AccuracyMode.HighestResolution => 0.9,
_ => 0.75
};

[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static bool ShouldTrust(double fps, AccuracyMode mode) => (fps, mode) switch
{
(>= 200, AccuracyMode.HighestResolution) => false,
(>= 100, AccuracyMode.HighResolutionWithBusyLoop) => false,
(>= 50, _) => false,
_ => true
};

/// <summary>
/// The underlying handle of the timer, if any.
/// </summary>
Expand Down Expand Up @@ -74,41 +91,41 @@ public BreakneckSleep()
/// <summary>
/// Sleeps for <paramref cref="duration"/>.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Sleep(TimeSpan duration)
{
var start = Stopwatch.GetTimestamp();
var emergencySpin = false;
if (OperatingSystem.IsWindows())
if (ShouldTrust(1 / duration.TotalSeconds, Accuracy))
{
emergencySpin = !WindowsWait(duration);
}
if (OperatingSystem.IsWindows())
{
WindowsWait(duration);
}

if (OperatingSystem.IsLinux())
{
LinuxWait(duration);
}
if (OperatingSystem.IsLinux())
{
LinuxWait(duration);
}

if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS() || OperatingSystem.IsTvOS() ||
OperatingSystem.IsWatchOS() || OperatingSystem.IsMacCatalyst())
{
AppleWait(duration);
if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS() || OperatingSystem.IsTvOS() ||
OperatingSystem.IsWatchOS() || OperatingSystem.IsMacCatalyst())
{
AppleWait(duration);
}
}

if (Accuracy is AccuracyMode.HighResolutionWithBusyLoop or AccuracyMode.BusyLoopOnly || emergencySpin)
do
{
do
if (X86Base.IsSupported)
{
if (X86Base.IsSupported)
{
X86Base.Pause();
}

if (ArmBase.IsSupported)
{
ArmBase.Yield();
}
} while (Stopwatch.GetElapsedTime(start, Stopwatch.GetTimestamp()) < duration);
}
X86Base.Pause();
}

if (ArmBase.IsSupported)
{
ArmBase.Yield();
}
} while (Stopwatch.GetElapsedTime(start, Stopwatch.GetTimestamp()) < duration);
}

private static unsafe (nint Handle, bool IsHighResolution) WindowsCreate()
Expand Down Expand Up @@ -160,7 +177,7 @@ static FILETIME CreateFileTime(TimeSpan ts)
(
Accuracy == AccuracyMode.HighestResolution
? duration
: TimeSpan.FromMicroseconds(duration.TotalMicroseconds * 0.9)
: TimeSpan.FromMicroseconds(duration.TotalMicroseconds * TrustFactor(Accuracy))
);
if (SetWaitableTimerEx(Handle, &ft, 0, null, null, null, 0) == 1)
{
Expand All @@ -173,6 +190,8 @@ static FILETIME CreateFileTime(TimeSpan ts)

private unsafe void LinuxWait(TimeSpan duration)
{
duration *= TrustFactor(AccuracyMode.HighResolutionWithBusyLoop);

[DllImport("libc", EntryPoint = "nanosleep")]
static extern int Nanosleep(Timespec* req, Timespec* rem);

Expand All @@ -188,7 +207,7 @@ private void AppleWait(TimeSpan duration)
{
[DllImport("libc", EntryPoint = "usleep")]
static extern int Usleep(uint micros);
_ = Usleep((uint) (duration.TotalMicroseconds * 0.9));
_ = Usleep((uint) (duration.TotalMicroseconds * TrustFactor(AccuracyMode.HighResolutionWithBusyLoop)));
}

public void Dispose()
Expand Down
129 changes: 129 additions & 0 deletions src/Core/Silk.NET.Core/Miscellaneous/TimeWindow.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;

namespace Silk.NET.Core;

/// <summary>
/// Represents a window/interval of time in which the caller can execute.
/// </summary>
/// <example>
/// To create a 60 FPS loop:
/// <code>
/// var window = new TimeWindow();
/// window.Window = TimeSpan.FromSeconds(1.0 / 60.0);
/// while (true)
/// {
/// if (window.BeginWindow())
/// {
/// // It's time for the next piece of work (e.g. rendering the next frame)
/// window.EndWindow(); // don't forget this!
/// }
/// // Anything that isn't necessarily dependent on a time window goes here.
/// // In most cases, BeginWindow will sleep and always return true. This is not guaranteed, however, so anything
/// // "real-time" (e.g. event processing) should be done here.
/// }
/// </code>
/// </example>
public class TimeWindow
{
#if NET7_0_OR_GREATER
private readonly BreakneckSleep _sleep = new();
#endif
private readonly Stopwatch _sw = Stopwatch.StartNew();
/// <summary>
/// Creates a <see cref="TimeWindow"/>.
/// </summary>
/// <param name="sleeping">
/// Whether <see cref="BeginWindow"/> should sleep until the next window is due to start.
/// </param>
// ReSharper disable once UnusedParameter.Local
public TimeWindow(bool sleeping = true)
{
#if NET7_0_OR_GREATER
Sleeping = sleeping;
#endif
}

/// <summary>
/// The duration of a single window. For example, if you wanted to render 60 frames per second, a single frame shall
/// be thought of as being rendered in a <c>1.0 / 60.0</c> second window.
/// </summary>
public TimeSpan Window { get; set; } = TimeSpan.FromSeconds(1.0 / 60.0);

/// <summary>
/// The offset from the point at which this <see cref="TimeWindow"/> was constructed (or the last time
/// <see cref="Reset"/> was called) at which the next window is due to begin.
/// </summary>
public TimeSpan NextWindowStart { get; private set; } = TimeSpan.Zero;

/// <summary>
/// The offset from the point at which this <see cref="TimeWindow"/> was constructed (or the last time
/// <see cref="Reset"/> was called) at which the next window is due to begin.
/// </summary>
public TimeSpan NextWindowEnd
{
[MethodImpl(MethodImplOptions.AggressiveInlining | (MethodImplOptions)768)]
get => NextWindowStart + Window;
}

/// <summary>
/// The offset from the point at which this <see cref="TimeWindow"/> was constructed (or the last time
/// <see cref="Reset"/> was called).
/// </summary>
public TimeSpan CurrentTime
{
[MethodImpl(MethodImplOptions.AggressiveInlining | (MethodImplOptions)768)]
get => _sw.Elapsed;
}

#if NET7_0_OR_GREATER
/// <summary>
/// Whether <see cref="BeginWindow"/> should use <see cref="BreakneckSleep"/>.
/// </summary>
private bool Sleeping { get; }
#endif

/// <summary>
/// Resets the <see cref="NextWindowStart"/> to zero and the offset from which <see cref="CurrentTime"/> is
/// measured.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining | (MethodImplOptions)768)]
public void Reset()
{
_sw.Restart();
NextWindowStart = TimeSpan.Zero;
}

/// <summary>
/// Determines whether the next window is due.
/// </summary>
/// <returns>True if the next window is due or overdue, false otherwise.</returns>
/// <remarks>
/// If false, do any time-independent, real-time work and call this method again.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining | (MethodImplOptions)768)]
public bool BeginWindow()
{
#if NET7_0_OR_GREATER
var ct = CurrentTime;
if (Sleeping && ct < NextWindowStart)
{
_sleep.Sleep(NextWindowStart - ct);
}
#endif
return CurrentTime >= NextWindowStart;
}

/// <summary>
/// Completes this time window.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining | (MethodImplOptions)768)]
public void EndWindow()
{
NextWindowStart += Window;
}
}
45 changes: 38 additions & 7 deletions src/Lab/Experiments/TimeTrials/Program.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,47 @@
// See https://aka.ms/new-console-template for more information

using System.Collections.Concurrent;
using System.Diagnostics;
using Silk.NET.Core;

for (var fps = 60; fps < 61440; fps *= 2)
var timeWindow = new TimeWindow();
var bag = new ConcurrentBag<double>();
var sw = new Stopwatch();
Console.Write("Enter frames per second: ");
timeWindow.Window = TimeSpan.FromSeconds(1 / double.Parse(Console.ReadLine()!));
Console.WriteLine("Press Ctrl+C to measure statistics.");
Console.CancelKeyPress += (_, _) =>
{
var sleep = new BreakneckSleep();
for (var t = 0; t < 5; t++)
var overshoots = bag.ToList();
var count = overshoots.Count;
sw.Stop();
Console.WriteLine($"Run time: {sw.Elapsed.TotalSeconds}");
Console.WriteLine($"Frames: {overshoots.Count}");
Console.WriteLine($"Expected Frames: {sw.Elapsed.TotalSeconds / timeWindow.Window.TotalSeconds}");
Console.WriteLine($"Overshoots: {overshoots.Count(x => x > 0)}");
Console.WriteLine($"Undershoots: {overshoots.Count(x => x < 0)}");
Console.WriteLine($"Max overshoot: {overshoots.Max()}");
Console.WriteLine($"Max undershoot: {overshoots.Min()}");
Console.WriteLine($"Mean overshoot/undershoot: {overshoots.Sum() / overshoots.Count}");
Console.WriteLine($"Median overshoot/undershoot: {overshoots.Order().ElementAt(overshoots.Count / 2)}");
Console.WriteLine($"Overshoot/undershoot range: {overshoots.Max() - overshoots.Min()}");
overshoots.RemoveAll(x => x < 0);
Console.WriteLine($"Min overshoot/wasted time per frame: {overshoots.Min()}");
Console.WriteLine($"Mean wasted time per frame: {overshoots.Sum() / overshoots.Count}");
Console.WriteLine($"Median wasted time per frame: {overshoots.Order().ElementAt(overshoots.Count / 2)}");
Console.WriteLine($"Range for wasted time per frame: {overshoots.Max() - overshoots.Min()}");
Console.WriteLine($"Final FPS: {1 / (sw.Elapsed.TotalSeconds / count)}");
};
sw.Start();
while (true)
{
if (timeWindow.BeginWindow())
{
bag.Add((timeWindow.CurrentTime - timeWindow.NextWindowStart).TotalSeconds);
timeWindow.EndWindow();
}
else
{
var now = Stopwatch.GetTimestamp();
sleep.Sleep(TimeSpan.FromSeconds(1 / (double)fps));
Console.WriteLine
($"{fps} FPS = {1 / Stopwatch.GetElapsedTime(now, Stopwatch.GetTimestamp()).TotalSeconds} FPS");
bag.Add(-(timeWindow.NextWindowStart - timeWindow.CurrentTime).TotalSeconds);
}
}

0 comments on commit 1a7a5c7

Please sign in to comment.