diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json
index 0c84b35cc7..5b6acdb13d 100644
--- a/.nuke/build.schema.json
+++ b/.nuke/build.schema.json
@@ -142,7 +142,6 @@
"items": {
"type": "string",
"enum": [
- "Angle",
"Assimp",
"BuildLibSilkDroid",
"Clean",
@@ -160,9 +159,11 @@
"RegenerateBindings",
"Restore",
"SDL2",
+ "Shaderc",
"ShipApi",
"SignPackages",
"Sln",
+ "SPIRVCross",
"SPIRVReflect",
"SwiftShader",
"Test",
@@ -183,7 +184,6 @@
"items": {
"type": "string",
"enum": [
- "Angle",
"Assimp",
"BuildLibSilkDroid",
"Clean",
@@ -201,9 +201,11 @@
"RegenerateBindings",
"Restore",
"SDL2",
+ "Shaderc",
"ShipApi",
"SignPackages",
"Sln",
+ "SPIRVCross",
"SPIRVReflect",
"SwiftShader",
"Test",
@@ -231,4 +233,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/Silk.NET.sln b/Silk.NET.sln
index 2afc3f6ef2..7ded3d5261 100644
--- a/Silk.NET.sln
+++ b/Silk.NET.sln
@@ -594,6 +594,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Silk.NET.Shaderc", "src\SPI
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Silk.NET.Shaderc.Native", "src\Native\Silk.NET.Shaderc.Native\Silk.NET.Shaderc.Native.csproj", "{D1E4EDC7-0A06-498A-B0F9-275B7D508A0E}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TimeTrials", "src\Lab\Experiments\TimeTrials\TimeTrials.csproj", "{021EE492-FB0E-46CF-AC07-768D5A999798}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -3615,6 +3617,18 @@ Global
{D1E4EDC7-0A06-498A-B0F9-275B7D508A0E}.Release|x64.Build.0 = Release|Any CPU
{D1E4EDC7-0A06-498A-B0F9-275B7D508A0E}.Release|x86.ActiveCfg = Release|Any CPU
{D1E4EDC7-0A06-498A-B0F9-275B7D508A0E}.Release|x86.Build.0 = Release|Any CPU
+ {021EE492-FB0E-46CF-AC07-768D5A999798}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {021EE492-FB0E-46CF-AC07-768D5A999798}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {021EE492-FB0E-46CF-AC07-768D5A999798}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {021EE492-FB0E-46CF-AC07-768D5A999798}.Debug|x64.Build.0 = Debug|Any CPU
+ {021EE492-FB0E-46CF-AC07-768D5A999798}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {021EE492-FB0E-46CF-AC07-768D5A999798}.Debug|x86.Build.0 = Debug|Any CPU
+ {021EE492-FB0E-46CF-AC07-768D5A999798}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {021EE492-FB0E-46CF-AC07-768D5A999798}.Release|Any CPU.Build.0 = Release|Any CPU
+ {021EE492-FB0E-46CF-AC07-768D5A999798}.Release|x64.ActiveCfg = Release|Any CPU
+ {021EE492-FB0E-46CF-AC07-768D5A999798}.Release|x64.Build.0 = Release|Any CPU
+ {021EE492-FB0E-46CF-AC07-768D5A999798}.Release|x86.ActiveCfg = Release|Any CPU
+ {021EE492-FB0E-46CF-AC07-768D5A999798}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -3904,6 +3918,7 @@ Global
{1E7C6166-58B2-46B3-A9BA-18099BD83AF0} = {20A4A2D1-D699-4D71-AA97-950154638576}
{E77BE8DB-3C74-42EB-9B65-67EAAA9AD7DB} = {15FC3D1A-25D7-446B-87A7-B45BA3C2225F}
{D1E4EDC7-0A06-498A-B0F9-275B7D508A0E} = {72E7FA64-5B1E-477D-BD30-63B7F206B3C4}
+ {021EE492-FB0E-46CF-AC07-768D5A999798} = {39B598E9-44BA-4A61-A1BB-7C543734DBA6}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F5273D7F-3334-48DF-94E3-41AE6816CD4D}
diff --git a/src/Core/Silk.NET.Core/Miscellaneous/BreakneckSleep.cs b/src/Core/Silk.NET.Core/Miscellaneous/BreakneckSleep.cs
new file mode 100644
index 0000000000..3c743f1fce
--- /dev/null
+++ b/src/Core/Silk.NET.Core/Miscellaneous/BreakneckSleep.cs
@@ -0,0 +1,221 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#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;
+using System.Runtime.Intrinsics.X86;
+using Silk.NET.Core.Native;
+
+namespace Silk.NET.Core;
+
+///
+/// Implementation of a high-resolution clock intended for short-duration sleeps.
+///
+public readonly struct BreakneckSleep : IDisposable
+{
+ ///
+ /// Determines how accurate a implementation is.
+ ///
+ public enum AccuracyMode
+ {
+ ///
+ /// The underlying implementation can be trusted to be fully accurate.
+ ///
+ HighestResolution,
+
+ ///
+ /// The underlying implementation is mostly accurate, but uses a busy loop for the 10% to account for potential
+ /// inaccuracies.
+ ///
+ HighResolutionWithBusyLoop,
+
+ ///
+ /// There is no high-resolution implementation available thus a busy loop is used.
+ ///
+ 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
+ };
+
+ ///
+ /// The underlying handle of the timer, if any.
+ ///
+ public nint Handle { get; }
+
+ ///
+ /// The accuracy of the underlying implementation.
+ ///
+ public AccuracyMode Accuracy { get; }
+
+ ///
+ /// Creates a high-resolution timer
+ ///
+ public BreakneckSleep()
+ {
+ Accuracy = AccuracyMode.BusyLoopOnly;
+ if (OperatingSystem.IsWindows())
+ {
+ var (handle, isHigh) = WindowsCreate();
+ Handle = handle;
+ Accuracy = isHigh ? AccuracyMode.HighestResolution : AccuracyMode.HighResolutionWithBusyLoop;
+ return;
+ }
+
+ Handle = 0;
+ if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() || OperatingSystem.IsIOS() ||
+ OperatingSystem.IsTvOS() || OperatingSystem.IsWatchOS() || OperatingSystem.IsMacCatalyst())
+ {
+ Accuracy = AccuracyMode.HighResolutionWithBusyLoop;
+ }
+ }
+
+ ///
+ /// Sleeps for .
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Sleep(TimeSpan duration)
+ {
+ var start = Stopwatch.GetTimestamp();
+ if (ShouldTrust(1 / duration.TotalSeconds, Accuracy))
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ WindowsWait(duration);
+ }
+
+ if (OperatingSystem.IsLinux())
+ {
+ LinuxWait(duration);
+ }
+
+ if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS() || OperatingSystem.IsTvOS() ||
+ OperatingSystem.IsWatchOS() || OperatingSystem.IsMacCatalyst())
+ {
+ AppleWait(duration);
+ }
+ }
+
+ do
+ {
+ if (X86Base.IsSupported)
+ {
+ X86Base.Pause();
+ }
+
+ if (ArmBase.IsSupported)
+ {
+ ArmBase.Yield();
+ }
+ } while (Stopwatch.GetElapsedTime(start, Stopwatch.GetTimestamp()) < duration);
+ }
+
+ private static unsafe (nint Handle, bool IsHighResolution) WindowsCreate()
+ {
+ const uint createWaitableTimerManualReset = 0x00000001;
+ const uint createWaitableTimerHighResolution = 0x00000002;
+ const uint timerAllAccess = 0x1F0003;
+ [DllImport("kernel32.dll", ExactSpelling = true)]
+ static extern nint CreateWaitableTimerExW
+ (
+ SecurityAttributes* lpTimerAttributes,
+ char* lpTimerName,
+ uint dwFlags,
+ uint dwDesiredAccess
+ );
+
+ var ret = CreateWaitableTimerExW
+ (null, null, createWaitableTimerManualReset | createWaitableTimerHighResolution, timerAllAccess);
+ return ret != 0
+ ? (ret, true)
+ : (CreateWaitableTimerExW(null, null, createWaitableTimerManualReset, timerAllAccess), false);
+ }
+
+ private unsafe bool WindowsWait(TimeSpan duration)
+ {
+ [DllImport("kernel32.dll", ExactSpelling = true)]
+ static extern uint SetWaitableTimerEx
+ (
+ nint hTimer,
+ FILETIME* lpDueTime,
+ long lPeriod,
+ void* pfnCompletionRoutine,
+ void* lpArgToCompletionRoutine,
+ void* wakeContext,
+ ulong tolerableDelay
+ );
+
+ static FILETIME CreateFileTime(TimeSpan ts)
+ {
+ var ul = unchecked((ulong) -ts.Ticks);
+ return new FILETIME
+ {
+ dwHighDateTime = (int) (ul >> 32),
+ dwLowDateTime = (int) (ul & 0xFFFFFFFF)
+ };
+ }
+
+ var ft = CreateFileTime
+ (
+ Accuracy == AccuracyMode.HighestResolution
+ ? duration
+ : TimeSpan.FromMicroseconds(duration.TotalMicroseconds * TrustFactor(Accuracy))
+ );
+ if (SetWaitableTimerEx(Handle, &ft, 0, null, null, null, 0) == 1)
+ {
+ SilkMarshal.WaitWindowsObjects(Handle);
+ return true;
+ }
+
+ return false;
+ }
+
+ private unsafe void LinuxWait(TimeSpan duration)
+ {
+ duration *= TrustFactor(AccuracyMode.HighResolutionWithBusyLoop);
+
+ [DllImport("libc", EntryPoint = "nanosleep")]
+ static extern int Nanosleep(Timespec* req, Timespec* rem);
+
+ var ts = new Timespec
+ {
+ Seconds = duration.Seconds,
+ Nanoseconds = duration.Nanoseconds - duration.Seconds * 1000000000
+ };
+ _ = Nanosleep(&ts, null);
+ }
+
+ private void AppleWait(TimeSpan duration)
+ {
+ [DllImport("libc", EntryPoint = "usleep")]
+ static extern int Usleep(uint micros);
+ _ = Usleep((uint) (duration.TotalMicroseconds * TrustFactor(AccuracyMode.HighResolutionWithBusyLoop)));
+ }
+
+ public void Dispose()
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ SilkMarshal.ThrowHResult(SilkMarshal.CloseWindowsHandle(Handle));
+ }
+ }
+}
+#endif
diff --git a/src/Core/Silk.NET.Core/Miscellaneous/TimeWindow.cs b/src/Core/Silk.NET.Core/Miscellaneous/TimeWindow.cs
new file mode 100644
index 0000000000..1ad4056baf
--- /dev/null
+++ b/src/Core/Silk.NET.Core/Miscellaneous/TimeWindow.cs
@@ -0,0 +1,131 @@
+// 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;
+
+///
+/// Represents a window/interval of time in which the caller can execute.
+///
+///
+/// To create a 60 FPS loop:
+///
+/// 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.
+/// }
+///
+///
+public class TimeWindow
+{
+#if NET7_0_OR_GREATER
+ private readonly BreakneckSleep _sleep = new();
+#endif
+ private readonly Stopwatch _sw = Stopwatch.StartNew();
+ ///
+ /// Creates a .
+ ///
+ ///
+ /// Whether should sleep until the next window is due to start.
+ ///
+ // ReSharper disable once UnusedParameter.Local
+ public TimeWindow(bool sleeping = true)
+ {
+#if NET7_0_OR_GREATER
+ Sleeping = sleeping;
+#endif
+ }
+
+ ///
+ /// 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 1.0 / 60.0 second window.
+ ///
+ public TimeSpan Window { get; set; } = TimeSpan.FromSeconds(1.0 / 60.0);
+
+ ///
+ /// The offset from the point at which this was constructed (or the last time
+ /// was called) at which the next window is due to begin.
+ ///
+ public TimeSpan NextWindowStart { get; private set; } = TimeSpan.Zero;
+
+ ///
+ /// The offset from the point at which this was constructed (or the last time
+ /// was called) at which the next window is due to begin.
+ ///
+ public TimeSpan NextWindowEnd
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining | (MethodImplOptions)768)]
+ get => NextWindowStart + Window;
+ }
+
+ ///
+ /// The offset from the point at which this was constructed (or the last time
+ /// was called).
+ ///
+ public TimeSpan CurrentTime
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining | (MethodImplOptions)768)]
+ get => _sw.Elapsed;
+ }
+
+#if NET7_0_OR_GREATER
+ ///
+ /// Whether should use .
+ ///
+ private bool Sleeping { get; }
+#endif
+
+ ///
+ /// Resets the to zero and the offset from which is
+ /// measured.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining | (MethodImplOptions)768)]
+ public void Reset()
+ {
+ _sw.Restart();
+ NextWindowStart = TimeSpan.Zero;
+ }
+
+ ///
+ /// Determines whether the next window is due.
+ ///
+ /// True if the next window is due or overdue, false otherwise.
+ ///
+ /// If false, do any time-independent, real-time work and call this method again.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining | (MethodImplOptions)768)]
+ public bool BeginWindow()
+ {
+#if NET7_0_OR_GREATER
+ var ct = CurrentTime;
+ var nw = NextWindowStart;
+ if (Sleeping && ct < nw)
+ {
+ _sleep.Sleep(nw - ct);
+ }
+#endif
+ // ct local var intentionally not used here to ensure we are up-to-date
+ return CurrentTime >= NextWindowStart;
+ }
+
+ ///
+ /// Completes this time window.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining | (MethodImplOptions)768)]
+ public void EndWindow()
+ {
+ NextWindowStart += Window;
+ }
+}
diff --git a/src/Core/Silk.NET.Core/Silk.NET.Core.csproj b/src/Core/Silk.NET.Core/Silk.NET.Core.csproj
index ae5a8b1fcc..76db24e0f8 100644
--- a/src/Core/Silk.NET.Core/Silk.NET.Core.csproj
+++ b/src/Core/Silk.NET.Core/Silk.NET.Core.csproj
@@ -1,7 +1,7 @@
- netstandard2.0;netstandard2.1;netcoreapp3.1;net5.0;net6.0
+ netstandard2.0;netstandard2.1;netcoreapp3.1;net5.0;net6.0;net7.0
true
preview
true
diff --git a/src/Lab/Experiments/TimeTrials/Program.cs b/src/Lab/Experiments/TimeTrials/Program.cs
new file mode 100644
index 0000000000..4db36a5b4f
--- /dev/null
+++ b/src/Lab/Experiments/TimeTrials/Program.cs
@@ -0,0 +1,103 @@
+// See https://aka.ms/new-console-template for more information
+
+using System.Collections.Concurrent;
+using System.Diagnostics;
+using Silk.NET.Core;
+using Silk.NET.Windowing;
+
+var timeWindow = new TimeWindow();
+var bag = new ConcurrentBag();
+var sw = new Stopwatch();
+Console.Write("Enter frames per second: ");
+timeWindow.Window = TimeSpan.FromSeconds(1 / double.Parse(Console.ReadLine()!));
+Console.Write("Open a window? (Y/N) ");
+ConsoleKey k;
+while ((k = Console.ReadKey().Key) is not ConsoleKey.Y and not ConsoleKey.Y) {}
+
+if (k == ConsoleKey.Y)
+{
+ Console.Write("\nOpening window...\n");
+ var realWindow = Window.Create
+ (
+ WindowOptions.Default with
+ {
+ VSync = false, FramesPerSecond = 1 / timeWindow.Window.TotalSeconds,
+ UpdatesPerSecond = 1 / timeWindow.Window.TotalSeconds
+ }
+ );
+ realWindow.Initialize();
+ realWindow.Title += " | 0 FPS | 0 UPS";
+ realWindow.Render += d =>
+ {
+ var cur = realWindow.Title;
+ realWindow.Title = cur.Replace
+ (cur[cur.IndexOf('|')..(cur.IndexOf(" FPS", StringComparison.Ordinal) + 4)], $"| {1 / d:0000.000} FPS");
+ Console.WriteLine($"{sw.Elapsed.TotalSeconds},{d}");
+ };
+ realWindow.Update += d =>
+ {
+ var cur = realWindow.Title;
+ realWindow.Title = cur.Replace
+ (cur[cur.LastIndexOf('|')..(cur.IndexOf(" UPS", StringComparison.Ordinal) + 4)], $"| {1 / d:0000.000} UPS");
+ };
+ Console.WriteLine("Recording Time,Frame Time");
+ sw.Start();
+ while (!realWindow.IsClosing)
+ {
+ if (timeWindow.BeginWindow())
+ {
+ if (!realWindow.IsClosing)
+ {
+ realWindow.DoUpdate();
+ }
+
+ if (!realWindow.IsClosing)
+ {
+ realWindow.DoRender();
+ }
+ timeWindow.EndWindow();
+ }
+ realWindow.DoEvents();
+ }
+
+ realWindow.DoEvents();
+ realWindow.Reset();
+ return;
+}
+
+Console.WriteLine("Press Ctrl+C to measure statistics.");
+Console.CancelKeyPress += (_, _) =>
+{
+ 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
+ {
+ bag.Add(-(timeWindow.NextWindowStart - timeWindow.CurrentTime).TotalSeconds);
+ }
+}
diff --git a/src/Lab/Experiments/TimeTrials/TimeTrials.csproj b/src/Lab/Experiments/TimeTrials/TimeTrials.csproj
new file mode 100644
index 0000000000..f4d5d74797
--- /dev/null
+++ b/src/Lab/Experiments/TimeTrials/TimeTrials.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net7.0
+ enable
+ enable
+
+
+
+
+
+
+
+