Skip to content

精细等待过程与循环内 FPS 精细调节过程


  • 使用精细等待过程改善 SLEEP 功能。

  • 使用循环内精细调节过程控制 FPS(每秒帧数)。

前言:

在用户过程中正确使用 SLEEP 和 TIMER 关键字,需要充分了解它们的限制和行为。

SLEEP 关键字

通常 SLEEP 功能在延迟精度方面表现不佳,且不能产生非常短的等待时间。

SLEEP 的精度因操作系统周期时间而异:

Windows NT/2K/XP:15 ms,9x/Me:50 ms,Linux:10 ms,DOS:55 ms。

使用低于这些精度值的延迟值时,SLEEP 无法产生对应的等待时间(实际等待时间总会更长,且大约等于这些精度值),但延迟值 '0' 除外。

TIMER 关键字

TIMER 返回时间的精度远高于 SLEEP 关键字通常的等待生成精度:

对于现代处理器,预期可达亚微秒级精度。

在某些平台上(Windows 和 Linux 除外),TIMER 的返回值会在午夜重置,因此如果开始时间和结束时间分别位于重置点两侧,若忽略此事件,某些程序可能会出现意外行为。

1. 克服 SLEEP 和 TIMER 关键字不利行为的原理

为了实现基于 SLEEP 和 TIMER 关键字的有效时间管理过程,必须克服上述某些不良行为。

产生精确延迟等待功能的原理

使用 SLEEP 不会占用 CPU,但精度较低,且与非常短的延迟不兼容。

使用循环同时测试 TIMER 值可以产生良好的精度,但会占用 CPU。

原理是将以下两部分串联:

  • 对请求延迟的第一部分使用 SLEEP 的粗粒度延迟(不占用 CPU),

  • 然后对剩余时间使用通过循环测试 TIMER 值的精细粒度延迟,直到达到目标值。

时间阈值使得在这两种操作类型之间进行切换成为可能。

该阈值的典型设置对应于操作系统周期时间的 2 倍。

这个值代表了 CPU 负载与延迟精度之间的权衡。

补偿 TIMER 返回值可能重置的原理

通过测试两次 TIMER 调用之间返回值的差的符号,来检测 TIMER 在午夜可能发生的重置。

如果第二个值小于第一个值(由于 TIMER 重置),则对第一个值应用负时间补偿,对应于以秒表示的一天时长。

2. 过程体(不含声明)描述

此处描述 3 个过程:

  • delay() 过程 => 生成精确时间的等待(以 ms 为单位的浮点值)

  • regulate() 过程 => 精细控制和调节循环频率(以整数值表示的每秒循环次数)

  • framerate() 过程 => 测量瞬时循环频率的工具(以整数值表示的每秒循环次数)

前两个过程非常适合精细调节和/或控制图像刷新的 FPS(每秒帧数)。

delay() 更适合偶尔的一次性使用,而 regulate() 更适合插入循环中以通常调整其 FPS。

regulate()framerate() 不是线程安全的,因为它们各自使用一个 Static 变量。

注意:与任何等待功能一样,强烈建议不要在屏幕锁定时使用 delay()regulate(),因此不要在 [ScreenLock...ScreenUnlock] 块内调用。

delay() 过程体(使用精细粒度等待改善 SLEEP 功能)

delay() 子程序用于生成精确的等待功能,比 SLEEP 关键字提供的更精确,且也与非常短的等待时间兼容。

如果请求的等待 amount 大于 threshold(+ 0.5),则使用 SLEEP 关键字执行请求等待时间的第一部分(amount - threshold)(不占用 CPU),剩余时间通过在循环中测试 TIMER 的精确值直到达到目标值来生成。

还会测试 TIMER 的可能重置,以便在发生时进行补偿。

start GeSHi

vb
Sub delay(ByVal amount As Single, ByVal threshold As Ulong)
    '' 'amount'  : requested temporisation to apply, in milliseconds
    '' 'thresold' : fixing threshold for fine-grain temporisation (by waiting loop), in milliseconds
    Dim As Double t1 = Timer
    Dim As Double t2
    Dim As Double t3 = t1 + amount / 1000
    If amount > threshold + 0.5 Then Sleep amount - threshold, 1
    Do
    #if Not defined(__FB_WIN32__) And Not defined(__FB_LINUX__)
        t2 = Timer
        If t2 < t1 Then t1 -= 24 * 60 * 60 : t3 -= 24 * 60 * 60
    Loop Until t2 >= t3
    #else
    Loop Until Timer >= t3
    #endif
