Fine-grain procedure for waiting and in-loop procedure for fine-regulating FPS
- Improving SLEEP feature by using a fine-grain waiting procedure.
- Controlling FPS (Frames Per Second) by using an in-loop fine-regulating procedure.
Preamble:
The proper use of the SLEEP and TIMER keywords in user procedures requires a good knowledge of their limits and behaviors.
SLEEP keyword
Generally the SLEEP feature has not a good accuracy with respect to the delay requested and does not allow to produce very low waiting times.
The accuracy of SLEEP is variable depending on the OS cycle time:
Windows NT/2K/XP: 15 ms, 9x/Me: 50 ms, Linux: 10ms, DOS: 55 ms.
And using for the delay values lower than these accuracy values does not allow SLEEP to produce the corresponding wait values (always higher wait, and of the order of these values) except for the delay value '0'.
TIMER keyword
The accuracy of the time returned by TIMER is much greater than that generally of the SLEEP keyword to generate a wait:
for modern processors, sub-microsecond accuracy is expected.
On some platforms (except Windows and Linux), the return value of TIMER is reset at midnight, so if the start and end time are on either side of the reset point, this could cause unexpected behavior in some programs if this event is ignored.
1. Principles to overcome the penalizing behaviors of SLEEP and TIMER keywords
To implement effective time management procedures based on SLEEP and TIMER keywords, some bad behaviors as described above must be overcome.
Principle for producing a waiting feature with accurate delay
Using SLEEP does not hog the CPU, but provides poor accuracy and is not compatible with very small delays.
Using a loop while testing the TIMER value produces a good accuracy but hogs the CPU.
The principle is to chain:
- a large-grain delay using SLEEP for the first part of the requested delay,
- then a fine-grain delay for the rest of the time using a loop that tests the TIMER value.
A time threshold makes it possible to switch between these two types of operation.
A typical setting for this threshold corresponds to a value of 2 times the OS cycle period.
This value corresponds to a tradeoff between CPU load and delay accuracy.
Principle for compensating a possible reset of the TIMER return value
The detection of a possible reset (at midnight) of the TIMER between two calls is carried out by testing the sign of the difference between the two returned values from the TIMER.
If the second value is smaller than the first (due to a reset of the TIMER), a negative temporal compensation is applied on the first, corresponding to a day expressed in seconds.
2. Procedures body (without declarations) description
3 procedures are described here:
- 'delay()' procedure => to generate the wait for an accurate time (floating value expressed in ms)
- 'regulate()'procedure => to finely control and regulate a loop frequency (number of loops per second in integer value)
- 'framerate()' procedure => tool to measure the instantaneous loop frequency (number of loops per second in integer value)
The 2 first procedures are well suited to finely regulate and/or control the FPS (Frames Per Second) of an image refresh.
'delay()' is more for one-shot use every now and then, while 'regulate()' is more suited for inserting a fine delay into a loop to generally adjust its FPS.
'regulate()' and 'framerate()' are not thread safe because they each use a 'Static' variable.
Note: Like any waiting feature, it is strongly discouraged to use 'delay()' or 'regulate()' when the screen is locked, so no call from inside a [ScreenLock...ScreenUnlock] block.
'delay()' procedure body (improving SLEEP feature by using a fine-grain waiting)
The 'delay()' sub is useful for generating accurate wait functionality, more precise than that provided by the SLEEP keyword, and also compatible with very short wait times.
If the requested wait 'amount' is greater than the 'threshold' (+ 0.5), a first part of the requested waiting time ('amount' - 'threshold') is executed thanks to the SLEEP keyword (not hogging the CPU), and the remaining time is generated by testing in loop the accurate values of TIMER up to the target value.
An eventual resetting of the TIMER is also tested to compensate for it if it occurs.
`
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
`
When the 'delay()' sub above is called from the "delay_regulate_framerate.bi" file to be included (see its definition below in paragraph 3.), a declaration added ahead the body of the sub defines its second parameter ('threshold') as an optional parameter and sets its default value.
'regulate()' procedure body (fine-controlling FPS by using an in-loop regulating)
The 'regulate()' function is built around the 'delay()' sub, but where the waiting time to be applied (if any remains) is deducted from the requested frame period (the inverse of the FPS) and the time already elapsed since the last call.
For debugging purposes, the 'regulate()' function returns the delay it applied (the one added to the initial loop). If the user does not wish to use this debug data, then they can call the function as a Sub quite simply.
If this delay value returned is very small, this means that it becomes difficult to reach the FPS setpoint. Otherwise, the fluctuation of the delay returned (assuming that the regulation is very accurate) represents the fluctuation of the user code executed in the loop from frame to frame.
`
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
`
When the 'regulate()' function above is called from the "delay_regulate_framerate.bi" file to be included (see its definition below in paragraph 3.), a declaration added ahead the body of the function defines its second parameter ('threshold') as an optional parameter and sets its default value.
'framerate()' procedure body (tool for FPS measuring)
For debugging purposes, an instantaneous measurement of FPS (by measuring the frame time and calculating the inverse) can be provided by calling from anywhere in the loop the following very simple tooling function 'framerate()'.
If 'framerate()' is placed right next to 'regulate()', this measures the intrinsic precision of the regulation.
If 'framerate()' is placed elsewhere in the program loop, it may be additionally impacted by program execution fluctuations from frame to frame.
`
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
`
Unlike 'delay()' and 'regulate()', the 'framerate()' function above can be used even when the screen is locked, so called even from inside a [ScreenLock...ScreenUnlock] block.
3. Complete source code to be included containing all the declarations and the procedure bodies
A header contains the declarations for the bodies of the 3 procedures described above.
For 'delay()' and 'regulate()', these allow to declare their second parameter as optional and to set the default values according to the platform used (Windows, Linux, DOS, or other).
File to be included: "delay_regulate_framerate.bi"
`
' 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
`
This code ('delay_regulate_framerate.bi' file) should be included at the top of the user source program so that they can call the 3 procedures:
`#include "delay_regulate_framerate.bi"`
4. Examples of use
Two examples are available here:
- first example using the 'delay()' sub,
- second example using the 'regulate()' function (and also the 'framerate()' function as debug tool).
Example using 'delay()' sub
An example generating, thanks to 'delay()', 4 small waits of decreasing values:
100 ms, 10 ms, 1 ms, 0.1 ms
`
#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
`
The measured results seem fairly accurate throughout the full range of use.
Example using 'regulate()' function and 'framerate()' tooling function
An example controlling, thanks to 'regulate()', the FPS (Frames Per Second) refreshing of a graphic image:
FPS range available from 10 to 100 in steps of 1
`
#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
`
The measured FPS is almost stable and tracks the requested FPS well.
To monitor the regulation, the delay added in the loop by 'regulate()' is also visualized.
5. Adjusting the commutation threshold (large-grain/fine-grain) for 'delay()' and 'regulate()' procedures depending on the real OS cycle period
If the user wants to refine or modify the default value of the large-grain/fine-grain commutation threshold for the 'delay()' or 'regulate()' procedures, just call it by explicitly specifying the second parameter (integer value given in ms).
But a typical setting for this threshold corresponds to a value of 2 times the OS cycle period (default values set in the file to be included: "delay_regulate_framerate.bi"):
- 2 times the OS cycle period is the recommended value => tradeoff between CPU load and delay accuracy.
- A higher threshold would promote the delay accuracy (increasing), but to the detriment of CPU load (increasing).
- A lower threshold would promote the CPU load (decreasing), but to the detriment of delay accuracy (decreasing).
- A downright null threshold would induce no CPU load as long as the required delay is greater than 0.5 ms. Only for a necessary required delay < 0.5 ms, the CPU load would not be minimized, but this small value already means that it will become difficult to reach the FPS setpoint.
Default values set in the header of the 'delay_regulate_framerate.bi' file to be included, according to the platform used:
- Windows `(basic resolution OS cycle period = 16 ms)` => `threshold = 2 * 16 ms`
- Windows `(high resolution OS cycle period = 1 ms)` => `threshold = 2 * 1 ms` (only used by 'delayHR() and 'regulateHR(), see paragraph 6 below).
- Linux `(OS cycle period = 10 ms)` => `threshold = 2 * 10 ms`.
- DOS `(OS cycle period = 55 ms)` => `threshold = 2 * 55 ms`.
- Other `(OS cycle period per default = 16 ms)` => `threshold = 2 * 16 ms by default`.
If the actual resolution of the OS cycle period is better than the default one above (depending on platform), the user can call 'delay()' and 'regulate()' by explicitly specifying the second parameter (two times this actual resolution value given in ms).
6. Windows platform
For the Windows platform only, the user can force temporarily the high resolution of the OS cycle period to 1 ms (instead of 16 ms) by calling 'delayHR()' or 'regulateHR()' instead.
The correct threshold (2 * 1 ms) is also temporarily applied.
This mainly reduces the CPU load because the threshold can be low.
'delayHR()' procedure included in the "delay_regulate_framerate.bi" file:
`
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
`
'regulateHR()' procedure included in the "delay_regulate_framerate.bi" file:
`
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
`
'regulateHR()' is not thread safe because it uses a 'Static' variable.
In any cases, a call to 'delay()' and 'regulate()' always runs in the default resolution, but with a threshold adapted to the basic resolution (2 * 16 ms).
So if the OS cycle period is already in high resolution (1 ms), it is necessary to use 'delayHR()' and 'regulateHR()' to also apply the correct corresponding threshold, otherwise this high resolution combined with a much too high threshold will not reduce CPU load compared to basic resolution (on the other hand the delay accuracy will be good).
Preferably use 'delay()' and 'regulate()' only when the OS cycle period is in basic resolution (16 ms).
Note:
For the Windows platform, the "delay_regulate_framerate.bi" file is only supported from Windows 2000 (because it refers to high resolution for OS cycle period).
For earlier Windows systems, remove all references to high resolution from the "delay_regulate_framerate.bi" file (last part of the `'#if defined(__FB_WIN32__)'`):
- remove declarations of '-setTimer', '_resetTimer', 'delayHR' and 'regulateHR',
- delete the body of the 'delayHR' and 'regulateHR' procedures.
7. Enhanced example of use
This example is improved compared to the second example of the paragraph 4.
It allows in addition to:
- Switch between the normal resolution and the high resolution of the OS cycle period for the Windows platform only.
- Modify the value of the optional parameter 'threshold'.
`
#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
`
See also

- `Sleep`
- `Timer`
- Graphics Mode Refresh and Anti-Flickering
- Lite regulation function to be integrated into user loop for FPS control