Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions source/ObjectViewer/ProgramS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions source/ObjectViewer/System/GameWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -263,6 +264,9 @@ protected override void OnRenderFrame(FrameEventArgs e)
Program.Renderer.Lighting.Initialize();
Program.Renderer.RenderScene(timeElapsed);
SwapBuffers();
// 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;
}
Expand Down Expand Up @@ -327,6 +331,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);
Expand Down
2 changes: 2 additions & 0 deletions source/ObjectViewer/formOptions.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 7 additions & 6 deletions source/ObjectViewer/formOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -277,10 +279,9 @@ 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;
Program.Renderer.GameWindow.TargetRenderFrequency = Interface.CurrentOptions.FPSLimit > 0 ? Interface.CurrentOptions.FPSLimit : 0;

// Saving shadow settings
switch (comboBoxShadowResolution.SelectedIndex)
Expand Down
7 changes: 4 additions & 3 deletions source/OpenBVE/Graphics/Screen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

}
Expand Down
5 changes: 5 additions & 0 deletions source/OpenBVE/System/GameWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -309,6 +310,9 @@ protected override void OnRenderFrame(FrameEventArgs e)
{
Interface.CurrentOptions.BlackBox = false;
}
// 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);
}

protected override void OnUpdateFrame(FrameEventArgs e)
Expand Down Expand Up @@ -547,6 +551,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++)
{
Expand Down
2 changes: 2 additions & 0 deletions source/OpenBVE/UserInterface/formMain.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 7 additions & 5 deletions source/OpenBVE/UserInterface/formMain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down
114 changes: 114 additions & 0 deletions source/OpenBveApi/System/Timers.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;

namespace OpenBveApi
{
Expand Down Expand Up @@ -85,4 +86,117 @@ public static int GetClockTicks()


}

/// <summary>
/// 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.
/// </summary>
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, 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 = 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;
private static long frameStartTimestamp;

[DllImport("winmm")]
private static extern uint timeBeginPeriod(uint uPeriod);

[DllImport("winmm")]
private static extern uint timeEndPeriod(uint uPeriod);

/// <summary>Marks the start of a frame. Must be called once at the beginning of each rendered frame.</summary>
public static void StartFrame()
{
frameStartTimestamp = Stopwatch.GetTimestamp();
}

/// <summary>Waits until the end of the current frame's allotted timeslot.</summary>
/// <param name="fpsLimit">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.</param>
public static void ApplyLimit(int fpsLimit)
{
int limit = fpsLimit > 0 ? System.Math.Min(fpsLimit, HardFpsLimit) : HardFpsLimit;
if (frameStartTimestamp == 0)
{
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;
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)
{
Thread.Sleep(ticks * schedulerPeriod);
}
while (Stopwatch.GetTimestamp() < target)
{
Thread.Yield();
}
}

/// <summary>Restores the system timer resolution, if it was previously raised.</summary>
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;
}
}
}
}
}
4 changes: 0 additions & 4 deletions source/RouteViewer/ProgramR.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions source/RouteViewer/System/Gamewindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -83,8 +84,10 @@ 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();
// 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);
}

protected override void OnResize(EventArgs e)
Expand Down Expand Up @@ -144,6 +147,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
Expand Down
2 changes: 2 additions & 0 deletions source/RouteViewer/formOptions.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 7 additions & 6 deletions source/RouteViewer/formOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -324,10 +326,9 @@ 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;
Program.Renderer.GameWindow.TargetRenderFrequency = Interface.CurrentOptions.FPSLimit > 0 ? Interface.CurrentOptions.FPSLimit : 0;

// Sun direction is already updated in real-time via slider events

Expand Down