Over the last month or so you've been reading about what we've been doing to FEZ to make the user experience as nice as possible, while also cleaning up a lot of the system-level stuff to make things faster, more accessible, and so on. FEZ 1.12 has been one of two times that I've actually done a hefty amount of work on the game side, but if you look at the 1.12 commit graph...http://www.flibitijibibo.com/fezfriday/112commits.png
... there's actually not a whole lot on my end. Renaud's still doing pretty much all the work on game logic patches, while I've been doing all these things that you've been reading about. There's definitely a lot going on in those changes, so where is it all going?
This week we get to talk a bit more about FNA, which was the main reason we started 1.12 in the first place.
Even on Linux/OSX, where a lot of code was taken from FNA, the MonoGame branch that FEZ has lived on up until now has been plagued with a lot of issues. Some were fixed in upstream MonoGame, some were fixed in FNA, and until recently some issues hadn't been fixed anywhere. These XNA replacements are always going to have issues in some way or another, and they're always going to be changing over time, so the question you have to ask is: "What is going to allow me to keep this game running reliably with the minimal amount of work for as long as people want to play this game?"
The first part of the question always seems easy to most developers, but when you bring up the problem of preservation the temperature of the room will drop by a good 10 degrees. XNA developers in particular ended up being a pretty good example of what happens when the long term isn't well thought-out; after 4 years of silence the library was put out of its misery and developers were left to figure out how to recover their development environment. Most devs opted to just use Unity, and we'll see if history repeats itself once Unity's 15 minutes are up, but this still left us with the previously released XNA games.
Back when Renaud started the PC version of FEZ, MonoGame happened to be the only option, so he ended up basing the port around that library knowing that multiplatform support would be a concern later on. Much like the early days of MonoGame-SDL2, which had been in development at the same time as FEZ PC, some of the uglier inaccuracies had to get worked around in order for something
resembling a finished product to show up, and eventually the PC version made its way out. Unlike
MG-SDL2, however, the FEZ MonoGame branch was left on its own to suffer from the same bit-rot that just about every other MonoGame branch has suffered, be it from small forks like the one used in DLC Quest (which, by the way, still has no Linux version despite advertised Linux support from the library even back then) to huge forks like Supergiant's version for Bastion, and the other
other version used for Transistor. The level of control they had over the source allowed them to make what they needed to make, but as soon as the product was done, so was the library.
XNA games really missed the opportunity to just stick with how they were written to begin with, while having XNA itself be allowed to evolve with the frequently changing platform ecosystems, to the benefit of the XNA game library.
FNA aims to solve this problem. Games like FEZ shouldn't be left to rot just because third-party libraries were given up on, and there needs to be a way to give every XNA game this opportunity.
With the games as the focus rather than the library itself, the development model has been the exact opposite of how XNA games have typically been ported. Instead of the game being contorted to the library's whims, it has been the primary goal of FNA to develop the library in service of the games first
. The former is MUCH easier than the latter, however; it would be a whole lot easier to just muck with your depth bias values in-game than to actually sit down and discover that D3D->GL depth bias values are completely
different and figure out that you have to do something like this to retain accurate rendering behavior for the original values:https://github.com/FNA-XNA/FNA/blob/master/src/Graphics/OpenGLDevice.cs#L1583https://github.com/FNA-XNA/FNA/blob/master/src/Graphics/OpenGLDevice.cs#L4095
And there are tons of things like this. From writing new features to making old features more accurate, there's a LOT that FNA has to do to preserve what XNA was doing and keep games running as they were before.
The process has essentially been to take one XNA game at a time, port it by making FNA work properly with the data, then move on to the next game with the slightly-more-refined library, using the older games as regression tests. That depth bias code is actually one such example of a game-driven change; FEZ was the game that introduced a need for more accurate glPolygonOffset calls after we undid the workaround used for the old MonoGame branch.
It's been a very slow process (spanning over 3 years as of writing), but so far it has exceeded everyone's expectations of what we could really do without the XNA sources sitting in front of us. People expected certain games to work well after a while, but what they didn't consider was that writing accurate behavior the first time meant never having to write it again, making ports marginally easier every time until we were able to turn "speedporting" into a real thing:https://www.youtube.com/watch?v=02wn1w53q1E
In the case of FEZ, it not only turned out to be a very simple process of moving from MonoGame to FNA (despite the time spent undoing workarounds), but the work put into previous games allowed us to use features that weren't available at the time (which I've talked about in previous posts).
But it doesn't end there... it's one thing to have new features working well, but what if you have old features working well too?
Pop Quiz: Name three ways to get the dimensions of the screen in XNA.
Answer: There's only three ways if you're super vague about what a "screen" is. It could mean the size of the window, which is what I imagine many of you were thinking. There is GameWindow.ClientBounds...https://msdn.microsoft.com/en-us/library/microsoft.xna.framework.gamewindow.clientbounds.aspx
... but perhaps you were thinking of "screen" as "display"? Don't worry, for that we have GraphicsDevice.DisplayMode...https://msdn.microsoft.com/en-us/library/microsoft.xna.framework.graphics.graphicsdevice.displaymode.aspx
... but wait, there's one more! Of course, these are just windows and displays, but what about the actual rendering context? That has a "screen" too, known as the backbuffer, which you can get via PresentationParameters.BackBufferWidth/Height:https://msdn.microsoft.com/en-us/library/microsoft.xna.framework.graphics.presentationparameters.backbufferwidth.aspxhttps://msdn.microsoft.com/en-us/library/microsoft.xna.framework.graphics.presentationparameters.backbufferheight.aspx
All of these measurements have
to be accurate. You may assume that these could all just derive from the same value, but in some cases, all three may be completely different!
Consider an XNA game running at 800x600, fullscreen, on a 1920x1200 display. Think about the above ways to get each set of dimensions, then try to guess what each size is. Ready?
GL Context: Indeterminant based on the above information
Annoyed? Don't feel bad, you weren't the one who had to reimplement this.
Recall that when we were talking about fullscreen a few weeks ago, I mentioned that FNA uses SDL_WINDOW_FULLSCREEN_DESKTOP and a faux-backbuffer to simulate lower-res fullscreen for higher-res displays, for a better user experience. Keep in mind that this is done totally behind the game's back, and so when a game asks what the game's display is running at, we have to be a bit dastardly and tell it that we're running the display at 800x600, the intended resolution:https://github.com/FNA-XNA/FNA/blob/master/src/Graphics/GraphicsDevice.cs#L71
As a sidenote, note the case for when we're not in fullscreen; that your bonus fourth way to get screen dimensions, this time it's the desktop resolution:https://msdn.microsoft.com/en-us/library/microsoft.xna.framework.graphics.graphicsadapter.currentdisplaymode.aspx
That's all fine and good for the display, but why are we being honest for the window? GameWindow is actually an abstract class for platform code (i.e. Win32 or Xbox 360 stuff) to sit underneath of, so in this case we're using this as an excuse to report SDL2_GameWindow values for developers who need the platform-specific behavior, recognizing that this code was volatile even for XNA's intended targets.
Then there's the backbuffer size... this is where things get really fun:
The backbuffer can actually vary in size, completely independent of what the window size is. Those of you who are familiar with GameWindow.AllowUserResizing are probably more than familiar with this after discovering that the rendering viewport didn't actually change with the window unless you actually did something about it in your own code; get ready to learn how much you can mess with that!
The faux-backbuffer in FNA is technically just for sizing low-res fullscreen images to high-res displays, but the way we turn the faux-backbuffer on is by comparing the window size with the backbuffer size, enabling it if the values are different. It doesn't actually care about the window mode, just the size! So coincidentally, this happened to be a great way to support backbuffers of varying size even for windowed mode... and there's a great use case:
Last week I mentioned that FEZ was going to be getting a pixel-perfect integer scale option, and after some discussion with Renaud, we've opted to add an integer downscale
option as well. So if you don't want to have letterboxing or possible point sampling artifacts, we're also going to give you an option to render the higher
integer scale and use linear downsampling to scale it to your window size. The way we do this is by setting the window size as usual, then resetting the GraphicsDevice an additional time to set the backbuffer width/height to the higher values, which will turn on the faux-backbuffer and automatically downscale the high-res image to your window resolution.
No fancy code was required to make this happen! The existing features and their robust behavior allowed us to get this option working in a lunch break.
But it doesn't end there
, either. As I hinted earlier, sometimes things change even after they've been written, so what got polished for FEZ?
In short: FEZ forced FNA to get really really fast
When I first booted FEZ-FNA, I had it running at about ~100fps. Not bad for a C# game, and while there were a lot of GL calls being made, it didn't make a whole lot of sense for a machine as beefy as mine. apitrace didn't reveal a lot of CPU/GPU sync points, so the main goal was to optimize on the CPU side. A handful of CPU optimizations were made over time and we eventually got to around ~120fps, but we're reasonably efficient on both the GPU and CPU though, so what are we missing?
The answer turned out to be memory. And we found out with a completely different game.
A common complaint I got from a handful of FNA developers profiling their games was that the Effects subsystem was stupidly slow, even for basic shaders. At the time, the Big Bastard Function was the reimplementation of D3DXEffect::CommitChanges, which looked like this:https://github.com/flibitijibibo/MojoShader/blob/8d6618d0bdb4448f8fcaf0327df92bdd0d3a015f/mojoshader_opengl.c#L2802Jesus Christ that's a lot of code.
The cutest thing about this was how basically everything was just inlined via macros in a desperate attempt to make it faster. But guess what, sometimes compilers don't inline for a reason... in this case, it turned out having this supermassive function was introducing a lot of pressure on the registers, and pulling the biggest blob into a function turned out to be slightly faster:https://github.com/flibitijibibo/MojoShader/commit/83f1ef3195235bd0ca88bfe8a694edb84ea62cd1https://github.com/flibitijibibo/MojoShader/commit/84690a1bad2a1f8b995c5088dc226cfc7f118f7c
But you had to have a POS machine to notice the difference. That's pretty much how every rewrite turned out to be: a bunch of microoptimizations that went as far as rewriting the function to use "do/while" loops over "for" loops to remove literally one
bool check in each case:https://github.com/flibitijibibo/MojoShader/commit/800b6d8e633da5b022bc249c24e83cc693ac5c7b
But for one game, there was a microoptimization that turned out to be not-so-micro:https://github.com/flibitijibibo/MojoShader/commit/bfd3188a52f098d61e164b4ce5f02167fe44b4fe
For some reason, this gave a really hefty boost for one of the more bloated shaders that had a bunch of bools in them (and nobody ever used bools in shaders, except for this one game). Suddenly it was a race to see how many of these I could get rid of... then I found this wonderful code I had written when originally writing the Effect support in MojoShader:https://github.com/flibitijibibo/MojoShader/blob/8d6618d0bdb4448f8fcaf0327df92bdd0d3a015f/mojoshader_opengl.c#L2877
Every time we apply a new effect we wipe MojoShader's constant buffers clean to ensure that the shader constants are accurate when we submit them. Wanna know how big those buffers are?https://github.com/flibitijibibo/MojoShader/blob/8d6618d0bdb4448f8fcaf0327df92bdd0d3a015f/mojoshader_opengl.c#L144https://github.com/flibitijibibo/MojoShader/blob/8d6618d0bdb4448f8fcaf0327df92bdd0d3a015f/mojoshader_opengl.c#L131
So in the end we're setting... ~331KB all to 0 every time a shader is being applied.
You youngsters out there probably don't think much of that, but even if that doesn't make your face bleed, consider that the actual buffer sizes of, say, the shaders in FEZ are less than a tenth of that. So obviously this is a complete waste, and it turns out that we've had the size of the program's constant buffer this whole time, so we use that...https://github.com/flibitijibibo/MojoShader/commit/3d0bde479d68afa6f030baa47f389d7e6d07183c
... and hey, while we're avoiding redundant memsets, let's avoid doing that to the graphics driver too:https://github.com/flibitijibibo/MojoShader/commit/a1dc110de4cd5a1f702e3315dd459b8047da0914
But hey, it's only a few hundred KB, I'm sure this won't be a huge deal and oh my god why is FEZ suddenly running at 500fps now, Renaud try this update and let me know if you see anything weird and what do you mean it's maxing out FRAPS what are you talking about
Developers who consider memory management to be a microoptimization should probably consider situations like this before handing off all of this to a CLR.
This was a huge win for everyone that tried it - people were reporting speeds comparable to XNA for the first time in FNA's history, so what might have been restricted to one game with the old development model ended up benefitting dozens
of games, including many that previously had reputations for running very poorly on Linux and OSX. And in the case of FEZ, it means that Linux, OSX, and
Windows now run a whole lot faster, especially on low-end machines.
My hope is that improvements like these, along with games getting better and better through FNA over time, will allow FNA to find its place as something other than "the FNA version of MonoGame*".
Next week we'll be making a return to the FEZ code, revisiting some of the graphics options one more time.
This week's further reading is just the FNA post from yesterday, in case you didn't care about it until today:https://plus.google.com/+flibitijibibo/posts/L7dFrURPPEz
* - This is an actual quote I found when searching for FNA on Google. I'm sure you can still find it somewhere.