End Sub

end GeSHi

当从要包含的 "delay_regulate_framerate.bi" 文件调用上面的 delay() 子程序时(参见下面第 3 段中的定义),在子程序体前面添加的声明将其第二个参数(threshold)定义为可选参数并设置其默认值。

regulate() 过程体(使用循环内调节精细控制 FPS)

regulate() 函数是围绕 delay() 子程序构建的,但其中要应用的等待时间(如果还有剩余)从请求的帧周期(FPS 的倒数)减去自上次调用以来已经过去的时间中扣除。

出于调试目的,regulate() 函数返回其应用的延迟(添加到初始循环的延迟)。如果用户不希望使用此调试数据,则可以简单地将该函数作为子程序调用。

如果返回的延迟值非常小,这意味着越来越难以达到 FPS 设定值。否则,返回延迟的波动(假设调节非常精确)代表了循环中用户代码从帧到帧执行的波动。

start GeSHi

vb
Function regulate(ByVal MyFps As Ulong, ByVal threshold As Ulong) As Single
    '' 'MyFps' : requested FPS value, in frames per second
    '' function return : applied delay (for debug), in milliseconds
    '' 'thresold' : fixing threshold for fine-grain temporisation (by waiting loop), in milliseconds
    Static As Double t1
    Dim As Single tf = 1 / MyFps
    Dim As Double t2 = Timer
    #if Not defined(__FB_WIN32__) And Not defined(__FB_LINUX__)
    If t2 < t1 Then t1 -= 24 * 60 * 60
    #endif
    Dim As Single dt = (tf - (t2 - t1)) * 1000
    delay(dt, threshold)
    t1 = Timer
    Return dt
End Function

end GeSHi

当从要包含的 "delay_regulate_framerate.bi" 文件调用上面的 regulate() 函数时(参见下面第 3 段中的定义),在函数体前面添加的声明将其第二个参数(threshold)定义为可选参数并设置其默认值。

framerate() 过程体(FPS 测量工具)

出于调试目的,可以通过在循环中的任意位置调用以下非常简单的工具函数 framerate(),提供 FPS 的瞬时测量(通过测量帧时间并计算其倒数)。

如果 framerate() 紧邻 regulate() 放置,则可测量调节的内在精度。

如果 framerate() 放置在程序循环的其他位置,则可能还会受到程序执行从帧到帧波动的额外影响。

start GeSHi

vb
Function framerate() As Ulong
    '' function return : measured FPS value (for debug), in frames per second
    Static As Double t1
    Dim As Double t2 = Timer
    #if Not defined(__FB_WIN32__) And Not defined(__FB_LINUX__)
    If t2 < t1 Then t1 -= 24 * 60 * 60
    #endif
    Dim As Ulong tf = 1 / (t2 - t1)
    t1 = t2
    Return tf
End Function

end GeSHi

delay()regulate() 不同,上面的 framerate() 函数即使在屏幕锁定时也可以使用,因此即使在 [ScreenLock...ScreenUnlock] 块内也可以调用。

3. 包含所有声明和过程体的完整源代码

头部包含上述 3 个过程体的声明。

对于 delay()regulate(),这些声明允许将其第二个参数声明为可选参数,并根据所用平台(Windows、Linux、DOS 或其他)设置默认值。

要包含的文件:"delay_regulate_framerate.bi"

start GeSHi

vb
'  delay_regulate_framerate.bi

