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 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.