nautilus at November 10th, 2012 07:03 — #1
Anybody knows of a way to detect the number of scan lines in the Vertical Blank Interval (VBI) of the monitor?
I need this to correctly sync with the vertical blank.
I'd call IDirectDraw7::WaitForVerticalBlank(), but the method performs polling and spins the CPU to 100%.
I have many DX7 games that show a strange behavior. On my hardware they compose new frames so fast that they manage to blit them within the same VBI. Multiple frames, all identical to one another. That's brutal.
Although there's no screen tearing, my graphics card pointlessly works like crazy and overheats badly on games that use 'jurassik' graphics by any modern standard. Forcing Vertical Sync doesn't solve this, of course.
In an effort to manually sync accurately with the VBI, and doing it without spinning my CPU to 100%(!), it'd help me to know how many scan lines are in the VBI.
Alas, during VBI the IDirectDraw7::GetScanLine() won't return a scan line number greater than my vertical screen resolution, in that if I run at 1024x768, during VBI the IDirectDraw7::GetScanLine() method always returns a max value of 768.
I quote from the docs:
Remarks Scan lines are reported as zero-based integers. The returned scan line value is in the range from 0 through n, where 0 is the first visible scan line on the screen and n is the last visible scan line, plus any scan lines that occur during the vertical blank period. So, in a case where an application is running at a resolution of 640×480 and there are 12 scan lines during vblank, the values returned by this method range from 0 through 491.
Were it true it'd mean that I have only 1 scan line during VBI. I've set my CRT monitor to all the supported resolutions from 800x600 up to 1600x1200. In every case I was reported a maximum VBI scan line number equal to the current vertical screen resolution. That bit of quoted documentation isn't accurate.
Back to the opening question:
Anybody knows of a way to detect the number of scan lines in the Vertical Blank Interval (VBI) of the monitor?
[edit: Deleted a portion. Corrected another. In my haste I had some details mixed up]
reedbeta at November 10th, 2012 12:50 — #2
I don't understand this concept of "scan lines during vertical blank". The vertical blank is when the beam resets back from the lower-right corner to the upper-left corner of the screen, so I'm not sure where these "extra" scan lines are supposed to be coming from. To me it makes perfect sense that GetScanLine() would have a constant value during the vblank since the monitor's not really on *any* scanline during that time. Setting that constant value to one more than the largest actual scanline also makes sense, since then it won't be confused with any actual scanline.
I guess you're actually trying to measure the amount of time the vblank takes, so you can sleep your game's main thread until it's over, to avoid the problem of presenting multiple frames in the same vblank? In that case, why not just literally do that using QueryPerformanceCounter, using GetScanLine() to detect when the vblank starts and ends? Run a few trials of that and take the minimum time, then you'll have the number and you can try using that to sync your game.
As for avoiding 100% CPU usage, try inserting a Sleep(1) in your polling loops.
nautilus at November 11th, 2012 07:02 — #3
I don't understand this concept of "scan lines during vertical blank". The vertical blank is when the beam resets back from the lower-right corner to the upper-left corner of the screen, so I'm not sure where these "extra" scan lines are supposed to be coming from.
From what I understand they aren't extra scan lines.
When blanked, the electron gun doesn't climb back to top-left straight from the lower-right. That movement would be too fast, leaving not enough time to perform a full blit. Since the gun has to move at constant speed, the solution was to have it 'waste time' by running through a few scan lines (without painting anything) while ascending back to top-left. These are the scan lines 'painted' during the VBI.
At least that's what I'm told. I'm no CRT technician myself.
I guess you're actually trying to measure the amount of time the vblank takes, so you can sleep your game's main thread until it's over, to avoid the problem of presenting multiple frames in the same vblank?
Currently I spin on GetScanLine() until it reports that I'm on the last non-VBI scan line (if screen is 1024x768, I spin until I'm on scan line 767). In that moment I take a time reference with QueryPerformaceCounter(). That becomes the instant 'zero' to sync with all subsequent vertical blank intervals. I can sleep the CPU when needing to wait for the next VBI. It works better than expected.
But it works by the assumption that the monitor shall never be late in its timing. Not even once. Should something occur, my code wouldn't notice it and all frames past that moment would be regularly out of sync. In order to react to this I'd have to run periodical checks that would force me to spin the CPU for the duration of 1 frame (in the worst case scenario). It'd be a noticeable hiccup.
reedbeta at November 11th, 2012 14:34 — #4
the solution was to have it 'waste time' by running through a few scan lines (without painting anything) while ascending back to top-left. These are the scan lines 'painted' during the VBI.
Hmm. I never heard of this before, and it seems like they could just have it wait in the upper-left corner for as long as you want until the frame starts...but I'm no CRT technician either. (And with LCD/LED types of screens, clearly there's no actual electron beam, so the "vblank" is literally just a waiting period in that case.)
But it works by the assumption that the monitor shall never be late in its timing. Not even once. Should something occur, my code wouldn't notice it and all frames past that moment would be regularly out of sync.
Yep, that would indeed be a problem. I wonder if you couldn't solve this without needing to stop everything and produce a hiccup, though. Could you run the periodic sync checks in a separate thread, and gradually steer the main thread back in sync by adjusting the timing a little bit each frame? Kind of a negative-feedback mechanism.
nautilus at November 13th, 2012 07:18 — #5
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?
stainless at November 13th, 2012 09:56 — #6
Not only is there a gap at the top and bottom of the screen, there are also gap on the left and right.
We used to do all sorts of things in this area of the screen. On some machines , mainly the atari machines, you could actually change the screen resolution in the hblank period. Meaning you could have a different resolution on all screen lines. You could even insert NMI's and delay processing of the hblank for a while, making the edge of the display move. Lot's of fun. Real coding when you time your code with border colour changes and little bits of post it notes.
Of course all this only applies to CRT's
Modern displays really do not have this.
Not everything gets better over time
Give me a clean machine and an assembler and I'm a happy little coder.