精细等待过程与循环内 FPS 精细调节过程
- 来源: https://www.freebasic.net/wiki/wikka.php?wakka=ProPgDelayRegulate
- 最后更新: 2024-02-18
使用精细等待过程改善 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
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 Subend GeSHi
当从要包含的 "delay_regulate_framerate.bi" 文件调用上面的 delay() 子程序时(参见下面第 3 段中的定义),在子程序体前面添加的声明将其第二个参数(threshold)定义为可选参数并设置其默认值。
regulate() 过程体(使用循环内调节精细控制 FPS)
regulate() 函数是围绕 delay() 子程序构建的,但其中要应用的等待时间(如果还有剩余)从请求的帧周期(FPS 的倒数)减去自上次调用以来已经过去的时间中扣除。
出于调试目的,regulate() 函数返回其应用的延迟(添加到初始循环的延迟)。如果用户不希望使用此调试数据,则可以简单地将该函数作为子程序调用。
如果返回的延迟值非常小,这意味着越来越难以达到 FPS 设定值。否则,返回延迟的波动(假设调节非常精确)代表了循环中用户代码从帧到帧执行的波动。
start GeSHi
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 Functionend GeSHi
当从要包含的 "delay_regulate_framerate.bi" 文件调用上面的 regulate() 函数时(参见下面第 3 段中的定义),在函数体前面添加的声明将其第二个参数(threshold)定义为可选参数并设置其默认值。
framerate() 过程体(FPS 测量工具)
出于调试目的,可以通过在循环中的任意位置调用以下非常简单的工具函数 framerate(),提供 FPS 的瞬时测量(通过测量帧时间并计算其倒数)。
如果 framerate() 紧邻 regulate() 放置,则可测量调节的内在精度。
如果 framerate() 放置在程序循环的其他位置,则可能还会受到程序执行从帧到帧波动的额外影响。
start GeSHi
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 Functionend GeSHi
与 delay() 和 regulate() 不同,上面的 framerate() 函数即使在屏幕锁定时也可以使用,因此即使在 [ScreenLock...ScreenUnlock] 块内也可以调用。
3. 包含所有声明和过程体的完整源代码
头部包含上述 3 个过程体的声明。
对于 delay() 和 regulate(),这些声明允许将其第二个参数声明为可选参数,并根据所用平台(Windows、Linux、DOS 或其他)设置默认值。
要包含的文件:"delay_regulate_framerate.bi"
start GeSHi
' 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 Functionend 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
#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
Sleepend GeSHi
在整个使用范围内,测量结果看起来相当准确。
使用 regulate() 函数和 framerate() 工具函数的示例
一个利用 regulate() 控制图形图像 FPS(每秒帧数)刷新的示例:
FPS 范围从 10 到 100,步进为 1
start GeSHi
#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)
Loopend 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 msWindows
(高分辨率操作系统周期时间 = 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
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 Subend GeSHi
包含在 "delay_regulate_framerate.bi" 文件中的 regulateHR() 过程:
start GeSHi
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 Functionend 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
#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()
Loopend GeSHi
另请参阅
SleepTimer- 图形模式刷新与防闪烁
- 集成到用户循环中用于 FPS 控制的轻量调节函数
返回 目录