#if defined(__FB_WIN32__)
Declare Sub delay(ByVal amount As Single, ByVal threshold As Ulong = 2 * 16)
Declare Function regulate(ByVal MyFps As Ulong, ByVal threshold As Ulong = 2 * 16) As Single
Declare Function _setTimer Lib "winmm" Alias "timeBeginPeriod"(ByVal As Ulong = 1) As Long
Declare Function _resetTimer Lib "winmm" Alias "timeEndPeriod"(ByVal As Ulong = 1) As Long
Declare Sub delayHR(ByVal amount As Single, ByVal threshold As Ulong = 2 * 1)
Declare Function regulateHR(ByVal MyFps As Ulong, ByVal threshold As Ulong = 2 * 1) As Single
Sub delayHR(ByVal amount As Single, ByVal threshold As Ulong)
    '' 'amount'  : requested temporisation to apply, in milliseconds
    '' 'thresold' : fixing threshold for fine-grain temporisation (by waiting loop), in milliseconds
    Dim As Double t1 = Timer
    Dim As Double t2
    Dim As Double t3 = t1 + amount / 1000
    If amount > threshold + 0.5 Then
        _setTimer()
        Sleep amount - threshold, 1
        _resetTimer()
    End If
    Do
    #if Not defined(__FB_WIN32__) And Not defined(__FB_LINUX__)
        t2 = Timer
        If t2 < t1 Then t1 -= 24 * 60 * 60 : t3 -= 24 * 60 * 60
    Loop Until t2 >= t3
    #else
    Loop Until Timer >= t3
    #endif
End Sub
Function regulateHR(ByVal MyFps As Ulong, ByVal threshold As Ulong) As Single
    '' 'MyFps' : requested FPS value, in frames per second
    '' function return : applied delay (for debug), in milliseconds
    '' 'thresold' : fixing threshold for fine-grain temporisation (by waiting loop), in milliseconds
    Static As Double t1
    Dim As Single tf = 1 / MyFps
    Dim As Double t2 = Timer
    #if Not defined(__FB_WIN32__) And Not defined(__FB_LINUX__)
    If t2 < t1 Then t1 -= 24 * 60 * 60
    #endif
    Dim As Single dt = (tf - (t2 - t1)) * 1000
    delayHR(dt, threshold)
    t1 = Timer
    Return dt
End Function
#elseif defined(__FB_LINUX__)
Declare Sub delay(ByVal amount As Single, ByVal threshold As Ulong = 2 * 10)
Declare Function regulate(ByVal MyFps As Ulong, ByVal threshold As Ulong = 2 * 10) As Single
#elseif defined(__FB_DOS__)
Declare Sub delay(ByVal amount As Single, ByVal threshold As Ulong = 2 * 55)
Declare Function regulate(ByVal MyFps As Ulong, ByVal threshold As Ulong = 2 * 55) As Single
#else
Declare Sub delay(ByVal amount As Single, ByVal threshold As Ulong = 2 * 16)
Declare Function regulate(ByVal MyFps As Ulong, ByVal Ulong As Single = 2 * 16) As Single
#endif

Declare Function framerate() As Ulong

'------------------------------------------------------------------------------

Sub delay(ByVal amount As Single, ByVal threshold As Ulong)
    '' 'amount'  : requested temporisation to apply, in milliseconds
    '' 'thresold' : fixing threshold for fine-grain temporisation (by waiting loop), in milliseconds
    Dim As Double t1 = Timer
    Dim As Double t2
    Dim As Double t3 = t1 + amount / 1000
    If amount > threshold + 0.5 Then Sleep amount - threshold, 1
    Do
    #if Not defined(__FB_WIN32__) And Not defined(__FB_LINUX__)
        t2 = Timer
        If t2 < t1 Then t1 -= 24 * 60 * 60 : t3 -= 24 * 60 * 60
    Loop Until t2 >= t3
    #else
    Loop Until Timer >= t3
    #endif
End Sub

Function regulate(ByVal MyFps As Ulong, ByVal threshold As Ulong) As Single
    '' 'MyFps' : requested FPS value, in frames per second
    '' function return : applied delay (for debug), in milliseconds
    '' 'thresold' : fixing threshold for fine-grain temporisation (by waiting loop), in milliseconds
    Static As Double t1
    Dim As Single tf = 1 / MyFps
    Dim As Double t2 = Timer
    #if Not defined(__FB_WIN32__) And Not defined(__FB_LINUX__)
    If t2 < t1 Then t1 -= 24 * 60 * 60
    #endif
    Dim As Single dt = (tf - (t2 - t1)) * 1000
    delay(dt, threshold)
    t1 = Timer
    Return dt
End Function

