the situation changed.
Things got simpler - and more complex at the same time.
I was refining my code, chasing the idea of resynch'ing if at the end of a frame I detected that I wasn't on the expected scan line number. The theory says I'd hiccup, but in practice: how often could it happen?
It was occurring very very very frequently.
There had to be an error in my procedure. Perhaps my very observing the situation was eating enough cycles to consistently ruin my checks. I reacted by reducing the wait time per frame by arbitrary amounts until I got decent results. That's when I noticed a number with a disturbing occurrence: 999.62(etc) milliseconds. I was counting 75 screen repaints in 999.62 msecs instead of 1000.
Long story short: my monitor is being faster than it claims to be.
But a better explanation is in order.
The scenario is that of a 1024x768 CRT monitor set at 75 Hz refresh rate.
My CPU is a Core2 Duo 3.0 GHz. QueryPerformanceFrequency() will return either 2999690000, or 2999700000, or 2999710000... depends on the days, and today it is 2999690000. So my CPU ticked from 1 to 2999690000 in 1 second.
If we blindly trust numbers, in the time my monitor paints 1 screen the CPU ticks 39995866 times (2999690000 / 75). Since the CPU counts faster than the monitor paints lines, I should have that every 39995866 ticks the electron gun is consistently on the same scan line. Right?
When I added control code to verify just that, I discovered that I'd easily desync starting with immediately, every 2nd or 3rd frame in fact (even if I re-synch promptly). It was making no sense until the thought that my monitor wouldn't repaint exactly 75 times per second struck me. But the very monitor's OSD reports a vertical frequency of 75.0 Hz. Must I distrust that?? Apparently yes.
I have my app create a new thread with highest priority (just not REAL_TIME) to run a benchmark routine. In this thread I start a loop to iterate for an X number of times, multiple of the screen refresh rate. For example: a 10 seconds benchmark at 75 Hz would make 750 iterations.
For every iteration...
1. I spin on GetScanLine() until it reports that I'm on scan line #0;
2. I take the reference with QueryPerformanceCounter();
3. I spin again on GetScanLine() till it reports that I'm no more on scan line #0;
4. Back to point 1.
This produces a list of CPU tick counts. All such ticks are absolute, so I first convert them into relative amounts with a simple subtraction:
// With time being the absolute CPU tick taken
// at instant 'zero', the begin of benchmark.
time[n] -= time[n-1];
After the conversion to relative times, I scan the list to delete the few anomalies that have occurred because the thread wasn't REAL_TIME priority. That is, once in a while a frame will score twice as much time as normal because the loop from point 1. failed. After accounting for these anomalies (from 2 to 6 over 750 iterations), I make a simple average of the remaining times.
Surprise: the results range from 3998079X to 3998081X ticks. Much different from the 39995866 ticks obtained by dividing the CPU frequency by the monitor refresh rate.
I'm sure that my monitor isn't painting exactly 75 screens per second. It says 75.0 Hz, but my benchmark detects 75.02826(etc) Hz (2999690000 / \~39980800), and they don't exclude one another.
To understand, the added 0.02826 Hz produces 1 extra screen every \~35.381 seconds. It's no small thing.
This is being difficult to deal with. Were the monitor precise I could rely on QueryPerformanceCounter() to sync with the VBI. Instead I must rely on an average determined empirically, which isn't an exact quantity, close as it may be, and forces me to wait for an amount of ticks inferior to that. Doing so I stop cycles away from the closest VBI and can afford to loop on GetVerticalBlankStatus() till it returns true.
It gives acceptable results without hogging the CPU, but it's easily disrupted if another thread gets the CPU slice while *I* need it the most. On a positive note, the loop on GetVerticalBlankStatus() at the end of each wait naturally re-synchs if my monitor is ever late with the painting.
To think that my timing procedure sleeps for most of the time and only runs time-critical code when looping on GetVerticalBlankStatus()...
Do you know of a way to ensure that such tight loop won't be paused by another thread?