From 1ae391078fc435b8db5f21e4b33c44cc797f35b0 Mon Sep 17 00:00:00 2001 From: adfriz <76892624+adfriz@users.noreply.github.com> Date: Sun, 23 Aug 2026 22:25:56 +0700 Subject: [PATCH 1/7] Improve FPS limiter performance --- source/ObjectViewer/ProgramS.cs | 4 - source/ObjectViewer/System/GameWindow.cs | 4 + source/ObjectViewer/formOptions.cs | 1 - source/OpenBVE/Graphics/Screen.cs | 7 +- source/OpenBVE/System/GameWindow.cs | 4 + source/OpenBveApi/System/Timers.cs | 99 ++++++++++++++++++++++++ source/RouteViewer/ProgramR.cs | 4 - source/RouteViewer/System/Gamewindow.cs | 7 +- source/RouteViewer/formOptions.cs | 1 - 9 files changed, 116 insertions(+), 15 deletions(-) diff --git a/source/ObjectViewer/ProgramS.cs b/source/ObjectViewer/ProgramS.cs index 1dbfae4c1..d658a69ba 100644 --- a/source/ObjectViewer/ProgramS.cs +++ b/source/ObjectViewer/ProgramS.cs @@ -182,10 +182,6 @@ internal static void Main(string[] args) Title = "Object Viewer" }; Renderer.GameWindow.VSync = Interface.CurrentOptions.VerticalSynchronization ? VSyncMode.On : VSyncMode.Off; - if (Interface.CurrentOptions.FPSLimit > 0) - { - Renderer.GameWindow.TargetRenderFrequency = Interface.CurrentOptions.FPSLimit; - } Renderer.GameWindow.Run(); // quit Renderer.TextureManager.UnloadAllTextures(false); diff --git a/source/ObjectViewer/System/GameWindow.cs b/source/ObjectViewer/System/GameWindow.cs index 59e58e97e..f291cb268 100644 --- a/source/ObjectViewer/System/GameWindow.cs +++ b/source/ObjectViewer/System/GameWindow.cs @@ -39,6 +39,7 @@ public ObjectViewer(int width, int height, GraphicsMode currentGraphicsMode, str protected override void OnRenderFrame(FrameEventArgs e) { + FrameLimiter.StartFrame(); if (Program.Renderer.RenderThreadJobWaiting) { while (!Program.Renderer.RenderThreadJobs.IsEmpty) @@ -263,6 +264,8 @@ protected override void OnRenderFrame(FrameEventArgs e) Program.Renderer.Lighting.Initialize(); Program.Renderer.RenderScene(timeElapsed); SwapBuffers(); + // A hard cap of 540fps is always applied, even when unlimited is selected + FrameLimiter.ApplyLimit(Interface.CurrentOptions.FPSLimit); RenderRealTimeElapsed = 0.0; } @@ -327,6 +330,7 @@ protected override void OnClosing(CancelEventArgs e) Interface.CurrentOptions.Save(Path.CombineFile(Program.FileSystem.SettingsFolder, "1.5.0/options_ov.cfg")); Program.Renderer.VisibilityThreadShouldRun = false; Program.Renderer.DeInitialize(); + FrameLimiter.RestoreTimerResolution(); if (Program.CurrentHost.MonoRuntime) { Environment.Exit(0); diff --git a/source/ObjectViewer/formOptions.cs b/source/ObjectViewer/formOptions.cs index aa9fea2b4..32617bbfa 100644 --- a/source/ObjectViewer/formOptions.cs +++ b/source/ObjectViewer/formOptions.cs @@ -280,7 +280,6 @@ private void CloseButton_Click(object sender, EventArgs e) int[] fpsPresets = { 0, 30, 60, 120, 240 }; Interface.CurrentOptions.FPSLimit = comboBoxFPSLimit.SelectedIndex >= 0 ? fpsPresets[comboBoxFPSLimit.SelectedIndex] : 0; Program.Renderer.GameWindow.VSync = Interface.CurrentOptions.VerticalSynchronization ? OpenTK.VSyncMode.On : OpenTK.VSyncMode.Off; - Program.Renderer.GameWindow.TargetRenderFrequency = Interface.CurrentOptions.FPSLimit > 0 ? Interface.CurrentOptions.FPSLimit : 0; // Saving shadow settings switch (comboBoxShadowResolution.SelectedIndex) diff --git a/source/OpenBVE/Graphics/Screen.cs b/source/OpenBVE/Graphics/Screen.cs index ad8b3fff9..2ea37ad00 100644 --- a/source/OpenBVE/Graphics/Screen.cs +++ b/source/OpenBVE/Graphics/Screen.cs @@ -131,10 +131,11 @@ internal static void Initialize() return; } - // Cap the render rate only, leaving the update rate uncapped. - // Setting the update rate too caused train shaking and dropped input device plugin events (issue #957) + // Cap the render rate via our own sleep-based limiter in OpenBVEGame.OnRenderFrame, + // as OpenTK's TargetRenderFrequency uses a spin-wait which causes very high power consumption. + // Setting the update rate caused train shaking and dropped input device plugin events (issue #957) Program.Renderer.GameWindow.TargetUpdateFrequency = 0; - Program.Renderer.GameWindow.TargetRenderFrequency = Interface.CurrentOptions.FPSLimit > 0 ? Interface.CurrentOptions.FPSLimit : 0; + Program.Renderer.GameWindow.TargetRenderFrequency = 0; Program.Renderer.GameWindow.VSync = Interface.CurrentOptions.VerticalSynchronization ? VSyncMode.On : VSyncMode.Off; } diff --git a/source/OpenBVE/System/GameWindow.cs b/source/OpenBVE/System/GameWindow.cs index d139b5595..3840222d0 100644 --- a/source/OpenBVE/System/GameWindow.cs +++ b/source/OpenBVE/System/GameWindow.cs @@ -100,6 +100,7 @@ protected override void OnRenderFrame(FrameEventArgs e) //If the load is not complete, then we shouldn't be running the mainloop return; } + FrameLimiter.StartFrame(); if (Program.Renderer.RenderThreadJobWaiting) { @@ -309,6 +310,8 @@ protected override void OnRenderFrame(FrameEventArgs e) { Interface.CurrentOptions.BlackBox = false; } + // A hard cap of 540fps is always applied, even when unlimited is selected + FrameLimiter.ApplyLimit(Interface.CurrentOptions.FPSLimit); } protected override void OnUpdateFrame(FrameEventArgs e) @@ -547,6 +550,7 @@ protected override void OnClosing(CancelEventArgs e) } } Program.Renderer.TextureManager.UnloadAllTextures(false); + FrameLimiter.RestoreTimerResolution(); Program.Renderer.VisibilityThreadShouldRun = false; for (int i = 0; i < InputDevicePlugin.AvailablePluginInfos.Count; i++) { diff --git a/source/OpenBveApi/System/Timers.cs b/source/OpenBveApi/System/Timers.cs index 116c03170..978025e6a 100644 --- a/source/OpenBveApi/System/Timers.cs +++ b/source/OpenBveApi/System/Timers.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.Runtime.InteropServices; +using System.Threading; namespace OpenBveApi { @@ -85,4 +86,102 @@ public static int GetClockTicks() } + + /// + /// Cross-platform frame rate limiter. + /// Uses a bulk Thread.Sleep followed by a short Thread.Yield wait, mirroring the approach used by modern OpenTK, + /// in order to avoid the high power consumption of a pure spin-wait based limiter. + /// + public static class FrameLimiter + { + // Tolerance as a fraction of the scheduler period, left unslept so that we do not overshoot the target + private const double Tolerance = 0.02; + // On Windows, timeBeginPeriod(8) is a good compromise between accuracy and power consumption + private const uint WindowsTimerPeriod = 8; + // Hard cap applied even when the user selects 'Unlimited' + private const int HardFpsLimit = 540; + + private static bool timerResolutionRaised; + private static int schedulerPeriod = 1; + private static long frameStartTimestamp; + + [DllImport("winmm")] + private static extern uint timeBeginPeriod(uint uPeriod); + + [DllImport("winmm")] + private static extern uint timeEndPeriod(uint uPeriod); + + /// Marks the start of a frame. Must be called once at the beginning of each rendered frame. + public static void StartFrame() + { + frameStartTimestamp = Stopwatch.GetTimestamp(); + } + + /// Waits until the end of the current frame's allotted timeslot. + /// The maximum frames per second selected by the user. A value of zero or less means unlimited, subject to the hard cap. + public static void ApplyLimit(int fpsLimit) + { + int limit = fpsLimit > 0 ? System.Math.Min(fpsLimit, HardFpsLimit) : HardFpsLimit; + RaiseTimerResolution(); + long now = Stopwatch.GetTimestamp(); + long target = frameStartTimestamp + (long)((1000.0 / fpsLimit / 1000.0) * Stopwatch.Frequency); + double remainingMs = (double)(target - now) * 1000.0 / Stopwatch.Frequency; + if (remainingMs <= 0.0) + { + return; + } + double sleepMs = remainingMs - schedulerPeriod * Tolerance; + int ticks = (int)(sleepMs / schedulerPeriod); + if (ticks > 0) + { + Thread.Sleep(ticks * schedulerPeriod); + } + while (Stopwatch.GetTimestamp() < target) + { + Thread.Yield(); + } + } + + /// Restores the system timer resolution, if it was previously raised. + public static void RestoreTimerResolution() + { + if (timerResolutionRaised) + { + timerResolutionRaised = false; + try + { + timeEndPeriod(WindowsTimerPeriod); + } + catch + { + // Not on Windows, or winmm unavailable + } + } + } + + private static void RaiseTimerResolution() + { + if (!timerResolutionRaised) + { + timerResolutionRaised = true; + try + { + if (Environment.OSVersion.Platform == PlatformID.Win32NT) + { + timeBeginPeriod(WindowsTimerPeriod); + schedulerPeriod = (int)WindowsTimerPeriod; + } + else + { + // Linux and macOS can accurately sleep for around 1ms + schedulerPeriod = 1; + } + } + catch + { + schedulerPeriod = 1; + } + } + } + } } diff --git a/source/RouteViewer/ProgramR.cs b/source/RouteViewer/ProgramR.cs index 73dfeb6d0..eb5cc8770 100644 --- a/source/RouteViewer/ProgramR.cs +++ b/source/RouteViewer/ProgramR.cs @@ -190,10 +190,6 @@ internal static void Main(string[] args) Renderer.GameWindow.TargetRenderFrequency = 0; Renderer.GameWindow.Title = "Route Viewer"; Renderer.GameWindow.VSync = Interface.CurrentOptions.VerticalSynchronization ? VSyncMode.On : VSyncMode.Off; - if (Interface.CurrentOptions.FPSLimit > 0) - { - Renderer.GameWindow.TargetRenderFrequency = Interface.CurrentOptions.FPSLimit; - } processCommandLineArgs = true; Renderer.GameWindow.Run(); //Unload diff --git a/source/RouteViewer/System/Gamewindow.cs b/source/RouteViewer/System/Gamewindow.cs index 81473826f..cc9c364b4 100644 --- a/source/RouteViewer/System/Gamewindow.cs +++ b/source/RouteViewer/System/Gamewindow.cs @@ -53,6 +53,7 @@ protected override void OnUpdateFrame(FrameEventArgs e) //This renders the frame protected override void OnRenderFrame(FrameEventArgs e) { + FrameLimiter.StartFrame(); Program.MouseMovement(); Program.Renderer.FrameRate = RenderFrequency; @@ -83,8 +84,9 @@ protected override void OnRenderFrame(FrameEventArgs e) Program.Renderer.Lighting.UpdateLighting(Program.CurrentRoute.SecondsSinceMidnight, Program.CurrentRoute.LightDefinitions); Program.Renderer.RenderScene(TimeElapsed); MessageManager.UpdateMessages(TimeElapsed); - SwapBuffers(); - + SwapBuffers(); + // A hard cap of 540fps is always applied, even when unlimited is selected + FrameLimiter.ApplyLimit(Interface.CurrentOptions.FPSLimit); } protected override void OnResize(EventArgs e) @@ -144,6 +146,7 @@ protected override void OnClosing(CancelEventArgs e) Loading.Cancel = true; } Program.Renderer.DeInitialize(); + FrameLimiter.RestoreTimerResolution(); if (Program.CurrentHost.MonoRuntime) { // Mono often fails to close the main window properly diff --git a/source/RouteViewer/formOptions.cs b/source/RouteViewer/formOptions.cs index 784f0d2a1..5f1f46d23 100644 --- a/source/RouteViewer/formOptions.cs +++ b/source/RouteViewer/formOptions.cs @@ -327,7 +327,6 @@ private void button1_Click(object sender, EventArgs e) int[] fpsPresets = { 0, 30, 60, 120, 240 }; Interface.CurrentOptions.FPSLimit = comboBoxFPSLimit.SelectedIndex >= 0 ? fpsPresets[comboBoxFPSLimit.SelectedIndex] : 0; Program.Renderer.GameWindow.VSync = Interface.CurrentOptions.VerticalSynchronization ? OpenTK.VSyncMode.On : OpenTK.VSyncMode.Off; - Program.Renderer.GameWindow.TargetRenderFrequency = Interface.CurrentOptions.FPSLimit > 0 ? Interface.CurrentOptions.FPSLimit : 0; // Sun direction is already updated in real-time via slider events From 3cf76df224b07193cd3543ef11d8097cbae5a1ba Mon Sep 17 00:00:00 2001 From: adfriz <76892624+adfriz@users.noreply.github.com> Date: Sun, 23 Aug 2026 22:45:23 +0700 Subject: [PATCH 2/7] Fix: OverflowException in FrameLimiter when FPS limit is unlimited --- source/OpenBveApi/System/Timers.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/source/OpenBveApi/System/Timers.cs b/source/OpenBveApi/System/Timers.cs index 978025e6a..d5acd6bc6 100644 --- a/source/OpenBveApi/System/Timers.cs +++ b/source/OpenBveApi/System/Timers.cs @@ -122,9 +122,13 @@ public static void StartFrame() public static void ApplyLimit(int fpsLimit) { int limit = fpsLimit > 0 ? System.Math.Min(fpsLimit, HardFpsLimit) : HardFpsLimit; + if (frameStartTimestamp == 0) + { + return; + } RaiseTimerResolution(); long now = Stopwatch.GetTimestamp(); - long target = frameStartTimestamp + (long)((1000.0 / fpsLimit / 1000.0) * Stopwatch.Frequency); + long target = frameStartTimestamp + (long)(Stopwatch.Frequency / limit); double remainingMs = (double)(target - now) * 1000.0 / Stopwatch.Frequency; if (remainingMs <= 0.0) { From b06d34596b7e0f31150e248aadd833be9addeb96 Mon Sep 17 00:00:00 2001 From: adfriz <76892624+adfriz@users.noreply.github.com> Date: Sun, 23 Aug 2026 22:51:10 +0700 Subject: [PATCH 3/7] Fix: Improve frame pacing accuracy of FPS limiter (1ms timer resolution, double-precision slot) --- source/OpenBveApi/System/Timers.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/source/OpenBveApi/System/Timers.cs b/source/OpenBveApi/System/Timers.cs index d5acd6bc6..a5080f680 100644 --- a/source/OpenBveApi/System/Timers.cs +++ b/source/OpenBveApi/System/Timers.cs @@ -96,8 +96,8 @@ public static class FrameLimiter { // Tolerance as a fraction of the scheduler period, left unslept so that we do not overshoot the target private const double Tolerance = 0.02; - // On Windows, timeBeginPeriod(8) is a good compromise between accuracy and power consumption - private const uint WindowsTimerPeriod = 8; + // On Windows, raise the timer resolution to 1ms for accurate frame pacing + private const uint WindowsTimerPeriod = 1; // Hard cap applied even when the user selects 'Unlimited' private const int HardFpsLimit = 540; @@ -128,7 +128,7 @@ public static void ApplyLimit(int fpsLimit) } RaiseTimerResolution(); long now = Stopwatch.GetTimestamp(); - long target = frameStartTimestamp + (long)(Stopwatch.Frequency / limit); + long target = frameStartTimestamp + (long)((double)Stopwatch.Frequency / limit); double remainingMs = (double)(target - now) * 1000.0 / Stopwatch.Frequency; if (remainingMs <= 0.0) { From b9dd78c0e262b7096d1c37fc74716e6faa20cde6 Mon Sep 17 00:00:00 2001 From: adfriz <76892624+adfriz@users.noreply.github.com> Date: Sun, 23 Aug 2026 23:03:08 +0700 Subject: [PATCH 4/7] Change: Only raise timer resolution when the frame limiter actually needs to wait --- source/OpenBveApi/System/Timers.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/source/OpenBveApi/System/Timers.cs b/source/OpenBveApi/System/Timers.cs index a5080f680..76006fa55 100644 --- a/source/OpenBveApi/System/Timers.cs +++ b/source/OpenBveApi/System/Timers.cs @@ -126,14 +126,16 @@ public static void ApplyLimit(int fpsLimit) { return; } - RaiseTimerResolution(); long now = Stopwatch.GetTimestamp(); long target = frameStartTimestamp + (long)((double)Stopwatch.Frequency / limit); double remainingMs = (double)(target - now) * 1000.0 / Stopwatch.Frequency; if (remainingMs <= 0.0) { + // The frame overran its timeslot (or was throttled by VSync) - nothing to wait for, + // so do not raise the system timer resolution needlessly return; } + RaiseTimerResolution(); double sleepMs = remainingMs - schedulerPeriod * Tolerance; int ticks = (int)(sleepMs / schedulerPeriod); if (ticks > 0) From 5657d313695c667ebb8f440751feb1690b4d4b08 Mon Sep 17 00:00:00 2001 From: adfriz <76892624+adfriz@users.noreply.github.com> Date: Sun, 23 Aug 2026 23:10:34 +0700 Subject: [PATCH 5/7] Change: Raise hard FPS cap to 1000 --- source/ObjectViewer/System/GameWindow.cs | 2 +- source/OpenBVE/System/GameWindow.cs | 2 +- source/OpenBveApi/System/Timers.cs | 2 +- source/RouteViewer/System/Gamewindow.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/source/ObjectViewer/System/GameWindow.cs b/source/ObjectViewer/System/GameWindow.cs index f291cb268..2873f5f2c 100644 --- a/source/ObjectViewer/System/GameWindow.cs +++ b/source/ObjectViewer/System/GameWindow.cs @@ -264,7 +264,7 @@ protected override void OnRenderFrame(FrameEventArgs e) Program.Renderer.Lighting.Initialize(); Program.Renderer.RenderScene(timeElapsed); SwapBuffers(); - // A hard cap of 540fps is always applied, even when unlimited is selected + // A hard cap of 1000fps is always applied, even when unlimited is selected FrameLimiter.ApplyLimit(Interface.CurrentOptions.FPSLimit); RenderRealTimeElapsed = 0.0; diff --git a/source/OpenBVE/System/GameWindow.cs b/source/OpenBVE/System/GameWindow.cs index 3840222d0..a5c030a66 100644 --- a/source/OpenBVE/System/GameWindow.cs +++ b/source/OpenBVE/System/GameWindow.cs @@ -310,7 +310,7 @@ protected override void OnRenderFrame(FrameEventArgs e) { Interface.CurrentOptions.BlackBox = false; } - // A hard cap of 540fps is always applied, even when unlimited is selected + // A hard cap of 1000fps is always applied, even when unlimited is selected FrameLimiter.ApplyLimit(Interface.CurrentOptions.FPSLimit); } diff --git a/source/OpenBveApi/System/Timers.cs b/source/OpenBveApi/System/Timers.cs index 76006fa55..2da05b941 100644 --- a/source/OpenBveApi/System/Timers.cs +++ b/source/OpenBveApi/System/Timers.cs @@ -99,7 +99,7 @@ public static class FrameLimiter // On Windows, raise the timer resolution to 1ms for accurate frame pacing private const uint WindowsTimerPeriod = 1; // Hard cap applied even when the user selects 'Unlimited' - private const int HardFpsLimit = 540; + private const int HardFpsLimit = 1000; private static bool timerResolutionRaised; private static int schedulerPeriod = 1; diff --git a/source/RouteViewer/System/Gamewindow.cs b/source/RouteViewer/System/Gamewindow.cs index cc9c364b4..8fcbe7556 100644 --- a/source/RouteViewer/System/Gamewindow.cs +++ b/source/RouteViewer/System/Gamewindow.cs @@ -85,7 +85,7 @@ protected override void OnRenderFrame(FrameEventArgs e) Program.Renderer.RenderScene(TimeElapsed); MessageManager.UpdateMessages(TimeElapsed); SwapBuffers(); - // A hard cap of 540fps is always applied, even when unlimited is selected + // A hard cap of 1000fps is always applied, even when unlimited is selected FrameLimiter.ApplyLimit(Interface.CurrentOptions.FPSLimit); } From 097e026d8891f87f7b6bf9d479ab312795e3b9d5 Mon Sep 17 00:00:00 2001 From: adfriz <76892624+adfriz@users.noreply.github.com> Date: Sun, 23 Aug 2026 23:15:36 +0700 Subject: [PATCH 6/7] Change: Skip frame pacing above ~330fps where sleep-based limiting is impractical --- source/ObjectViewer/System/GameWindow.cs | 3 ++- source/OpenBVE/System/GameWindow.cs | 3 ++- source/OpenBveApi/System/Timers.cs | 11 ++++++++++- source/RouteViewer/System/Gamewindow.cs | 3 ++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/source/ObjectViewer/System/GameWindow.cs b/source/ObjectViewer/System/GameWindow.cs index 2873f5f2c..57b82bfde 100644 --- a/source/ObjectViewer/System/GameWindow.cs +++ b/source/ObjectViewer/System/GameWindow.cs @@ -264,7 +264,8 @@ protected override void OnRenderFrame(FrameEventArgs e) Program.Renderer.Lighting.Initialize(); Program.Renderer.RenderScene(timeElapsed); SwapBuffers(); - // A hard cap of 1000fps is always applied, even when unlimited is selected + // Applies the FPS limit; a hard cap of 1000fps applies when unlimited is selected, + // although no pacing is performed above around 330fps as sleep granularity makes it impractical FrameLimiter.ApplyLimit(Interface.CurrentOptions.FPSLimit); RenderRealTimeElapsed = 0.0; diff --git a/source/OpenBVE/System/GameWindow.cs b/source/OpenBVE/System/GameWindow.cs index a5c030a66..8c7b6c600 100644 --- a/source/OpenBVE/System/GameWindow.cs +++ b/source/OpenBVE/System/GameWindow.cs @@ -310,7 +310,8 @@ protected override void OnRenderFrame(FrameEventArgs e) { Interface.CurrentOptions.BlackBox = false; } - // A hard cap of 1000fps is always applied, even when unlimited is selected + // Applies the FPS limit; a hard cap of 1000fps applies when unlimited is selected, + // although no pacing is performed above around 330fps as sleep granularity makes it impractical FrameLimiter.ApplyLimit(Interface.CurrentOptions.FPSLimit); } diff --git a/source/OpenBveApi/System/Timers.cs b/source/OpenBveApi/System/Timers.cs index 2da05b941..b39943aa6 100644 --- a/source/OpenBveApi/System/Timers.cs +++ b/source/OpenBveApi/System/Timers.cs @@ -100,6 +100,9 @@ public static class FrameLimiter private const uint WindowsTimerPeriod = 1; // Hard cap applied even when the user selects 'Unlimited' private const int HardFpsLimit = 1000; + // Frameslots shorter than this multiple of the scheduler period cannot be paced accurately by + // Thread.Sleep without a power-hungry busy spin, so limiting is skipped above around 330fps + private const double MinimumSlotPeriods = 3.0; private static bool timerResolutionRaised; private static int schedulerPeriod = 1; @@ -118,7 +121,8 @@ public static void StartFrame() } /// Waits until the end of the current frame's allotted timeslot. - /// The maximum frames per second selected by the user. A value of zero or less means unlimited, subject to the hard cap. + /// The maximum frames per second selected by the user. A value of zero or less means unlimited, subject to the hard cap. + /// Above around 330fps no limiting is performed, as sleep-based pacing is impractical at such frame rates. public static void ApplyLimit(int fpsLimit) { int limit = fpsLimit > 0 ? System.Math.Min(fpsLimit, HardFpsLimit) : HardFpsLimit; @@ -126,6 +130,11 @@ public static void ApplyLimit(int fpsLimit) { return; } + if ((double)Stopwatch.Frequency / limit < schedulerPeriod * MinimumSlotPeriods) + { + // The timeslot is too short to pace accurately without busy-spinning - skip limiting + return; + } long now = Stopwatch.GetTimestamp(); long target = frameStartTimestamp + (long)((double)Stopwatch.Frequency / limit); double remainingMs = (double)(target - now) * 1000.0 / Stopwatch.Frequency; diff --git a/source/RouteViewer/System/Gamewindow.cs b/source/RouteViewer/System/Gamewindow.cs index 8fcbe7556..bbf683aa7 100644 --- a/source/RouteViewer/System/Gamewindow.cs +++ b/source/RouteViewer/System/Gamewindow.cs @@ -85,7 +85,8 @@ protected override void OnRenderFrame(FrameEventArgs e) Program.Renderer.RenderScene(TimeElapsed); MessageManager.UpdateMessages(TimeElapsed); SwapBuffers(); - // A hard cap of 1000fps is always applied, even when unlimited is selected + // Applies the FPS limit; a hard cap of 1000fps applies when unlimited is selected, + // although no pacing is performed above around 330fps as sleep granularity makes it impractical FrameLimiter.ApplyLimit(Interface.CurrentOptions.FPSLimit); } From 985bd07fd187210856ac882f8c65323daeff3065 Mon Sep 17 00:00:00 2001 From: adfriz <76892624+adfriz@users.noreply.github.com> Date: Sun, 23 Aug 2026 23:23:20 +0700 Subject: [PATCH 7/7] New: Add 45 and 75 FPS presets to FPS limit option --- source/ObjectViewer/formOptions.Designer.cs | 2 ++ source/ObjectViewer/formOptions.cs | 12 +++++++----- source/OpenBVE/UserInterface/formMain.Designer.cs | 2 ++ source/OpenBVE/UserInterface/formMain.cs | 12 +++++++----- source/RouteViewer/formOptions.Designer.cs | 2 ++ source/RouteViewer/formOptions.cs | 12 +++++++----- 6 files changed, 27 insertions(+), 15 deletions(-) diff --git a/source/ObjectViewer/formOptions.Designer.cs b/source/ObjectViewer/formOptions.Designer.cs index e07a8743c..3c4ca9d7a 100644 --- a/source/ObjectViewer/formOptions.Designer.cs +++ b/source/ObjectViewer/formOptions.Designer.cs @@ -987,7 +987,9 @@ private void InitializeComponent() this.comboBoxFPSLimit.Items.AddRange(new object[] { "Unlimited", "30", + "45", "60", + "75", "120", "240"}); this.comboBoxFPSLimit.Location = new System.Drawing.Point(160, 279); diff --git a/source/ObjectViewer/formOptions.cs b/source/ObjectViewer/formOptions.cs index 32617bbfa..013ff30e0 100644 --- a/source/ObjectViewer/formOptions.cs +++ b/source/ObjectViewer/formOptions.cs @@ -90,13 +90,15 @@ private formOptions() // VSync and FPS Limit comboBoxVSync.SelectedIndex = Interface.CurrentOptions.VerticalSynchronization ? 1 : 0; - // Map FPSLimit value to combo index: 0=Unlimited, 1=30, 2=60, 3=120, 4=240 + // Map FPSLimit value to combo index: 0=Unlimited, 1=30, 2=45, 3=60, 4=75, 5=120, 6=240 switch (Interface.CurrentOptions.FPSLimit) { case 30: comboBoxFPSLimit.SelectedIndex = 1; break; - case 60: comboBoxFPSLimit.SelectedIndex = 2; break; - case 120: comboBoxFPSLimit.SelectedIndex = 3; break; - case 240: comboBoxFPSLimit.SelectedIndex = 4; break; + case 45: comboBoxFPSLimit.SelectedIndex = 2; break; + case 60: comboBoxFPSLimit.SelectedIndex = 3; break; + case 75: comboBoxFPSLimit.SelectedIndex = 4; break; + case 120: comboBoxFPSLimit.SelectedIndex = 5; break; + case 240: comboBoxFPSLimit.SelectedIndex = 6; break; default: comboBoxFPSLimit.SelectedIndex = 0; break; } UpdateFPSLimitEnabled(); @@ -277,7 +279,7 @@ private void CloseButton_Click(object sender, EventArgs e) // VSync and FPS Limit Interface.CurrentOptions.VerticalSynchronization = comboBoxVSync.SelectedIndex == 1; // Map combo index to FPSLimit value - int[] fpsPresets = { 0, 30, 60, 120, 240 }; + int[] fpsPresets = { 0, 30, 45, 60, 75, 120, 240 }; Interface.CurrentOptions.FPSLimit = comboBoxFPSLimit.SelectedIndex >= 0 ? fpsPresets[comboBoxFPSLimit.SelectedIndex] : 0; Program.Renderer.GameWindow.VSync = Interface.CurrentOptions.VerticalSynchronization ? OpenTK.VSyncMode.On : OpenTK.VSyncMode.Off; diff --git a/source/OpenBVE/UserInterface/formMain.Designer.cs b/source/OpenBVE/UserInterface/formMain.Designer.cs index 5eda0d59b..0f397b1b9 100644 --- a/source/OpenBVE/UserInterface/formMain.Designer.cs +++ b/source/OpenBVE/UserInterface/formMain.Designer.cs @@ -1718,7 +1718,9 @@ private void InitializeComponent() { this.comboBoxFPSLimit.Items.AddRange(new object[] { "Unlimited", "30", + "45", "60", + "75", "120", "240"}); this.comboBoxFPSLimit.Location = new System.Drawing.Point(156, 99); diff --git a/source/OpenBVE/UserInterface/formMain.cs b/source/OpenBVE/UserInterface/formMain.cs index 729452d8b..1f1af2ddd 100644 --- a/source/OpenBVE/UserInterface/formMain.cs +++ b/source/OpenBVE/UserInterface/formMain.cs @@ -387,13 +387,15 @@ private void formMain_Load(object sender, EventArgs e) comboboxVSync.Items.Add(""); comboboxVSync.Items.Add(""); comboboxVSync.SelectedIndex = Interface.CurrentOptions.VerticalSynchronization ? 1 : 0; - // Map FPSLimit value to combo index: 0=Unlimited, 1=30, 2=60, 3=120, 4=240 + // Map FPSLimit value to combo index: 0=Unlimited, 1=30, 2=45, 3=60, 4=75, 5=120, 6=240 switch (Interface.CurrentOptions.FPSLimit) { case 30: comboBoxFPSLimit.SelectedIndex = 1; break; - case 60: comboBoxFPSLimit.SelectedIndex = 2; break; - case 120: comboBoxFPSLimit.SelectedIndex = 3; break; - case 240: comboBoxFPSLimit.SelectedIndex = 4; break; + case 45: comboBoxFPSLimit.SelectedIndex = 2; break; + case 60: comboBoxFPSLimit.SelectedIndex = 3; break; + case 75: comboBoxFPSLimit.SelectedIndex = 4; break; + case 120: comboBoxFPSLimit.SelectedIndex = 5; break; + case 240: comboBoxFPSLimit.SelectedIndex = 6; break; default: comboBoxFPSLimit.SelectedIndex = 0; break; } UpdateFPSLimitEnabled(); @@ -1243,7 +1245,7 @@ private void formMain_FormClosing() Interface.CurrentOptions.FullscreenMode = radiobuttonFullscreen.Checked; Interface.CurrentOptions.VerticalSynchronization = comboboxVSync.SelectedIndex == 1; // Map combo index to FPSLimit value - int[] fpsPresets = { 0, 30, 60, 120, 240 }; + int[] fpsPresets = { 0, 30, 45, 60, 75, 120, 240 }; Interface.CurrentOptions.FPSLimit = comboBoxFPSLimit.SelectedIndex >= 0 ? fpsPresets[comboBoxFPSLimit.SelectedIndex] : 0; Interface.CurrentOptions.WindowWidth = (int)Math.Round(updownWindowWidth.Value); Interface.CurrentOptions.WindowHeight = (int)Math.Round(updownWindowHeight.Value); diff --git a/source/RouteViewer/formOptions.Designer.cs b/source/RouteViewer/formOptions.Designer.cs index 3fdd49873..ba525dcc4 100644 --- a/source/RouteViewer/formOptions.Designer.cs +++ b/source/RouteViewer/formOptions.Designer.cs @@ -963,7 +963,9 @@ private void InitializeComponent() this.comboBoxFPSLimit.Items.AddRange(new object[] { "Unlimited", "30", + "45", "60", + "75", "120", "240"}); this.comboBoxFPSLimit.Location = new System.Drawing.Point(160, 495); diff --git a/source/RouteViewer/formOptions.cs b/source/RouteViewer/formOptions.cs index 5f1f46d23..517408a50 100644 --- a/source/RouteViewer/formOptions.cs +++ b/source/RouteViewer/formOptions.cs @@ -93,13 +93,15 @@ public FormOptions() // VSync and FPS Limit comboBoxVSync.SelectedIndex = Interface.CurrentOptions.VerticalSynchronization ? 1 : 0; - // Map FPSLimit value to combo index: 0=Unlimited, 1=30, 2=60, 3=120, 4=240 + // Map FPSLimit value to combo index: 0=Unlimited, 1=30, 2=45, 3=60, 4=75, 5=120, 6=240 switch (Interface.CurrentOptions.FPSLimit) { case 30: comboBoxFPSLimit.SelectedIndex = 1; break; - case 60: comboBoxFPSLimit.SelectedIndex = 2; break; - case 120: comboBoxFPSLimit.SelectedIndex = 3; break; - case 240: comboBoxFPSLimit.SelectedIndex = 4; break; + case 45: comboBoxFPSLimit.SelectedIndex = 2; break; + case 60: comboBoxFPSLimit.SelectedIndex = 3; break; + case 75: comboBoxFPSLimit.SelectedIndex = 4; break; + case 120: comboBoxFPSLimit.SelectedIndex = 5; break; + case 240: comboBoxFPSLimit.SelectedIndex = 6; break; default: comboBoxFPSLimit.SelectedIndex = 0; break; } UpdateFPSLimitEnabled(); @@ -324,7 +326,7 @@ private void button1_Click(object sender, EventArgs e) // VSync and FPS Limit Interface.CurrentOptions.VerticalSynchronization = comboBoxVSync.SelectedIndex == 1; // Map combo index to FPSLimit value - int[] fpsPresets = { 0, 30, 60, 120, 240 }; + int[] fpsPresets = { 0, 30, 45, 60, 75, 120, 240 }; Interface.CurrentOptions.FPSLimit = comboBoxFPSLimit.SelectedIndex >= 0 ? fpsPresets[comboBoxFPSLimit.SelectedIndex] : 0; Program.Renderer.GameWindow.VSync = Interface.CurrentOptions.VerticalSynchronization ? OpenTK.VSyncMode.On : OpenTK.VSyncMode.Off;