Ah the windows message pump – you just can’t get away from it regardless of the language you write in. I may be developing Windows programs on my Mac now (using Parallels) and I’m using C# for the current project, but necessity demands I write yet another version of the message pump. I’ve blogged about this before for various reasons.

I’m currently developing an application which will playout video on a live-to-air broadcast. Apart from the necessary robustness this demands, another crucial element is the timing and synchronising with a timebase. Generally as GUI developers we don’t have to deal with things this way around. It’s more likely we have to cope with displaying high volumes of data from a stream in a window somewhere. You’re then faced with the usual – do I throttle it, buffer it, lockless queue it type questions. As long as the painting of the screen doesn’t end up in white outs or frozen, partially painted windows the users are happy. I’ve written about this before and it’s all about keeping the message pump, pumping away.

The point here is you’re not particularly concerned that WM_PAINT and WM_TIMER messages, being the amoeba of the windows message world, get pushed to the back of the message queue, because eventually they will get processed.

However, if you have to provide screen updates with accurate, rock solid countdown timers that don’t stutter and can be absolutely relied upon, you need to tackle things slightly differently. You need accurate timers and timing which is not normally the forte of Win32.

For what I needed to achieve, working with video was a very accurate video frame counter and execute some code in the message pump every frame. Since it is PAL this means every 40mS and the code shouldn’t take more than 40mS to execute. If it does then it needs to take account of the overrun.

So the choices for accurate timing are:

GetTickCount()
Well you could use this, but you’d be mad. Although it is documented to return the current system time in milliseconds, the actual precision can range from a few up to 55mS depending on operating system.
Accuracy: 5 – 55 mS
Resolution : 1 mS
Execution time : 0.1 uS

timeGetTime()
As part of the multimedia system, this is accurate to 1 mS and can be relied upon, but it takes a long time to execute.
Accuracy: 1 mS
Resolution : 1mS
Execution time : 8.0 uS

Performance counter
The way to go if you want uS accuracy and resolution.
Accuracy: 1 uS or better
Resolution : 1uS
Execution time : 4.0 uS

Unfortunately, the performance counter is not available on all PCs. If you’re running on old hardware or an old version of Windows this API uses a programmable timer chip (part of the chipset) as the clock. This has a frequency of 1.19318MHz meaning an accuracy of around a uS. More recent chipsets will from Intel will give you a frequency of 3.579545MHz.

The frequency only affects the resolution and the API gives you a call to query it, so you can take it into account.

So how can we incorporate this into a message pump and get the accuracy required. A typical message pump looks like this:


MSG msg;
while (GetMessage(&msg, NULL, 0, 0))
{
  TranslateMessage( &msg );
  DispatchMessage( &msg );
}

The well known problem with this is GetMessage. It won’t return until a message is available to be dealt with, so we’re always waiting for Windows to give us something – obviously no good because we need to do stuff every 40 mS. The classic way to solve this uses a PeekMessage in the message pump to see if there is a message available. If not then you can call what ever code you need to execute. So the alternative which incorporates a rock solid 40 mS with a resolution of about 1 uS is like this:


MSG msg;
LONGLONG timeNow;
DWORD msPerFrame = 40;
LONGLONG timerFrequency;
LONGLONG nextTime=0;
BOOL okToDoWork = true;
::QueryPerformanceFrequency((LARGE_INTEGER *) &timerFrequency)
msPerFrame = timerFrequency / 25; // 25 frames per second i.e 40mS frame rate
::QueryPerformanceCounter((LARGE_INTEGER *) &nextTime);
::PeekMessage( &msg, NULL, 0, 0, PM_NOREMOVE);
while (msg.message!=WM_QUIT)
{
  if (PeekMessage( &msg, NULL, 0, 0, PM_REMOVE))
  {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }
  else
  {
    if (okToDoWork)
    {
      DoSomeWork();
      okToDoWork = false;
    }    ::QueryPerformanceCounter((LARGE_INTEGER *) &timeNow);
    if (timeNow > nextTime)
    {
      DrawStuff();
      nextTime += msPerFrame;     
      // Effectively drops a frame if we've taken too long
      // to draw stuff.
      if (nextTime < timeNow)
        nextTime = timeNow + msPerFrame;     
      okToDoWork = true;
    }
  }
}

Advertisements