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 + + + + + + + +