Winquake/GLQuake Timer Problem and Fix

Post tutorials on how to do certain tasks within game or engine code here.
Post Reply
mh
Posts: 2292
Joined: Sat Jan 12, 2008 1:38 am

Winquake/GLQuake Timer Problem and Fix

Post by mh »

First of all a description of the problem.

On Windows, Quake uses the QueryPerformanceFrequency and QueryPerformanceCounter API calls for it's timer. These give microsecond resolution, and worked perfectly fine in the past, but on modern PCs they are causing problems. Depending on combinations of how many CPUs you have, how many CPU cores you have, whether or not you have power-saving in operation and whether or not you're 64-bit, you may or may not encounter one of these problems:
  • The CPU (or Core) that Quake starts up on may not be the same CPU (or Core) that it runs on; the OS can freely switch applications between different CPUs or Cores as it sees fit, and there may be subtle differences in the counters for each.
  • The initial performance frequency may not be valid if your computer was running in a low power mode when it was queried.
  • Modern CPUs can be so fast that they can overflow the 64-bit value returned by the counter query.
For to fix it we're going to use the Windows Multimedia Timer API instead. This has resolution of 1 millisecond - somewhat less fine-grained, but more than adequate for Quake, and immune to these problems.

To make the process as painless as possible we're going to just copy the code across from QuakeWorld, which - in common with all of ID's subsequent engines - uses these timers insetad of QPf/QPC. It's instructive sometimes to look at the QuakeWorld code as it's the next direct evolution of Quake and can often contain useful bugfixes that never made it back into the original codebase. I believe ID's plan at the time was to amalgamate the two codebases, and it's a shame that they never did as our corner of the world would have been a lot better if they had.

All work is in sys_win.c

First look for the Sys_Init function and replace it with this:

Code: Select all

void Sys_Init (void)
{
	int i;
	OSVERSIONINFO	vinfo;

	MaskExceptions ();
	Sys_SetFPCW ();

	for (i = 1;; i++)
	{
		MMRESULT mmr = timeBeginPeriod (i);

		if (mmr == TIMERR_NOERROR) break;
		if (i >= 100) Sys_Error ("Could not initialize Multimedia Timer");
	}

	vinfo.dwOSVersionInfoSize = sizeof (vinfo);

	if (!GetVersionEx (&vinfo)) Sys_Error ("Couldn't get OS info");
	if ((vinfo.dwMajorVersion < 4) || (vinfo.dwPlatformId == VER_PLATFORM_WIN32s))
		Sys_Error ("QuakeWorld requires at least Win95 or NT 4.0");

	if (vinfo.dwPlatformId == VER_PLATFORM_WIN32_NT)
		WinNT = true;
	else WinNT = false;
}
The new code is in the for loop by the way, and all I'm doing here is setting the multimedia timer to as low a resolution as it can be. I'm probably being over-cautious with this as QuakeWorld just assumed that a value of 1 would always work, and it would be a very strange PC indeed that it did not work on.

Next we need to change Sys_FloatTime to use the new timer, so once again here's the function from QuakeWorld (it was called Sys_DoubleTime there):

Code: Select all

double Sys_FloatTime (void)
{
	static DWORD starttime;
	static qboolean first = true;
	DWORD now;
	double t;

	now = timeGetTime ();

	if (first)
	{
		first = false;
		starttime = now;
		return 0.0;
	}

	if (now < starttime) return (now / 1000.0) + (LONG_MAX - starttime / 1000.0);
	if (now - starttime == 0) return 0.0;

	return (now - starttime) / 1000.0;
}
Note the check for a wrap around - I haven't actually tested it to see if it's valid, and it looks as though ID hadn't either as they had a question mark beside it. Because we're counting in milliseconds and storing the result in a 32-bit DWORD the value will wrap-around every 49-odd days. Those of you with long memories may recall a bug where Windows 95 crashed if it was left up for 49 days - it was on account of something similar. I don't suppose anyone leaves a Windows client machine up that long any more (I have seen servers up for longer), and I'd guess that Windows 95 would have crashed from other causes well before that time.

The wraparound case, by the way, is only valid if Quake was started before it wrapped and is running during the actual wraparound; it should be rare enough.

That's about it, no other changes.
We had the power, we had the space, we had a sense of time and place
We knew the words, we knew the score, we knew what we were fighting for
Post Reply