diff --git a/RGB.NET.Core/Helper/TimerHelper.cs b/RGB.NET.Core/Helper/TimerHelper.cs
new file mode 100644
index 0000000..fac9476
--- /dev/null
+++ b/RGB.NET.Core/Helper/TimerHelper.cs
@@ -0,0 +1,179 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Threading;
+
+namespace RGB.NET.Core;
+
+public static class TimerHelper
+{
+ #region DLL-Imports
+
+ [DllImport("winmm.dll", EntryPoint = "timeBeginPeriod")]
+ private static extern void TimeBeginPeriod(int t);
+
+ [DllImport("winmm.dll", EntryPoint = "timeEndPeriod")]
+ private static extern void TimeEndPeriod(int t);
+
+ #endregion
+
+ #region Properties & Fields
+
+ private static readonly object HIGH_RESOLUTION_TIMER_LOCK = new();
+
+ private static bool _areHighResolutionTimersEnabled = false;
+ private static int _highResolutionTimerUsers = 0;
+
+ private static bool _useHighResolutionTimers = true;
+ ///
+ /// Gets or sets if High Resolution Timers should be used.
+ ///
+ public static bool UseHighResolutionTimers
+ {
+ get => _useHighResolutionTimers;
+ set
+ {
+ lock (HIGH_RESOLUTION_TIMER_LOCK)
+ {
+ _useHighResolutionTimers = value;
+ CheckHighResolutionTimerUsage();
+ }
+ }
+ }
+
+ // ReSharper disable once InconsistentNaming
+ private static readonly HashSet _timerLeases = new();
+
+ #endregion
+
+ #region Methods
+
+ ///
+ /// Executes the provided action and blocks if needed until the the has passed.
+ ///
+ /// The action to execute.
+ /// The time in ms this method should block. default: 0
+ /// The time in ms spent executing the .
+ public static double Execute(Action action, double targetExecuteTime = 0)
+ {
+ long preUpdateTicks = Stopwatch.GetTimestamp();
+
+ action();
+
+ double updateTime = GetElapsedTime(preUpdateTicks);
+
+ if (targetExecuteTime > 0)
+ {
+ int sleep = (int)(targetExecuteTime - updateTime);
+ if (sleep > 0)
+ Thread.Sleep(sleep);
+ }
+
+ return updateTime;
+ }
+
+ ///
+ /// Calculates the ellapsed time in ms from the provided timestamp until now.
+ ///
+ /// The initial timestamp to calculate the time from.
+ /// The elapsed time in ms.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static double GetElapsedTime(long initialTimestamp) => ((Stopwatch.GetTimestamp() - initialTimestamp) / (double)TimeSpan.TicksPerMillisecond);
+
+ ///
+ /// Requests to use to use High Resolution Timers if enabled.
+ /// IMPORTANT: Always dispose the returned disposable if High Resolution Timers are no longer needed for the caller.
+ ///
+ /// A disposable to remove the request.
+ public static IDisposable RequestHighResolutionTimer()
+ {
+ lock (HIGH_RESOLUTION_TIMER_LOCK)
+ {
+ _highResolutionTimerUsers++;
+ CheckHighResolutionTimerUsage();
+ }
+
+ HighResolutionTimerDisposable timerLease = new();
+ _timerLeases.Add(timerLease);
+ return timerLease;
+ }
+
+ private static void CheckHighResolutionTimerUsage()
+ {
+ if (UseHighResolutionTimers && (_highResolutionTimerUsers > 0))
+ EnableHighResolutionTimers();
+ else
+ DisableHighResolutionTimers();
+ }
+
+ private static void EnableHighResolutionTimers()
+ {
+ lock (HIGH_RESOLUTION_TIMER_LOCK)
+ {
+ if (_areHighResolutionTimersEnabled) return;
+
+ // DarthAffe 06.05.2022: Linux should use 1ms timers by default
+ if (OperatingSystem.IsWindows())
+ TimeBeginPeriod(1);
+
+ _areHighResolutionTimersEnabled = true;
+ }
+ }
+
+ private static void DisableHighResolutionTimers()
+ {
+ lock (HIGH_RESOLUTION_TIMER_LOCK)
+ {
+ if (!_areHighResolutionTimersEnabled) return;
+
+ if (OperatingSystem.IsWindows())
+ TimeEndPeriod(1);
+
+ _areHighResolutionTimersEnabled = false;
+ }
+ }
+
+ ///
+ /// Disposes all open High Resolution Timer Requests.
+ /// This should be called once when exiting the application to make sure nothing remains open and the application correctly unregisters itself on OS level.
+ /// Shouldn't be needed if everything is disposed, but better safe then sorry.
+ ///
+ public static void DisposeAllHighResolutionTimerRequests()
+ {
+ List timerLeases = _timerLeases.ToList();
+ foreach (HighResolutionTimerDisposable timer in timerLeases)
+ timer.Dispose();
+ }
+
+ #endregion
+
+ private class HighResolutionTimerDisposable : IDisposable
+ {
+ #region Properties & Fields
+
+ private bool _isDisposed = false;
+
+ #endregion
+
+ #region Methods
+
+ public void Dispose()
+ {
+ if (_isDisposed) return;
+
+ _isDisposed = true;
+
+ lock (HIGH_RESOLUTION_TIMER_LOCK)
+ {
+ _timerLeases.Remove(this);
+ _highResolutionTimerUsers--;
+ CheckHighResolutionTimerUsage();
+ }
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/RGB.NET.Core/Update/Devices/DeviceUpdateTrigger.cs b/RGB.NET.Core/Update/Devices/DeviceUpdateTrigger.cs
index d2ce849..6868a87 100644
--- a/RGB.NET.Core/Update/Devices/DeviceUpdateTrigger.cs
+++ b/RGB.NET.Core/Update/Devices/DeviceUpdateTrigger.cs
@@ -1,6 +1,5 @@
// ReSharper disable MemberCanBePrivate.Global
-using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
@@ -142,25 +141,10 @@ public class DeviceUpdateTrigger : AbstractUpdateTrigger, IDeviceUpdateTrigger
{
OnStartup();
- while (!UpdateToken.IsCancellationRequested)
- {
- if (HasDataEvent.WaitOne(Timeout))
- {
- long preUpdateTicks = Stopwatch.GetTimestamp();
-
- OnUpdate();
-
- double lastUpdateTime = ((Stopwatch.GetTimestamp() - preUpdateTicks) / 10000.0);
- LastUpdateTime = lastUpdateTime;
-
- if (UpdateFrequency > 0)
- {
- int sleep = (int)((UpdateFrequency * 1000.0) - lastUpdateTime);
- if (sleep > 0)
- Thread.Sleep(sleep);
- }
- }
- }
+ using (TimerHelper.RequestHighResolutionTimer())
+ while (!UpdateToken.IsCancellationRequested)
+ if (HasDataEvent.WaitOne(Timeout))
+ LastUpdateTime = TimerHelper.Execute(() => OnUpdate(), UpdateFrequency * 1000);
}
///
diff --git a/RGB.NET.Core/Update/ManualUpdateTrigger.cs b/RGB.NET.Core/Update/ManualUpdateTrigger.cs
index f9b315c..67e8ca4 100644
--- a/RGB.NET.Core/Update/ManualUpdateTrigger.cs
+++ b/RGB.NET.Core/Update/ManualUpdateTrigger.cs
@@ -1,6 +1,5 @@
// ReSharper disable MemberCanBePrivate.Global
-using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
@@ -86,11 +85,7 @@ public sealed class ManualUpdateTrigger : AbstractUpdateTrigger
while (!UpdateToken.IsCancellationRequested)
{
if (_mutex.WaitOne(100))
- {
- long preUpdateTicks = Stopwatch.GetTimestamp();
- OnUpdate(_customUpdateData);
- LastUpdateTime = ((Stopwatch.GetTimestamp() - preUpdateTicks) / 10000.0);
- }
+ LastUpdateTime = TimerHelper.Execute(() => OnUpdate(_customUpdateData));
}
}
diff --git a/RGB.NET.Core/Update/TimerUpdateTrigger.cs b/RGB.NET.Core/Update/TimerUpdateTrigger.cs
index d15f049..14a7e9f 100644
--- a/RGB.NET.Core/Update/TimerUpdateTrigger.cs
+++ b/RGB.NET.Core/Update/TimerUpdateTrigger.cs
@@ -1,7 +1,6 @@
// ReSharper disable MemberCanBePrivate.Global
using System;
-using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
@@ -130,21 +129,10 @@ public class TimerUpdateTrigger : AbstractUpdateTrigger
{
OnStartup();
- while (!UpdateToken.IsCancellationRequested)
- {
- long preUpdateTicks = Stopwatch.GetTimestamp();
+ using (TimerHelper.RequestHighResolutionTimer())
+ while (!UpdateToken.IsCancellationRequested)
+ LastUpdateTime = TimerHelper.Execute(() => OnUpdate(_customUpdateData), UpdateFrequency * 1000);
- OnUpdate(_customUpdateData);
-
- if (UpdateFrequency > 0)
- {
- double lastUpdateTime = ((Stopwatch.GetTimestamp() - preUpdateTicks) / 10000.0);
- LastUpdateTime = lastUpdateTime;
- int sleep = (int)((UpdateFrequency * 1000.0) - lastUpdateTime);
- if (sleep > 0)
- Thread.Sleep(sleep);
- }
- }
}
///