Double-render bug on fast GPUs (Erratic input lag during VSYNC ON)

Hello,

I discovered a strange behavior in MonoGame in full screen mode.

On a very fast system at 60Hz, most of the time it is 1/60sec (~17ms) between Draw() and the Update() calls and almost instantaneous from Update() back to Draw().

However, occasionally, (about 1 out of 100 refresh cycles), suddenly Draw() then Update() takes 2/60 sec (~33ms). But a smooth scrolling animation shows absolutely no judder at all, and I can see a single frame drop. The subsequent Draw-then-Update becomes suddenly 0/60sec to ‘catch’ up, before going back to a normal cadence of 1/60sec.

Since Update() does the keyboard reads, I need input reads to occur exactly 1/60sec apart for more predictable VSYNC ON activity – rather than erratically jittering around 1/60sec from proper input read timing (creating erratic input lag).

I’m using the Stopwatch object (better than microsecond resolution).

I’m getting these numbers (microseconds between Draw() and then Update() as an example:

0.0167842321
0.0167051777
0.0166395418
0.0334241493 <— Double VSYNC
0.0000173187 <— Near instant
0.0167538933
0.0167816058

What’s causing this sudden major timing jitter? (that doesn’t show up as a visual judder of a dropped frame – motion is still perfectly smooth, zero frame drops).

The VSYNC buffering is probably smoothing it out perfectly with absolutely zero jitters, but the problem is that input reads occur with Update() and these sudden lag jitter is throwing aiming off. (Visualize this example about how important precise input reads are – A smooth scrolling archery target at 1000 pixels/sec = 17 pixels per refresh cycle. A mis-timed button read that’s late, will cause an arrow to miss the dead center when the button is timed perfectly – because of this strange bug that forces input reads to be out-of-sync with the displayed frame)

This is on a Geforce Titan, so the graphics performance is not an issue here. If VSYNC is OFF (graphics.SynchronizeWithVerticalRetrace = false) then framerate runs at >2000fps, so there is no rendertime issue at play here. Also, garbage collection pattern isn’t matching these intervals, so it doesn’t seem to be garbage collection either.

Is this a graphics driver behavior or something else?
Or a monogame issue?

My fix:

Add this line:
System.Threading.Thread.Sleep(1)
To the end of Draw() before the base.Draw() call.

That solved this problem 100%


Why this works:

I found the cause. My GeForce Titan is so fast it can render 2 frames during the VBlank interval at 60Hz refresh rate. If I turn VSYNC OFF (graphics.SynchronizeWithVerticalRetrace = false) then I get over 2000+ frames per second. Thats 2000+ Draw-Update cycles for simple sprite-based graphics – only half-millisecond rendertimes per frame.

Now, I also separately (in a separate Direct3D9 application) benchmarked the time length of a vertical blanking interval. For some reason, occasionally Update-Draw-Update-Draw is called TWICE per refresh cycle (Direct3D9 equivalent RasterStatus.INVBlank() == still true) It cycles true-false-true-false. When it says true, “RasterStatus.INVBlank == true” for approximately 0.5 milliseconds at the 60Hz refresh rate. Coincidentially, 1 second divided by 0.5ms equals 2000 frames per second.

And this MonoGame bug only appears when rendertimes fall below ~0.5ms…

Usually a display spends approximately ~3% of its time inside the blanking interval (basically 3% of 1/60sec = about 0.5ms). If rendertimes are fast enough, then a display may still be inside the vertical blanking interval when a frame is already rendered. Which if you try to wait for VSYNC, you might be seeing the same VSYNC interval rather than the NEXT VSYNC interval. Which results in a double-render in one refresh cycle (extra unnecessary buffered render), followed by a skipped render (catchup).

The proper MonoGame engine fix is to wait for it to EXIT the blanking interval (wait to exit VSYNC), before WAITING AGAIN for blanking interval again (wait on VSYNC). This will ensure that ultrashort rendertimes doesn’t cause lag-fluctuation bugs in MonoGame.

In the past, most never needed to “wait to exit VSYNC” because rendertimes always took longer than the short length of VSYNC “pulse” between refresh cycles (~0.5ms) But this isn’t the case anymore with fast GPUs, especially with simpler sprite-based graphics. So this additional logic will need to be added to a future version of MonoGame (wait-to-exit-vsync).

As a workaround waiting 1ms at the bottom of Draw() (before base.Draw()) guarantees that we’re outside VBlank again, so that MonoGame waits accurately for the next VBlank without unnecessary lag fluctuations (piling up the buffers).

I’ll have to dig through the Monogame source code to figure out where the bug is (or if it’s a graphics driver bug).

(P.S. Why doesn’t MonoGame implement RasterStatus object found in XNA? I was forced to write a separate app to troubleshoot and figure out this bug.)


So for now, if your graphics is very simple and fast (and at risk at running at 0.5ms rendertimes on top-end graphics card), the current MonoGame developer workaround is to add a small sleep at the bottom of MonoGame:

   protected override void Draw(GameTime gameTime)
   {
        // Do your drawing deed here

        System.Threading.Thread.Sleep(1);   // Ensure we've exited VBlank first.
        base.Draw(gameTime);
    }

This doesn’t visually affect anything, and doesn’t slow down your framerate – but it fixes the erratic fluctuating input lag effect. It is simply a workaround for fast graphics cards to ensure that the MonoGame’s “SynchronizeWithVerticalRetrace” logic is working properly in sync with keyboard/mouse reads (rather than randomly delaying a keyboard/mouse read by 1/60sec = 16 milliseconds).

This workaround will keep your input lag consistent (non-varying) on ultrafast video cards such as Geforce GTX Titan.

(For the purposes of this post, VSYNC = Blanking Interval = Vertical Retrace = Vertical Sync Interval… These terms are the same for all intents of this specific post)

NOTE: This only is needed for full screen mode (graphics.IsFullScreen = true in conjunction with graphics.SynchronizeWithVerticalRetrace = true)