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:
- on Windows 95/98/ME it is accurate only to multiples of approximately 55 ms, which is too coarse for most purposes; on Windows NT/2000/XP it has an accuracy of 10 ms by default, which could also be improved upon
- it has the very lowest level in the hierarchy of the Windows event/message generating system; that is, the timer event/message may be postponed, or even not be generated at all, if there is any other event that in the queue
The code snippet below has a solution for both these problems:
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 SubAs 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.