Function framerate() As Ulong
    '' function return : measured FPS value (for debug), in frames per second
    Static As Double t1
    Dim As Double t2 = Timer
    #if Not defined(__FB_WIN32__) And Not defined(__FB_LINUX__)
    If t2 < t1 Then t1 -= 24 * 60 * 60
    #endif
    Dim As Ulong tf = 1 / (t2 - t1)
    t1 = t2
    Return tf
End Function

end GeSHi

此代码(delay_regulate_framerate.bi 文件)应包含在用户源程序的顶部,以便调用这 3 个过程:

#include "delay_regulate_framerate.bi"

4. 使用示例

这里提供两个示例:

  • 第一个示例使用 delay() 子程序,

  • 第二个示例使用 regulate() 函数(以及 framerate() 函数作为调试工具)。

使用 delay() 子程序的示例

一个利用 delay() 生成 4 个递减值小等待的示例:

100 ms,10 ms,1 ms,0.1 ms

start GeSHi

vb
#include "delay_regulate_framerate.bi"

Dim As Double t
Dim As Single t0 = 100

For N As Integer = 1 To 4
    Print "Requested delay :"; t0; " ms"
    For I As Integer = 1 To 4
        t = Timer
        delay(t0)
        Print Using"  Measured delay : ###.### ms"; (Timer - t) * 1000
    Next I
    Print
    t0 /= 10
Next N

Sleep

end GeSHi

在整个使用范围内,测量结果看起来相当准确。

使用 regulate() 函数和 framerate() 工具函数的示例

一个利用 regulate() 控制图形图像 FPS(每秒帧数)刷新的示例:

FPS 范围从 10 到 100,步进为 1

start GeSHi

vb
#include "delay_regulate_framerate.bi"

Screen 12
Dim As Ulong FPS = 60
Do
    Static As ULongInt l
    Static As Single dt
    ScreenLock
    Cls
    Color 11
    Print Using "Requested FPS : ###"; FPS
    Print
    Print Using "Applied delay : ###.### ms"; dt
    Print Using "Measured FPS  : ###"; framerate()
    Print
    Print
    Print
    Color 14
    Print "`<+>`      : Increase FPS"
    Print "`<->`      : Decrease FPS"
    Print "`<Escape>` : Quit"
    Line (0, 80)-(639, 96), 7, B
    Line (0, 80)-(l, 96), 7, BF
    ScreenUnlock
    l = (l + 1) Mod 640
    Dim As String s = Inkey
    Select Case s
    Case "+"
        If FPS < 100 Then FPS += 1
    Case "-"
        If FPS > 10 Then FPS -= 1
    Case Chr(27)
        Exit Do
    End Select
    dt = regulate(FPS)
Loop

end GeSHi

测量的 FPS 几乎保持稳定,并能很好地跟踪请求的 FPS。

为了监控调节,由 regulate() 在循环中添加的延迟也会被可视化显示。

5. 根据实际操作系统周期时间调整 delay()regulate() 过程的粗粒度/精细粒度切换阈值

如果用户想要细化或修改 delay()regulate() 过程的粗粒度/精细粒度切换阈值的默认值,只需通过显式指定第二个参数(以 ms 为单位的整数值)调用即可。

但该阈值的典型设置对应于操作系统周期时间的 2 倍(在要包含的 "delay_regulate_framerate.bi" 文件中设置的默认值):

  • 操作系统周期时间的 2 倍是推荐值 => CPU 负载与延迟精度之间的权衡。

  • 更高的阈值会提高延迟精度(增加),但会损害 CPU 负载(增加)。

  • 更低的阈值会改善 CPU 负载(减少),但会损害延迟精度(减少)。

  • 完全为零的阈值,只要所需延迟大于 0.5 ms,就不会引起 CPU 负载。只有当必要的所需延迟 < 0.5 ms 时,CPU 负载才不会最小化,但这个小值已经意味着达到 FPS 设定值将变得困难。

