Browse Source

Greatly improved the timing routine.

* Rewrote the timing routine to be more accurate and more stable.
    * The Stopwatch is now used instead of SDL_GetTicks, as the former is
      significantly more accurate.
    * OS-specific delay routines are now used where possible.
        - Currently, only POSIX platforms support this (using nanosleep).
          Other platforms will fall back to SDL_Delay.
        - Improved the SDL_Delay method to take fractional times into account
          as much as possible.
    * BREAKING CHANGES:
        - Changed SdlWindow's Start() function to use hertz instead of
          milliseconds, as that's likely going to be more intuitive for games.
        - Changed SdlWindow's VideoUpdateTicks and GameUpdateTicks properties
          to be VideoUpdateRate and GameUpdateRate, respectively. These also
          reports the rate in hertz rather than milliseconds.
        - The game loop now passes a float (delta) to OnUpdate to indicate the
          expected amount of time that has passed between update calls.
    * Updated the sample projects to accommodate the aforementioned breaking
      changes. :)
improved_timing
Ian Burgmyer 5 years ago
parent
commit
aa9f02f8e7
  1. 105
      DotSDL/Graphics/SdlWindow.cs
  2. 13
      DotSDL/Platform/BasePlatform.cs
  3. 14
      DotSDL/Platform/IPlatform.cs
  4. 29
      DotSDL/Platform/Interop/Fallback/Timing.cs
  5. 36
      DotSDL/Platform/Interop/Posix/Timing.cs
  6. 18
      DotSDL/Platform/PlatformFactory.cs
  7. 10
      DotSDL/Platform/PosixPlatform.cs
  8. 2
      Samples/Sample.Audio/Program.cs
  9. 4
      Samples/Sample.Audio/Window.cs
  10. 2
      Samples/Sample.BasicPixels/Program.cs
  11. 2
      Samples/Sample.Layers/Program.cs
  12. 2
      Samples/Sample.Layers/Window.cs
  13. 2
      Samples/Sample.Power/Program.cs
  14. 2
      Samples/Sample.Sprites/Program.cs
  15. 2
      Samples/Sample.Sprites/Window.cs

105
DotSDL/Graphics/SdlWindow.cs

