Accurate frame timing in Visual Basic

Skip to main content (skip navigation menu)






Accurate frame timing in Visual Basic

 

A common problem in Windows programs, especially those that are concerned with animation, is repeating something at accurate intervals. In the case of animation, for example, you need to display a new frame or an updated scenery every, say, 50 milliseconds.

The standard Windows timer (the SetTimer function) has two major problems:

The code snippet below has a solution for both these problems:

Accurate timing
Public Declare Function timeGetTime Lib "winmm.dll" () As Long
Public Declare Function timeBeginPeriod Lib "winmm.dll" \ 
        (ByVal uPeriod As Long) As Long
'16-bit programs must use MMSYSTEM.DLL instead of WINMM.DLL

Public Declare Sub Sleep Lib "kernel32" (ByVal uDuration As Long)
'16-bit programs cannot do a "Sleep", just omit it in that case

Public Sub TimedCode(ByVal Interval As Integer)

    Dim StartTime As Long
    Dim NextFrameTime As Long

    timeBeginPeriod 1   'switch resolution to 1 ms
    Do
        StartTime = timeGetTime
        NextFrameTime = Interval + StartTime

        ' The main code goes here; e.g. a new frame in the animation

        DoEvents
        Do While timeGetTime < NextFrameTime
            DoEvents
            If timeGetTime + 5 < NextFrameTime Then
                Sleep NextFrameTime - (timeGetTime + 5)
            End If
        Loop
    Loop

End Sub

As is apparent from the snippet above, the code uses timeGetTime rather than SetTimer or GetTickCount, for the sake of an accuracy of the timestamps of 1 millisecond. Actually, this is a little more involved: on Windows 95/98/ME timeGetTime has a resolution of 1 ms and SetTimer / GetTickCount don't do better than about 55 ms. However, on Windows NT/2000/XP, all these timers are ultimately based on a single interbal timer, which has a 10 ms resolution by default. That is, on Windows 95/98/ME timeGetTime has a resolution of 1 ms and on Windows NT/2000/XP this same function has a resolution of 10 ms. Fortunately, you can increase the resolution of the internal Windows NT/2000/XP timer by calling timeBeginPeriod. If you want to be nice to the OS, you can also call timeEndPeriod when you are done with the routine, but to this date, all versions of Microsoft Windows ignore calls to timeEndPeriod. The periodicity of the timed loop is given to the TimedCode via the parameter Interval. This parameter is the number of milliseconds between two consequtive runs of the "main code". If you are using this code for animation, it is typical to indicate the refresh speed in "frames per second" (fps); the relation between fps and milliseconds is just: "Interval = 1000 / fps". (20 fps gives 50 milliseconds per frame).

The calls to DoEvents allow user input to this or to other programs, while the TimedCode sits in a loop. In C/C++, you can emulate DoEvents with a PeekMessage loop. In 16-bit Windows (Windows 3.1x) this would be enough, but in any 32-bit version of Microsoft Windows, a DoEvents or PeekMessage loop does not release the time slice when there is nothing to do. As a result, the program using such a loop would take near 100% of the processor usage. To fix it, you can call the function Sleep with the number of milliseconds that you do not need attention from the CPU. In the snippet above, I call Sleep with the number of milliseconds to the next frame, minus 5 milliseconds. I subtract 5 because the Sleep is not very precise and it is better to drop into the DoEvents loop (without Sleep) for the last 5 milliseconds.

The loop needs a modification if it is going to be used in a program that should run for many days without interruption. The timeGetTime counter overflows after 2,147,484 seconds, or 596 hours, or nearly 25 days. If an overflow occurs, the Do While loop will not end, at least not for another 25 days. To avoid the endless loop on the overflow, you can replace the test "timeGetTime < NextFrameTime" by "timeGetTime < NextFrameTime And timeGetTime >= StartTime". Now, when an overflow occurs, one frame will have a interval that is too short, but that is usually not critical.