根据所用平台,在要包含的 delay_regulate_framerate.bi 文件的头部设置的默认值:

  • Windows (基本分辨率操作系统周期时间 = 16 ms) => threshold = 2 * 16 ms

  • Windows (高分辨率操作系统周期时间 = 1 ms) => threshold = 2 * 1 ms(仅由 'delayHR() 和 'regulateHR() 使用,参见下面第 6 段)。

  • Linux (操作系统周期时间 = 10 ms) => threshold = 2 * 10 ms

  • DOS (操作系统周期时间 = 55 ms) => threshold = 2 * 55 ms

  • 其他 (默认操作系统周期时间 = 16 ms) => 默认 threshold = 2 * 16 ms

如果操作系统周期时间的实际分辨率优于上述默认值(取决于平台),用户可以通过显式指定第二个参数(该实际分辨率值的两倍,以 ms 为单位)来调用 delay()regulate()

6. Windows 平台

仅对于 Windows 平台,用户可以通过调用 delayHR()regulateHR() 来临时将操作系统周期时间的分辨率强制提升至 1 ms(而非 16 ms)。

同时也会临时应用正确的阈值(2 * 1 ms)。

这主要因为阈值可以很低而减少了 CPU 负载。

包含在 "delay_regulate_framerate.bi" 文件中的 delayHR() 过程:

start GeSHi

vb
Sub delayHR(ByVal amount As Single, ByVal threshold As Ulong)
    '' 'amount'  : requested temporisation to apply, in milliseconds
    '' 'thresold' : fixing threshold for fine-grain temporisation (by waiting loop), in milliseconds
    Dim As Double t1 = Timer
    Dim As Double t2
    Dim As Double t3 = t1 + amount / 1000
    If amount > threshold + 0.5 Then
        _setTimer()
        Sleep amount - threshold, 1
        _resetTimer()
    End If
    Do
    #if Not defined(__FB_WIN32__) And Not defined(__FB_LINUX__)
        t2 = Timer
        If t2 < t1 Then t1 -= 24 * 60 * 60 : t3 -= 24 * 60 * 60
    Loop Until t2 >= t3
    #else
    Loop Until Timer >= t3
    #endif
End Sub

end GeSHi

包含在 "delay_regulate_framerate.bi" 文件中的 regulateHR() 过程:

start GeSHi

vb
Function regulateHR(ByVal MyFps As Ulong, ByVal threshold As Ulong) As Single
    '' 'MyFps' : requested FPS value, in frames per second
    '' function return : applied delay (for debug), in milliseconds
    '' 'thresold' : fixing threshold for fine-grain temporisation (by waiting loop), in milliseconds
    Static As Double t1
    Dim As Single tf = 1 / MyFps
    Dim As Double t2 = Timer
    #if Not defined(__FB_WIN32__) And Not defined(__FB_LINUX__)
    If t2 < t1 Then t1 -= 24 * 60 * 60
    #endif
    Dim As Single dt = (tf - (t2 - t1)) * 1000
    delayHR(dt, threshold)
    t1 = Timer
    Return dt
End Function

end GeSHi

regulateHR() 不是线程安全的,因为它使用一个 Static 变量。

在任何情况下,对 delay()regulate() 的调用总是以默认分辨率运行,但使用了适应基本分辨率的阈值(2 * 16 ms)。

因此,如果操作系统周期时间已经处于高分辨率(1 ms),则需要使用 delayHR()regulateHR() 以同时应用正确的对应阈值,否则这种高分辨率结合过高的阈值将无法相对于基本分辨率减少 CPU 负载(另一方面,延迟精度将会良好)。

优先仅在操作系统周期时间处于基本分辨率(16 ms)时使用 delay()regulate()

注意:

对于 Windows 平台,"delay_regulate_framerate.bi" 文件仅从 Windows 2000 开始支持(因为它引用了操作系统周期时间的高分辨率)。

对于更早的 Windows 系统,请从 "delay_regulate_framerate.bi" 文件中删除所有高分辨率引用('#if defined(__FB_WIN32__)' 的最后部分):

  • 删除 '-setTimer'、'_resetTimer'、'delayHR' 和 'regulateHR' 的声明,

  • 删除 'delayHR' 和 'regulateHR' 过程的主体。

7. 增强使用示例

与第 4 段的第二个示例相比,此示例得到了改进。

它还额外允许:

  • 仅针对 Windows 平台,在操作系统周期时间的正常分辨率和高分辨率之间切换。

  • 修改可选参数 'threshold' 的值。

start GeSHi

