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
Back to Programmer's Guide
Valid XHTML :: Valid CSS: :: Powered by WikkaWiki



sf.net phatcode