diff --git a/FreedomFighter/Game1.cs b/FreedomFighter/Game1.cs index aa0fda2..0c6f7f5 100644 --- a/FreedomFighter/Game1.cs +++ b/FreedomFighter/Game1.cs @@ -32,6 +32,11 @@ public class Game1 : Game int _fps, _fpsFrames; double _fpsTimer; + // precise 120Hz pacing (MonoGame's fixed-timestep Sleep overshoots and drops ticks) + const double TickSeconds = 1 / 120.0; + readonly System.Diagnostics.Stopwatch _frameClock = System.Diagnostics.Stopwatch.StartNew(); + double _nextFrameTime; + public Game1() { _graphics = new GraphicsDeviceManager(this) @@ -42,8 +47,7 @@ public class Game1 : Game }; Content.RootDirectory = "Content"; IsMouseVisible = true; - IsFixedTimeStep = true; - TargetElapsedTime = TimeSpan.FromSeconds(1 / 120.0); + IsFixedTimeStep = false; // paced manually in EndDraw for an accurate 120Hz Window.Title = "Freedom Fighter"; Window.AllowUserResizing = true; Window.ClientSizeChanged += (_, _) => @@ -113,6 +117,21 @@ public class Game1 : Game } } + protected override void EndDraw() + { + base.EndDraw(); + + // hybrid sleep/spin wait until the next 1/120s boundary + _nextFrameTime += TickSeconds; + double now = _frameClock.Elapsed.TotalSeconds; + if (now > _nextFrameTime + 0.25) + _nextFrameTime = now; // resync after a stall (window drag, load, etc.) + while (_nextFrameTime - _frameClock.Elapsed.TotalSeconds > 0.002) + System.Threading.Thread.Sleep(1); + while (_frameClock.Elapsed.TotalSeconds < _nextFrameTime) + System.Threading.Thread.SpinWait(50); + } + protected override void Update(GameTime gameTime) { // all logic runs in Draw, matching the original's timer callback diff --git a/FreedomFighter/Program.cs b/FreedomFighter/Program.cs index a6ae8bc..f8b3fc7 100644 --- a/FreedomFighter/Program.cs +++ b/FreedomFighter/Program.cs @@ -1,17 +1,31 @@ using System; +using System.Runtime.InteropServices; -using var game = new FreedomFighter.Game1(); -// debug aid: --shot [frames] renders, saves the backbuffer to a PNG, and exits -var cmdArgs = Environment.GetCommandLineArgs(); -for (int i = 1; i < cmdArgs.Length - 1; i++) +// 1ms timer resolution so the 120Hz fixed timestep's Thread.Sleep doesn't overshoot +// (default ~15.6ms granularity caps the loop at 80-90 FPS; OmegaTimer did the same) +[DllImport("winmm.dll")] static extern uint timeBeginPeriod(uint ms); +[DllImport("winmm.dll")] static extern uint timeEndPeriod(uint ms); +timeBeginPeriod(1); + +try { - if (cmdArgs[i] == "--shot") + using var game = new FreedomFighter.Game1(); + // debug aid: --shot [frames] renders, saves the backbuffer to a PNG, and exits + var cmdArgs = Environment.GetCommandLineArgs(); + for (int i = 1; i < cmdArgs.Length - 1; i++) { - game.ShotFile = cmdArgs[i + 1]; - if (i + 2 < cmdArgs.Length && int.TryParse(cmdArgs[i + 2], out int frames)) - game.ShotFrames = frames; + if (cmdArgs[i] == "--shot") + { + game.ShotFile = cmdArgs[i + 1]; + if (i + 2 < cmdArgs.Length && int.TryParse(cmdArgs[i + 2], out int frames)) + game.ShotFrames = frames; + } + if (cmdArgs[i] == "--level" && int.TryParse(cmdArgs[i + 1], out int level)) + game.StartLevel = level; } - if (cmdArgs[i] == "--level" && int.TryParse(cmdArgs[i + 1], out int level)) - game.StartLevel = level; + game.Run(); +} +finally +{ + timeEndPeriod(1); } -game.Run();