精确捕获循环
Precise capture loop
问题
我正在使用 FPS/FPM/FPH(秒、分钟、小时)控件实现捕获循环。这意味着用户可以正常或延时拍摄某些内容。
这是我的代码:
private System.Threading.CancellationTokenSource _captureToken;
private List<long> _timeList = new List<long>();
private void CaptureRun(int interval)
{
var sw = new Stopwatch();
while (_captureToken != null && !_captureToken.IsCancellationRequested)
{
sw.Restart();
//Capture happens here...
//With or without my capture code, the result is the same (the difference in average time).
//So I removed this part of the code to make it easier to understand.
//If behind wait time, wait before capturing again.
if (sw.ElapsedMilliseconds < interval)
System.Threading.SpinWait.SpinUntil(() => sw.ElapsedMilliseconds >= interval);
_timeList.Add(sw.ElapsedMilliseconds);
}
}
//Code that starts the capture (simplified).
private void StopCapture()
{
_captureToken = new System.Threading.CancellationTokenSource();
Task.Run(() => CaptureRun(16), _captureToken.Token);
}
//Code that stops the capture.
private void StopCapture()
{
if (_captureToken != null)
{
_captureToken.Cancel();
_captureToken.Dispose();
_captureToken = null;
}
}
问题在于,如果将间隔设置为 60 FPS(16 毫秒),则生成的捕获时间平均为 30 毫秒。但是,如果我将捕获间隔设置为 15 毫秒(> 60 FPS),它会按预期平均为 15 毫秒。
我想知道为什么会这样,是否可以改进代码。
解决方案
根据 Alois 的评论,我设法创建了这个扩展 class:
internal class TimerResolution : IDisposable
{
#region Native
[StructLayout(LayoutKind.Sequential)]
private readonly struct TimeCaps
{
internal readonly uint MinimumResolution;
internal readonly uint MaximumResolution;
};
internal enum TimerResult : uint
{
NoError = 0,
NoCanDo = 97
}
[DllImport("winmm.dll", EntryPoint = "timeGetDevCaps", SetLastError = true)]
private static extern uint GetDevCaps(ref TimeCaps timeCaps, uint sizeTimeCaps);
[DllImport("ntdll.dll", EntryPoint = "NtQueryTimerResolution", SetLastError = true)]
private static extern int QueryTimerResolution(out int maximumResolution, out int minimumResolution, out int currentResolution);
[DllImport("winmm.dll", EntryPoint = "timeBeginPeriod")]
internal static extern uint BeginPeriod(uint uMilliseconds);
[DllImport("winmm.dll", EntryPoint = "timeGetTime")]
internal static extern uint GetTime();
[DllImport("winmm.dll", EntryPoint = "timeEndPeriod")]
internal static extern uint EndPeriod(uint uMilliseconds);
#endregion
#region Properties
/// <summary>
/// The target resolution in milliseconds.
/// </summary>
public uint TargetResolution { get; private set; }
/// <summary>
/// The current resolution in milliseconds.
/// May differ from target resolution based on system limitation.
/// </summary>
public uint CurrentResolution { get; private set; }
/// <summary>
/// True if a new resolution was set (target resolution or not).
/// </summary>
public bool SuccessfullySetResolution { get; private set; }
/// <summary>
/// True if a new target resolution was set.
/// </summary>
public bool SuccessfullySetTargetResolution { get; private set; }
#endregion
/// <summary>
/// Tries setting a given target timer resolution to the current thread.
/// If the selected resolution can be set, a nearby value will be set instead.
/// This must be disposed afterwards (or call EndPeriod() passing the CurrentResolution)
/// </summary>
/// <param name="targetResolution">The target resolution in milliseconds.</param>
public TimerResolution(int targetResolution)
{
TargetResolution = (uint) targetResolution;
//Get system limits.
var timeCaps = new TimeCaps();
if (GetDevCaps(ref timeCaps, (uint) Marshal.SizeOf(typeof(TimeCaps))) != (uint) TimerResult.NoError)
return;
//Calculates resolution based on system limits.
CurrentResolution = Math.Min(Math.Max(timeCaps.MinimumResolution, TargetResolution), timeCaps.MaximumResolution);
//Begins the period in which the thread will run on this new timer resolution.
if (BeginPeriod(CurrentResolution) != (uint) TimerResult.NoError)
return;
SuccessfullySetResolution = true;
if (CurrentResolution == TargetResolution)
SuccessfullySetTargetResolution = true;
}
public void Dispose()
{
if (SuccessfullySetResolution)
EndPeriod(CurrentResolution);
}
}
只需在 using
块中使用扩展名 class 并将其放入您想要的任何内容中 运行 在另一个计时器分辨率中:
using (var resolution = new TimerResolution(1))
{
//...
}
休眠 15 毫秒时延迟 15 毫秒,休眠 16 毫秒时延迟 30 毫秒,因为 SpinWait 在后台使用 Environment.TickCount,它依赖于系统时钟,显然在您的系统上有 15 毫秒的分辨率.
您可以使用 timeBeginPeriod 设置系统范围内的计时器分辨率。
参见
和
有关时钟分辨率的更多详细信息。您可以使用 sysinternals 的 clockres 检查当前系统范围的计时器分辨率。
查看示例输出:
C:>clockres
Clockres v2.1 - Clock resolution display utility
Copyright (C) 2016 Mark Russinovich
Sysinternals
Maximum timer interval: 15.625 ms
Minimum timer interval: 0.500 ms
**Current timer interval: 15.625 ms**
当 WPF 应用程序 运行(例如 Visual Studio)
Maximum timer interval: 15.625 ms
Minimum timer interval: 0.500 ms
**Current timer interval: 1.000 ms**
那么您将获得 1 毫秒的分辨率,因为每个 WPF 应用程序都会将时钟分辨率更改为 1 毫秒。这也被一些人用作“解决”问题的解决方法。
问题
我正在使用 FPS/FPM/FPH(秒、分钟、小时)控件实现捕获循环。这意味着用户可以正常或延时拍摄某些内容。
这是我的代码:
private System.Threading.CancellationTokenSource _captureToken;
private List<long> _timeList = new List<long>();
private void CaptureRun(int interval)
{
var sw = new Stopwatch();
while (_captureToken != null && !_captureToken.IsCancellationRequested)
{
sw.Restart();
//Capture happens here...
//With or without my capture code, the result is the same (the difference in average time).
//So I removed this part of the code to make it easier to understand.
//If behind wait time, wait before capturing again.
if (sw.ElapsedMilliseconds < interval)
System.Threading.SpinWait.SpinUntil(() => sw.ElapsedMilliseconds >= interval);
_timeList.Add(sw.ElapsedMilliseconds);
}
}
//Code that starts the capture (simplified).
private void StopCapture()
{
_captureToken = new System.Threading.CancellationTokenSource();
Task.Run(() => CaptureRun(16), _captureToken.Token);
}
//Code that stops the capture.
private void StopCapture()
{
if (_captureToken != null)
{
_captureToken.Cancel();
_captureToken.Dispose();
_captureToken = null;
}
}
问题在于,如果将间隔设置为 60 FPS(16 毫秒),则生成的捕获时间平均为 30 毫秒。但是,如果我将捕获间隔设置为 15 毫秒(> 60 FPS),它会按预期平均为 15 毫秒。
我想知道为什么会这样,是否可以改进代码。
解决方案
根据 Alois 的评论,我设法创建了这个扩展 class:
internal class TimerResolution : IDisposable
{
#region Native
[StructLayout(LayoutKind.Sequential)]
private readonly struct TimeCaps
{
internal readonly uint MinimumResolution;
internal readonly uint MaximumResolution;
};
internal enum TimerResult : uint
{
NoError = 0,
NoCanDo = 97
}
[DllImport("winmm.dll", EntryPoint = "timeGetDevCaps", SetLastError = true)]
private static extern uint GetDevCaps(ref TimeCaps timeCaps, uint sizeTimeCaps);
[DllImport("ntdll.dll", EntryPoint = "NtQueryTimerResolution", SetLastError = true)]
private static extern int QueryTimerResolution(out int maximumResolution, out int minimumResolution, out int currentResolution);
[DllImport("winmm.dll", EntryPoint = "timeBeginPeriod")]
internal static extern uint BeginPeriod(uint uMilliseconds);
[DllImport("winmm.dll", EntryPoint = "timeGetTime")]
internal static extern uint GetTime();
[DllImport("winmm.dll", EntryPoint = "timeEndPeriod")]
internal static extern uint EndPeriod(uint uMilliseconds);
#endregion
#region Properties
/// <summary>
/// The target resolution in milliseconds.
/// </summary>
public uint TargetResolution { get; private set; }
/// <summary>
/// The current resolution in milliseconds.
/// May differ from target resolution based on system limitation.
/// </summary>
public uint CurrentResolution { get; private set; }
/// <summary>
/// True if a new resolution was set (target resolution or not).
/// </summary>
public bool SuccessfullySetResolution { get; private set; }
/// <summary>
/// True if a new target resolution was set.
/// </summary>
public bool SuccessfullySetTargetResolution { get; private set; }
#endregion
/// <summary>
/// Tries setting a given target timer resolution to the current thread.
/// If the selected resolution can be set, a nearby value will be set instead.
/// This must be disposed afterwards (or call EndPeriod() passing the CurrentResolution)
/// </summary>
/// <param name="targetResolution">The target resolution in milliseconds.</param>
public TimerResolution(int targetResolution)
{
TargetResolution = (uint) targetResolution;
//Get system limits.
var timeCaps = new TimeCaps();
if (GetDevCaps(ref timeCaps, (uint) Marshal.SizeOf(typeof(TimeCaps))) != (uint) TimerResult.NoError)
return;
//Calculates resolution based on system limits.
CurrentResolution = Math.Min(Math.Max(timeCaps.MinimumResolution, TargetResolution), timeCaps.MaximumResolution);
//Begins the period in which the thread will run on this new timer resolution.
if (BeginPeriod(CurrentResolution) != (uint) TimerResult.NoError)
return;
SuccessfullySetResolution = true;
if (CurrentResolution == TargetResolution)
SuccessfullySetTargetResolution = true;
}
public void Dispose()
{
if (SuccessfullySetResolution)
EndPeriod(CurrentResolution);
}
}
只需在 using
块中使用扩展名 class 并将其放入您想要的任何内容中 运行 在另一个计时器分辨率中:
using (var resolution = new TimerResolution(1))
{
//...
}
休眠 15 毫秒时延迟 15 毫秒,休眠 16 毫秒时延迟 30 毫秒,因为 SpinWait 在后台使用 Environment.TickCount,它依赖于系统时钟,显然在您的系统上有 15 毫秒的分辨率. 您可以使用 timeBeginPeriod 设置系统范围内的计时器分辨率。 参见
和
有关时钟分辨率的更多详细信息。您可以使用 sysinternals 的 clockres 检查当前系统范围的计时器分辨率。
查看示例输出:
C:>clockres
Clockres v2.1 - Clock resolution display utility
Copyright (C) 2016 Mark Russinovich
Sysinternals
Maximum timer interval: 15.625 ms
Minimum timer interval: 0.500 ms
**Current timer interval: 15.625 ms**
当 WPF 应用程序 运行(例如 Visual Studio)
Maximum timer interval: 15.625 ms
Minimum timer interval: 0.500 ms
**Current timer interval: 1.000 ms**
那么您将获得 1 毫秒的分辨率,因为每个 WPF 应用程序都会将时钟分辨率更改为 1 毫秒。这也被一些人用作“解决”问题的解决方法。