-
-
Notifications
You must be signed in to change notification settings - Fork 397
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
213 additions
and
34 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |