After looking at nothing but options menus for two solid weeks...

https://plus.google.com/+flibitijibibo/posts/Jh43KkHR1Yz
https://plus.google.com/+flibitijibibo/posts/48R4vB5MVPN

... it's about time we look at something that's not walls of text. Except of course, this will still be a wall of text, but you get the idea.

Some of you may be familiar with a statement I made a few months ago about timesteps:

https://plus.google.com/+flibitijibibo/posts/W5wEyFZRUwA

It's time to explain the new VSync option in FEZ, as I hinted about last week.

Being an XNA game, FEZ used the FixedTimeStep option by default, and so was forced to run at 60Hz. An option like VSync is irrelevant, as the game would be held up either by the sync or by a Sleep call:

https://github.com/FNA-XNA/FNA/blob/master/src/Game.cs#L475

But it gets worse. That number, 60Hz? It's actually not true... for FEZ specifically, the real number is... ready?

Approximately 58.8 Hz.

FEZ carries a deep, dark secret about its inner workings. Somehow, for the entirety of its original development for the Xbox 360, Renaud had the timestep not set to 16.6667 milliseconds, as it's set by default...

https://github.com/FNA-XNA/FNA/blob/master/src/Game.cs#L238

... but to exactly 1/60 seconds.

Wait, that's not how math works. Except it does if you're working with C# and TimeSpan:

http://www.flibitijibibo.com/fezfriday/csharpblows.png

So, in a very subtle way, the game was accidentally developed with a 17ms timestep. Thanks, C#.

This was actually found during the 360 version's development, but by the time it was discovered it was too late to redo everything and risk delaying the game even further. And thanks to the nature of floating point, the game was cursed with the tiniest of bugs in both its physics and its animations whenever the step wasn't exactly as the game wanted. It's been a bug that's plagued the game through the PC version as well, with Renaud doing rather dirty things to both MonoGame and FEZ internally to get the game working with 16.6667 milliseconds... here are just a few examples that I can share:

https://github.com/renaudbedard/MonoGame/commit/6292c96d52851b1a9cc518e2619fd3d1d21743a7
https://github.com/renaudbedard/MonoGame/commit/ba97b4fe7c7606d9c60b29612a35e235e1855760
https://github.com/renaudbedard/MonoGame/commit/bfc578f2f90e413993cf20444e8579322402a9d6
https://github.com/renaudbedard/MonoGame/commit/813303bc64ad4d7a7d6f5bb4fce76e90f6533009
https://github.com/renaudbedard/MonoGame/commit/4b23897fea999b7450ace081e98a5d0ee716afbb

The power-saving mode mentioned in the commits was particularly awful, as it in many cases did exactly what you wanted even for machines that didn't need energy efficiency. On both my development machine and my Dev Days BRIX Pro, the -ps option was far smoother than the default mode. Pretty annoying!

With the move to FNA, this work essentially got thrown out the window, for better or for worse. FNA has a very strict rule of not adding any game-specific hacks or workarounds no matter what the reason may be, so we're left with fixing the issue entirely in FEZ.

CAUTION: SPOILERS AHEAD. TO SKIP THEM, SCROLL TO THE NEXT LINE OF BOLDED CAPSLOCK TEXT.

To give a good example of why "just make it 60fps, lazy devs" doesn't quite work, here's the 32-cube cutscene at 60Hz and 58.8Hz... see if you can spot all the differences:

https://www.youtube.com/watch?v=PcJ4zj2zb0E
https://www.youtube.com/watch?v=hacN1QEl72M

OKAY, SPOILERS ARE CLEAR. ENJOY THE REST.

And that's just a bunch of animations. In-game it can get so, so much worse... mainly due to lack of determinism, which is why you're not looking at a video of it right now.

So the first thing we're stuck doing is forcing the timestep to be 17ms to avoid those bugs. But now we're back to the issue of stuttering when not in the power-saving mode, since the 17ms step means occasionally missing the sync and getting either getting bumped down to 30Hz or getting a bunch of tearing when using EXT_swap_control_tear.

Consider the following solution as an important lesson in keeping your engine's subsystems properly decoupled.

Fixing this problem is, for the most part, as easy as boiling it down to cause and effect. We have the game doing updates in 17ms intervals, and as a result the game gets visual stuttering and tearing, depending on the GL swap interval.

Notice anything weird about that? We've got a behavioral issue in the game logic that negatively affects the rendering behavior. As dumb as the update behavior is, it seems a little odd that the visuals should suffer for it.

One of the more sensible things that XNA does is provide time information for both the Update and the Draw stages for each tick:

https://msdn.microsoft.com/en-us/library/microsoft.xna.framework.game.update.aspx
https://msdn.microsoft.com/en-us/library/microsoft.xna.framework.game.draw.aspx

As it happens, FEZ actually uses these appropriately, and the Draw stage can actually have a variable timestep! So the first step to fixing this issue is to set FixedTimeStep option to false. For 60Hz displays this works for rendering, but updates are back to being a little wonky... and for 120Hz displays, for example, it's a total mess. This was something we had to deal with for 1.11 as well, and when you tried to use the normal timestep at 120...

https://www.youtube.com/watch?v=rdTAWu7wUF0

... Yeah. Anyway, it was still a problem. So updates still need to be fixed, but we can at least reduce this to just the Update stage. While a game's Update() function might look like this...

protected override void Update(GameTime gameTime)
{
    // Yeah, this override is a no-op.
    // Pseudocode, people!
    base.Update(gameTime);
}

... we can invent our own little fixed timestep in this function:

private static readonly TimeSpan force17 = TimeSpan.FromMilliseconds(17);
private static readonly TimeSpan time60 = TimeSpan.FromMilliseconds(16);
private TimeSpan lastUpdate = TimeSpan.Zero;
protected override void Update(GameTime gameTime)
{
    // Still pseudocode!
    TimeSpan realTime = gameTime.TotalGameTime;
    TimeSpan delta = realTime - lastUpdate;
    if (delta < time60) return;
    double deltaMS = delta.TotalMilliseconds;
    // Yes, 16.0. Need space for VSync irregularities
    while (deltaMS > 16.0)
    {
        base.Update(new GameTime(realTime, force17));
        deltaMS -= 16.0;
    }
    lastUpdate = realTime.Subtract(
        TimeSpan.FromMilliseconds(deltaMS)
    );
}

This sends the 17ms step that the Update stage wants, while at the same time sticking to 60Hz, the more accurate update interval. Since we can make our own homemade step, we may as well fix the actual interval, right?

Don't mess with time, kids. It gets really dangerous really fast, in the most subtle ways...

While the steps are still accurate, the time in which steps occur has changed, which has some nasty side effects. On top of real-time gameplay being affected (I bet speedrunners would have wondered why their first 1.12 run was so close to their PB), this ignores the fact that game time doesn't magically change real time.

Want to know where real time's going to conflict? Let's have a listen... listen carefully now!

https://www.youtube.com/watch?v=lv-oBlK1dqg
https://www.youtube.com/watch?v=AjMh08QUpWU

The sound mixer, and the sound files being played through it, don't care about our timestep. So with these microseconds of difference, you get what turns out to be a major collision of shots in a single cutscene. Kind of ruins the magic, doesn't it?

So unfortunately we can't be clever, and so we're left doing the proper thing with the proper timings:

private const double timestepMS = 17.0;
private static readonly TimeSpan timestep = TimeSpan.FromMilliseconds(timestepMS);
private TimeSpan lastUpdate = TimeSpan.Zero;
protected override void Update(GameTime gameTime)
{
    // Still pseudocode!
    TimeSpan realTime = gameTime.TotalGameTime;
    TimeSpan delta = realTime - lastUpdate;
    if (delta < timestep) return;
    double deltaMS = delta.TotalMilliseconds;
    while (deltaMS > timestepMS)
    {
        base.Update(new GameTime(realTime, timestep));
        deltaMS -= timestepMS;
    }
    lastUpdate = realTime.Subtract(
        TimeSpan.FromMilliseconds(deltaMS)
    );
}

I was actually worried that this might introduce stuttering again for visuals that depended on game logic, but so far we've not seen any cases, even for high refresh rates. So that was nice.

The end result of all this is that we now have both VSync support in addition to high refresh rate support, with no command line arguments needed for whatever mode you've got. 60, 120, 144, 240, as long as your GPU's fast enough it should work just fine.

Next week, we look at another issue that has plagued FEZ up until now - Renaud's somewhat famous OggStream.
Shared publiclyView activity