vb
#include "delay_regulate_framerate.bi"

Screen 12, , 2
ScreenSet 1, 0

Dim As ULongInt MyFps = 100
   
Dim As String res = "N"
Dim As Ulong thresholdNR = 32
Dim As Ulong thresholdHR = 2

Do
    Static As ULongInt l
    Static As Double dt
    Static As Ulong fps
    Static As Double t
    Static As Ulong averageFps
    Static As Double sumFps
    Static As Double averageDelay
    Static As Double sumDelay
    Static As Long N
    Static As Ulong fpsE
    Dim As Double t1
    Dim As Double t2
    t = Timer
    Cls
    Print
    Color 15
    Select Case res
    Case "N"
        Print "                      NORMAL RESOLUTION"
    Case "H"
        Print "                      HIGH RESOLUTION (for Windows only)"
    End Select
    Print
    Select Case res
    Case "N"
        Print " Procedure : regulate( " & MyFPS & " [, " & thresholdNR & " ])"
    Case "H"
        Print " Procedure : regulateHR( " & MyFPS & " [, " & thresholdHR & " ])"
    End Select
    Print
    Color 11
    Print Using " Measured FPS  : ###          (average : ###)"; fpsE; averageFps
    Print Using " Applied delay : ###.### ms   (average : ###.### ms)"; dt; averageDelay
    Print
    Print
    Print
    Color 14
    #if defined(__FB_WIN32__)
    Print " `<n>` or `<N>` : Normal resolution"
    Print " `<h>` or `<H>` : High resolutiion"
    Print
    #endif
    Print " `<+>`        : Increase FPS"
    Print " `<->`        : Decrease FPS"
    Print
    Print " Optional parameter :"
    Select Case res
    Case "N"
        Print "    <i> or <I> : Increase NR threshold"
        Print "    `<d>` or `<D>` : Decrease NR threasold"
        Draw String (320, 280), "(optimal value : 32)"
    Case "H"
        Print "    <i> or <I> : Increase HR threshold"
        Print "    `<d>` or `<D>` : Decrease HR threasold"
        Draw String (320, 280), "(optimal value : 2)"
    End Select
    Print
    Print " `<escape>`   : Quit"
    Line (8, 128)-(631, 144), 7, B
    Line (8, 128)-(8 + l, 144), 7, BF
    Do
    #if Not defined(__FB_WIN32__) And Not defined(__FB_LINUX__)
        t2 = Timer
        If t2 < t Then t -= 24 * 60 * 60
    Loop Until t2 >= t + 0.002
    #else
    Loop Until Timer >= t + 0.002
    #endif
    ScreenCopy
    l = (l + 1) Mod 624
    Dim As String s = UCase(Inkey)
    Select Case s
    Case "+"
        If MyFPS < 500 Then MyFPS += 1
    Case "-"
        If MyFPS > 10 Then MyFPS -= 1
    #if defined(__FB_WIN32__)
    Case "N"
        If res = "H" Then
            res = "N"
        End If
    Case "H"
        If res = "N" Then
            res = "H"
        End If
    #endif
    Case "I"
        Select Case res
        Case "N"
            If thresholdNR < 64 Then thresholdNR += 16
        Case "H"
            If thresholdHR < 4 Then thresholdHR += 1
        End Select
    Case "D"
        Select Case res
        Case "N"
            If thresholdNR > 0 Then thresholdNR -= 16
        Case "H"
            If thresholdHR > 0 Then thresholdHR -= 1
        End Select
    Case Chr(27)
        Exit Do
    End Select
    sumFps += fpsE
    sumDelay += dt
    N += 1
    If N >= MyFps / 2 Then
        averageFps = sumFps / N
        averageDelay = sumDelay / N
        N = 0
        sumFps = 0
        sumDelay = 0
    End If
    Select Case res
    Case "N"
        dt = regulate(MyFps, thresholdNR)
    #if defined(__FB_WIN32__)
    Case "H"
        dt = regulateHR(MyFps, thresholdHR)
    #endif
    End Select
    fpsE = framerate()
Loop

end GeSHi

另请参阅

返回 目录

基于 FreeBASIC 官方文档翻译 如有侵权请联系我们删除
FreeBASIC 是开源项目,与微软公司无隶属关系