@ -1,8 +1,10 @@
using DotSDL.Events;
using DotSDL.Input;
using DotSDL.Interop.Core;
using DotSDL.Platform;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace DotSDL.Graphics {
@ -19,13 +21,24 @@ namespace DotSDL.Graphics {
private IntPtr _texture;
private bool _hasTexture;
private float _videoUpdateRate, _gameUpdateRate;
private float _videoUpdateMs, _gameUpdateMs;
private bool _videoUpdateUncapped, _gameUpdateUncapped;
private bool _running;
private uint _nextVideoUpdate;
private uint _nextGameUpdate;
private float _nextVideoUpdate;
private float _nextGameUpdate;
private float _updateDelta;
private ScalingQuality _scalingQuality = ScalingQuality.Nearest;
/// <summary>
/// An <see cref="IPlatform"/> that contains native functions appropriate to
/// the platform that this application is running on.
/// </summary>
protected IPlatform Platform { get; } = PlatformFactory.GetPlatform();
/// <summary>Gets the background layer of this window. This is equivalent to accessing Layers[0].</summary>
public Canvas Background => Layers[0];
@ -66,11 +79,35 @@ namespace DotSDL.Graphics {
public int RenderHeight { get; }
/// <summary>The amount of time, in milliseconds, from when the application was started.</summary>
public uint TicksElapsed => Timer.GetTicks();
/// <summary>Gets or sets the amount of time, in milliseconds, between video updates.</summary>
public uint VideoUpdateTicks { get; set; }
/// <summary>Gets or sets the amount of time, in milliseconds, between game (logic) updates.</summary>
public uint GameUpdateTicks { get; set; }
public float MillisecondsElapsed { get; private set; } = 0.0f;
/// <summary>Gets or sets the rate, in hertz, between video updates.</summary>
public float VideoUpdateRate {
get => _videoUpdateUncapped ? 0 : _videoUpdateRate;
set {
_videoUpdateRate = value;
if(System.Math.Abs(_videoUpdateRate) < 1.0f) {
_videoUpdateUncapped = true;
} else {
_videoUpdateUncapped = false;
_videoUpdateMs = 1000 / _videoUpdateRate;
}
}
}
/// <summary>Gets or sets the rate, in hertz, between game (logic) updates.</summary>
public float GameUpdateRate {
get => _gameUpdateUncapped ? 0 : _gameUpdateRate;
set {
_gameUpdateRate = value;
if(System.Math.Abs(_gameUpdateRate) < 1.0f) {
_gameUpdateUncapped = true;
} else {
_gameUpdateUncapped = false;
_gameUpdateMs = 1000 / _gameUpdateRate;
}
}
}
/// <summary>Gets a <see cref="Rectangle"/> that can be manipulated to modify how much of the scene is displayed.</summary>
public Rectangle CameraView { get; }
@ -270,11 +307,11 @@ namespace DotSDL.Graphics {
/// <summary>
/// Handles updating the application logic for the <see cref="SdlWindow"/>.
/// </summary>
private void BaseUpdate() {
private void BaseUpdate(float delta) {
if(IsDestroyed) return;
Events.EventHandler.ProcessEvents();
OnUpdate(); // Call the overridden Update function.
OnUpdate(delta); // Call the overridden Update function.
}
/// <summary>
@ -401,26 +438,40 @@ namespace DotSDL.Graphics {
/// A game loop that calls the <see cref="SdlWindow"/> update and draw functions.
/// </summary>
private void Loop() {
long MsToNs(float ms) => (long)(ms * 1000000);
var sw = new Stopwatch();
_running = true;
while(_running) {
var ticks = TicksElapsed;
sw.Restart();
if(ticks > _nextGameUpdate || GameUpdateTicks == 0) {
_nextGameUpdate = ticks + GameUpdateTicks;
BaseUpdate();
if(_nextGameUpdate <= 0 || _gameUpdateUncapped) {
BaseUpdate(_updateDelta);
_updateDelta = 0;
_nextGameUpdate += _gameUpdateMs;
}
if(ticks > _nextVideoUpdate || VideoUpdateTicks == 0) {
_nextVideoUpdate = ticks + VideoUpdateTicks;
if(_nextVideoUpdate <= 0 || _videoUpdateUncapped) {
BaseDraw();
_nextVideoUpdate += _videoUpdateMs;
}
if(VideoUpdateTicks <= 0 && GameUpdateTicks <= 0) continue; // Cook the CPU!
var cycleElapsed = (float)sw.Elapsed.TotalMilliseconds;
MillisecondsElapsed += cycleElapsed;
_nextGameUpdate -= cycleElapsed;
_nextVideoUpdate -= cycleElapsed;
_updateDelta += cycleElapsed;
var updateTicks = (long)(_nextGameUpdate > _nextVideoUpdate ? _nextVideoUpdate : _nextGameUpdate) - TicksElapsed;
if(updateTicks > 0)
Timer.Delay((uint)updateTicks);
if(!_videoUpdateUncapped && !_gameUpdateUncapped) {
var waitMs = _nextGameUpdate > _nextVideoUpdate ? _nextVideoUpdate : _nextGameUpdate;
if(waitMs > 0)
Platform.Nanosleep(MsToNs(waitMs));
_updateDelta += waitMs;
_nextGameUpdate -= waitMs;
_nextVideoUpdate -= waitMs;
}
}
}
@ -461,7 +512,7 @@ namespace DotSDL.Graphics {
/// <summary>
/// Called every time the application logic update runs.
/// </summary>
protected virtual void OnUpdate() { }
protected virtual void OnUpdate(float delta) { }
/// <summary>
/// Removes a layer from the layer stack.
@ -485,19 +536,19 @@ namespace DotSDL.Graphics {
/// <summary>
/// Displays the window and begins executing code that's associated with it.
/// </summary>
/// <param name="updateRate">The desired number of milliseconds between frames and game logic updates. 0 causes the display and game to be continuously updated.</param>
public void Start(uint updateRate) {
/// <param name="updateRate">The desired number of video and game logic updates per second. 0 causes the display and game to be updated as quickly as possible.</param>
public void Start(float updateRate) {
Start(updateRate, updateRate);
}
/// <summary>
/// Displays the window and begins executing code that's associated with it.
/// </summary>
/// <param name="drawRate">The desired number of milliseconds between draw calls. 0 causes the display to be continuously updated.</param>
/// <param name="updateRate">The desired number of milliseconds between game logic updates. 0 causes the game to be continuously updated.</param>
public void Start(uint drawRate, uint updateRate) {
VideoUpdateTicks = drawRate;
GameUpdateTicks = updateRate;
/// <param name="drawRate">The desired number of draw calls per second. 0 causes the display to be updated as quickly as possible.</param>
/// <param name="updateRate">The desired number of game logic updates per second. 0 causes the game to be updated as quickly as possible.</param>
public void Start(float drawRate, float updateRate) {
VideoUpdateRate = drawRate;
GameUpdateRate = updateRate;
BaseLoad();
Video.ShowWindow(_window);

13
DotSDL/Platform/BasePlatform.cs

@ -0,0 +1,13 @@
using System;
using DotSDL.Platform.Interop.Fallback;
namespace DotSDL.Platform {
/// <summary>
/// Implements the
/// </summary>
public class BasePlatform : IPlatform {
private readonly Timing _fallbackTiming = new Timing();
public virtual Action<long> Nanosleep => _fallbackTiming.Nanosleep;
}
}

14
DotSDL/Platform/IPlatform.cs

@ -0,0 +1,14 @@
using System;
namespace DotSDL.Platform {
/// <summary>
/// Defines a set of function pointers for platform-specific native calls.
/// </summary>
public interface IPlatform {
/// <summary>
/// A high-resolution sleep timer that attempts to rest for a given
/// number of nanoseconds.
/// </summary>
Action<long> Nanosleep { get; }
}
}

29
DotSDL/Platform/Interop/Fallback/Timing.cs

@ -0,0 +1,29 @@
using DotSDL.Interop.Core;
namespace DotSDL.Platform.Interop.Fallback {
public class Timing {
private float _sleepSkew = 0.0f;
/// <summary>
/// Sleeps for a given number of nanoseconds.
/// </summary>
/// <param name="ns">The number of nanoseconds to sleep.</param>
/// <remarks>This implementation uses the SDL_Delay function and should work
/// with all platforms. It attempts to skirt around the resolution issues using
/// the number of fractional milliseconds.</remarks>
public void Nanosleep(long ns) {
var waitTime = ns / 1000000;
_sleepSkew += (float)ns / 1000000 - waitTime;
if(_sleepSkew >= 1) {
// Take the whole part of the skew and add it to the sleep time.
var skewAdd = (long)_sleepSkew;
waitTime += skewAdd;
_sleepSkew -= skewAdd;
}
if(waitTime > 0)
Timer.Delay((uint)waitTime);
}
}
}

36
DotSDL/Platform/Interop/Posix/Timing.cs

@ -0,0 +1,36 @@
using System;
using System.Runtime.InteropServices;
namespace DotSDL.Platform.Interop.Posix {
public class Timing {
private struct Timespec {
/// <summary>
/// A number of seconds.
/// </summary>
public int tvSec;
/// <summary>
/// A number of nanoseconds. This field must be in the range of 0 to 999999999.
/// </summary>
public long tvNsec;
}
/// <summary>
/// Suspends a thread until at least the time given in <paramref name="req"/> has
/// elapsed.
/// </summary>
/// <param name="req">The requested amount of time to sleep.</param>
/// <param name="rem">The remaining sleep time, or NULL if this isn't necessary.</param>
[DllImport("c", EntryPoint = "nanosleep", CallingConvention = CallingConvention.Cdecl)]
private static extern void PosixNanosleep(in Timespec req, out Timespec rem);
/// <summary>
/// Sleeps for a given number of nanoseconds.
/// </summary>
/// <param name="ns">The number of nanoseconds to sleep.</param>
/// <remarks>This implementation uses the nanosleep function introduced
/// with POSIX.1.</remarks>
public void Nanosleep(long ns) =>
PosixNanosleep(new Timespec { tvSec = 0, tvNsec = ns}, out _);
}
}

18
DotSDL/Platform/PlatformFactory.cs

@ -0,0 +1,18 @@
using System;
namespace DotSDL.Platform {
/// <summary>
/// Senses the user's platform and returns a new instance of the most
/// appropriate <see cref="IPlatform"/> implementation.
/// </summary>
public static class PlatformFactory {
public static IPlatform GetPlatform() {
switch(Environment.OSVersion.Platform) {
case PlatformID.Unix:
return new PosixPlatform();
default:
return new BasePlatform();
}
}
}
}

10
DotSDL/Platform/PosixPlatform.cs

@ -0,0 +1,10 @@
using System;
using DotSDL.Platform.Interop.Posix;
namespace DotSDL.Platform {
public class PosixPlatform : BasePlatform {
private readonly Timing _posixTiming = new Timing();
public override Action<long> Nanosleep => _posixTiming.Nanosleep;
}
}

2
Samples/Sample.Audio/Program.cs

@ -2,7 +2,7 @@
internal class Program {
internal static void Main(string[] args) {
var window = new Window(720, 180);
window.Start(16);
window.Start(60);
}
}
}

4
Samples/Sample.Audio/Window.cs

@ -107,7 +107,7 @@ namespace Sample.Audio {
base.OnDraw();
}
protected override void OnUpdate() {
protected override void OnUpdate(float _) {
var delta = Fast ? 10 : 1;
if(UpPressed)
_freq += delta;
@ -117,7 +117,7 @@ namespace Sample.Audio {
if(_freq > _maxFreq) _freq = _maxFreq;
if(_freq < _minFreq) _freq = _minFreq;
base.OnUpdate();
base.OnUpdate(delta);
}
private void Window_KeyPressed(object sender, DotSDL.Events.KeyboardEvent e) {

2
Samples/Sample.BasicPixels/Program.cs

@ -2,7 +2,7 @@
internal class Program {
private static void Main(string[] args) {
var window = new Window(512, 256);
window.Start(100, 16); // 10fps, 62.5ups
window.Start(10, 60); // 10 fps, 60hz updates
}
}
}

2
Samples/Sample.Layers/Program.cs

@ -2,7 +2,7 @@
internal class Program {
private static void Main(string[] args) {
var window = new Window(4);
window.Start(16);
window.Start(60);
}
}
}

2
Samples/Sample.Layers/Window.cs

@ -82,7 +82,7 @@ namespace Sample.Layers {
Stop();
}
protected override void OnUpdate() {
protected override void OnUpdate(float delta) {
_redPhase += RedSpeed;
_greenPhase += GreenSpeed;
_bluePhase += BlueSpeed;

2
Samples/Sample.Power/Program.cs

@ -4,7 +4,7 @@ namespace Sample.Power {
class Program {
static void Main(string[] args) {
var window = new Window(256, 128);
window.Start(100, 16);
window.Start(10, 60);
}
}
}

2
Samples/Sample.Sprites/Program.cs

@ -2,7 +2,7 @@
internal static class Program {
private static void Main(string[] args) {
var window = new Window(4);
window.Start(16);
window.Start(60);
}
}
}

2
Samples/Sample.Sprites/Window.cs

@ -155,7 +155,7 @@ namespace Sample.Sprites {
}
}
protected override void OnUpdate() {
protected override void OnUpdate(float delta) {
_player1.Move(_player1Delta);
_player2.Move(_player2Delta);

Loading…
Cancel
Save