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);