|
An extremely important part of a good emulator
is maintaining proper timing across a variety of hardware platforms.
When your game requires 60 frames per second, you need to make sure
it runs at 60 fps on a 486-66 as well as a K-6 350. There
are basically three kinds of timing methods in Win32: 1) The 'standard'
Windows timer functions, 2) The 'multimedia' timer functions, 3)
The 'high performance' timer functions.
1) The 'standard' Windows timer functions
- This includes the two functions SetTimer() and KillTimer().
Windows allows you to define up to 16 separate interval timers which
can either call a procedure or post a message to a window.
One problem with these timers is that they are based on the Windows
tick counter which runs at a frequency of 18.2Hz (55ms period).
If your game requires 60 frames per second (16.667ms period), you
cannot get accurate timing from these functions. The other
problem is that the WM_TIMER message is a low priority message and
like the WM_PAINT message is really just a bit flag. When
multiple timer messages get stacked up because your code is busy
doing something, the extra messages are thrown away. These
functions are really useless for real-time or accuracy and only
serve for doing 'gross' kinds of time events which need an accuracy
in terms or seconds or minutes. I have included a description
of this type of timer for completeness, but it is not something
you would use for video frame timing in an emulator.
2) The 'multimedia' timer functions
- These functions reside in WINMM.DLL and are considered part of
the multimedia extensions to standard Windows. The functions
that we are interested in are timeBeginPeriod(), timeGetTime(),
and timeEndPeriod(). These functions are supported on most
hardware and allow for an accuracy down to 1ms. There are
other time functions included in this library, but I'm only going
to cover these. Accuracy of 1ms is almost good enough for
accurate emulation. If you don't mind your game running at
58.8fps or 62.5 fps because 1000 is not evenly divisible by 60fps,
so either you use 16 or 17ms per frame.
3) The 'high performance' timer functions
- These functions reside in KERNEL32.DLL and allow you access to
the countdown timer used to trigger the 'low performance' Windows
tick counter. In the standard PC-compatible hardware architecture,
there is a 1.19Mhz crystal oscillator connected to an Intel timer
chip. The timer chip has a 16-bit countdown timer which is
loaded with 0 at the start of each cycle. 1.19Mhz divided
by a countdown value of 65536 gives 18.2 Hz or 55ms. This
is the basis for the PC's tick counter and the timing of the WM_TIMER
messages. The 'high performance' functions allow you to directly
read the countdown timer value. The value returned is a 64bit
integer with the upper 48-bits being the system tick counter and
the lower 16 bits being the 1.19Mhz countdown timer value.
Microsoft says that this timer is not supposed to exist on all versions
of Windows or on all PC's, but every PC I have tried has these functions
and they work on Win95, Win98, and Windows NT.
How does it fit into the emulator?
I chose to use method #3 and in case Microsoft was telling the truth,
I fall back to using method #2 for machines which don't support
method #3. The way that I use timers may not be ideal, but
it does provide accurate timing with minimal wasted cycles.
I basically poll (read in a tight loop) the timer functions until
I arrive at the correct time interval for each video frame.
This, at first, seems like a very poorly behaved Windows program
since you are supposed to be event driven in Windows and not use
polling techniques. The way I correct for this is to use the
Sleep() function. If I arrive at the end of my frame handling
code and there are more than 2ms to spare, I let the sleep function
take up the slack which allows other threads to execute. I
don't trust the Sleep() function for intervals of less than 3ms.
The reason I don't trust the Sleep() function for less than 3ms
is because thread priority/timing on Win95/98 is not perfect, so
by giving up your time slice with Sleep(), you may not get control
again for a longer time than you expected. I don't have hard
evidence to support my belief, but I feel it's better to be safe
than sorry. Below is pseudocode showing my timing technique in action:
while (!bUserAbort)
{
CheckKeysAndJoysticks();
EmulateCPU(oneframetime);
ProduceSound(oneframetime);
if (ScreenNeedsPainting)
UpdateScreen();
WasteLeftoverTime();
}
void WasteLeftoverTime()
{
if (timeleft > 2ms)
Sleep(timeleft - 1ms);
while (timeleft)
CheckTimer();
}
Judging by the CPU utilization shown on the NT
task manager, my technique uses less CPU time than other Windows
game emulators out there. Obviously there is more that you
can do with timers than I've written here, but this is enough information
to get your program timing its frames accurately.
Webdesign
by Deep Magic Studios
- HanaHo Games, Inc. Copyright © 2002 |