diff --git a/DotSDL/Graphics/SdlWindow.cs b/DotSDL/Graphics/SdlWindow.cs
index 9998b15..808bc36 100644
--- a/DotSDL/Graphics/SdlWindow.cs
+++ b/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;
+ ///
+ /// An that contains native functions appropriate to
+ /// the platform that this application is running on.
+ ///
+ protected IPlatform Platform { get; } = PlatformFactory.GetPlatform();
+
/// Gets the background layer of this window. This is equivalent to accessing Layers[0].
public Canvas Background => Layers[0];
@@ -66,11 +79,35 @@ namespace DotSDL.Graphics {
public int RenderHeight { get; }
/// The amount of time, in milliseconds, from when the application was started.
- public uint TicksElapsed => Timer.GetTicks();
- /// Gets or sets the amount of time, in milliseconds, between video updates.
- public uint VideoUpdateTicks { get; set; }
- /// Gets or sets the amount of time, in milliseconds, between game (logic) updates.
- public uint GameUpdateTicks { get; set; }
+ public float MillisecondsElapsed { get; private set; } = 0.0f;
+
+ /// Gets or sets the rate, in hertz, between video updates.
+ 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;
+ }
+ }
+ }
+
+ /// Gets or sets the rate, in hertz, between game (logic) updates.
+ 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;
+ }
+ }
+ }
/// Gets a that can be manipulated to modify how much of the scene is displayed.
public Rectangle CameraView { get; }
@@ -270,11 +307,11 @@ namespace DotSDL.Graphics {
///
/// Handles updating the application logic for the .
///
- 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.
}
///
@@ -401,26 +438,40 @@ namespace DotSDL.Graphics {
/// A game loop that calls the update and draw functions.
///
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 {
///
/// Called every time the application logic update runs.
///
- protected virtual void OnUpdate() { }
+ protected virtual void OnUpdate(float delta) { }
///
/// Removes a layer from the layer stack.
@@ -485,19 +536,19 @@ namespace DotSDL.Graphics {
///
/// Displays the window and begins executing code that's associated with it.
///
- /// The desired number of milliseconds between frames and game logic updates. 0 causes the display and game to be continuously updated.
- public void Start(uint 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.
+ public void Start(float updateRate) {
Start(updateRate, updateRate);
}
///
/// Displays the window and begins executing code that's associated with it.
///
- /// The desired number of milliseconds between draw calls. 0 causes the display to be continuously updated.
- /// The desired number of milliseconds between game logic updates. 0 causes the game to be continuously updated.
- public void Start(uint drawRate, uint updateRate) {
- VideoUpdateTicks = drawRate;
- GameUpdateTicks = updateRate;
+ /// The desired number of draw calls per second. 0 causes the display to be updated as quickly as possible.
+ /// The desired number of game logic updates per second. 0 causes the game to be updated as quickly as possible.
+ public void Start(float drawRate, float updateRate) {
+ VideoUpdateRate = drawRate;
+ GameUpdateRate = updateRate;
BaseLoad();
Video.ShowWindow(_window);
diff --git a/DotSDL/Platform/BasePlatform.cs b/DotSDL/Platform/BasePlatform.cs
new file mode 100644
index 0000000..2baa1f7
--- /dev/null
+++ b/DotSDL/Platform/BasePlatform.cs
@@ -0,0 +1,13 @@
+using System;
+using DotSDL.Platform.Interop.Fallback;
+
+namespace DotSDL.Platform {
+ ///
+ /// Implements the
+ ///
+ public class BasePlatform : IPlatform {
+ private readonly Timing _fallbackTiming = new Timing();
+
+ public virtual Action Nanosleep => _fallbackTiming.Nanosleep;
+ }
+}
diff --git a/DotSDL/Platform/IPlatform.cs b/DotSDL/Platform/IPlatform.cs
new file mode 100644
index 0000000..9bc6d54
--- /dev/null
+++ b/DotSDL/Platform/IPlatform.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace DotSDL.Platform {
+ ///
+ /// Defines a set of function pointers for platform-specific native calls.
+ ///
+ public interface IPlatform {
+ ///
+ /// A high-resolution sleep timer that attempts to rest for a given
+ /// number of nanoseconds.
+ ///
+ Action Nanosleep { get; }
+ }
+}
diff --git a/DotSDL/Platform/Interop/Fallback/Timing.cs b/DotSDL/Platform/Interop/Fallback/Timing.cs
new file mode 100644
index 0000000..245658a
--- /dev/null
+++ b/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;
+
+ ///
+ /// Sleeps for a given number of nanoseconds.
+ ///
+ /// The number of nanoseconds to sleep.
+ /// 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.
+ 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);
+ }
+ }
+}
diff --git a/DotSDL/Platform/Interop/Posix/Timing.cs b/DotSDL/Platform/Interop/Posix/Timing.cs
new file mode 100644
index 0000000..2afb07d
--- /dev/null
+++ b/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 {
+ ///
+ /// A number of seconds.
+ ///
+ public int tvSec;
+
+ ///
+ /// A number of nanoseconds. This field must be in the range of 0 to 999999999.
+ ///
+ public long tvNsec;
+ }
+
+ ///
+ /// Suspends a thread until at least the time given in has
+ /// elapsed.
+ ///
+ /// The requested amount of time to sleep.
+ /// The remaining sleep time, or NULL if this isn't necessary.
+ [DllImport("c", EntryPoint = "nanosleep", CallingConvention = CallingConvention.Cdecl)]
+ private static extern void PosixNanosleep(in Timespec req, out Timespec rem);
+
+ ///
+ /// Sleeps for a given number of nanoseconds.
+ ///
+ /// The number of nanoseconds to sleep.
+ /// This implementation uses the nanosleep function introduced
+ /// with POSIX.1.
+ public void Nanosleep(long ns) =>
+ PosixNanosleep(new Timespec { tvSec = 0, tvNsec = ns}, out _);
+ }
+}
diff --git a/DotSDL/Platform/PlatformFactory.cs b/DotSDL/Platform/PlatformFactory.cs
new file mode 100644
index 0000000..9301b50
--- /dev/null
+++ b/DotSDL/Platform/PlatformFactory.cs
@@ -0,0 +1,18 @@
+using System;
+
+namespace DotSDL.Platform {
+ ///
+ /// Senses the user's platform and returns a new instance of the most
+ /// appropriate implementation.
+ ///
+ public static class PlatformFactory {
+ public static IPlatform GetPlatform() {
+ switch(Environment.OSVersion.Platform) {
+ case PlatformID.Unix:
+ return new PosixPlatform();
+ default:
+ return new BasePlatform();
+ }
+ }
+ }
+}
diff --git a/DotSDL/Platform/PosixPlatform.cs b/DotSDL/Platform/PosixPlatform.cs
new file mode 100644
index 0000000..7eb0d81
--- /dev/null
+++ b/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 Nanosleep => _posixTiming.Nanosleep;
+ }
+}
diff --git a/Samples/Sample.Audio/Program.cs b/Samples/Sample.Audio/Program.cs
index 9df09d3..528fdd6 100644
--- a/Samples/Sample.Audio/Program.cs
+++ b/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);
}
}
}
diff --git a/Samples/Sample.Audio/Window.cs b/Samples/Sample.Audio/Window.cs
index 362c9f6..e5cffba 100644
--- a/Samples/Sample.Audio/Window.cs
+++ b/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) {
diff --git a/Samples/Sample.BasicPixels/Program.cs b/Samples/Sample.BasicPixels/Program.cs
index 2534880..b9425e3 100644
--- a/Samples/Sample.BasicPixels/Program.cs
+++ b/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
}
}
}
diff --git a/Samples/Sample.Layers/Program.cs b/Samples/Sample.Layers/Program.cs
index 83d2dfe..97e86b2 100644
--- a/Samples/Sample.Layers/Program.cs
+++ b/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);
}
}
}
diff --git a/Samples/Sample.Layers/Window.cs b/Samples/Sample.Layers/Window.cs
index 262b24a..a060745 100644
--- a/Samples/Sample.Layers/Window.cs
+++ b/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;
diff --git a/Samples/Sample.Power/Program.cs b/Samples/Sample.Power/Program.cs
index 6ba803c..c37207f 100644
--- a/Samples/Sample.Power/Program.cs
+++ b/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);
}
}
}
diff --git a/Samples/Sample.Sprites/Program.cs b/Samples/Sample.Sprites/Program.cs
index 75e5851..4b1b0c1 100644
--- a/Samples/Sample.Sprites/Program.cs
+++ b/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);
}
}
}
diff --git a/Samples/Sample.Sprites/Window.cs b/Samples/Sample.Sprites/Window.cs
index 81529b0..676d7d8 100644
--- a/Samples/Sample.Sprites/Window.cs
+++ b/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);