精确捕获循环

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 毫秒。这也被一些人用作“解决”问题的解决方法。