So far we've talked about the more visible changes in FEZ 1.12, from the button icons to the refresh rate...

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

... it's time we look into something a bit more audible.

When working on the original MonoGame build of FEZ for Windows, Renaud had the task of replacing the XACT code/content in FEZ with something else, as XACT had not been implemented at the time (it has since been mostly implemented in FNA). The result was a C# library called the OggStream:

http://theinstructionlimit.com/ogg-streaming-using-opentk-and-nvorbis

It essentially does the work of a standard OpenAL stream, but with a little glitter on top:

https://github.com/renaudbedard/MonoGame/blob/develop/MonoGame.Framework/Desktop/Audio/OggStream.cs

This is now one of the more popular solutions for C# games that stream Vorbis files, and it's even made its way into the MonoGame codebase as an official utility.

The problem is that it's not very good. In fact, it's actually pretty awful. For the most part it was quickly cobbled together to get something resembling the XACT music Cue behavior, with little concern for a lot of what we're going to go over in detail today. There are enough problems with it that I actually had to use chapters to get it all organized... are you ready?

XNA SPEC

The first obvious problem with the OggStream is that it's not really XNA4-compliant, so it shouldn't be in FNA like it is in the FEZ MonoGame branch. This introduces a major problem: OggStream has a direct dependency on OpenAL, which is already being wrangled by the FNA OpenALDevice, so we've got two conflicting libraries trying to manage resources in two different ways.

The funny thing is that audio streaming is actually an XNA4 feature. It's called the DynamicSoundEffectInstance:

https://msdn.microsoft.com/en-us/library/microsoft.xna.framework.audio.dynamicsoundeffectinstance.aspx

When separated from everything else, it's actually pretty simple to implement, and we have it in FNA:

https://github.com/FNA-XNA/FNA/blob/master/src/Audio/DynamicSoundEffectInstance.cs

So step 1 is to strip out all of the OpenAL code and use DynamicSoundEffectInstance instead. On top of simply having less to maintain on the game side, it's a lot more robust - DynamicSoundEffectInstance has been put through a LOT more stuff, one of which we'll be looking at in a moment. Code like this...

https://github.com/renaudbedard/MonoGame/blob/develop/MonoGame.Framework/Desktop/Audio/OggStream.cs#L180

... is now a single call to DynamicSoundEffectInstance.Pause(). Easy!

The only remaining task was the low-pass filter, which is not accessible through the XNA SoundEffect API. Luckily this was something that we needed internally for FNA, so I cleaned that work up and made it easy for FEZ to break into for use with the new OggStream:

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

With the system-level work done, we're now left with the Vorbis streaming, which brings us to the next step:

PERFORMANCE PART 1: VORBIS

OggStream uses NVorbis to stream Vorbis files. It's a 100% managed library that avoids the need for native code, so you link to that and you're done.

Rule #1 to make your managed code faster is to just write it in native code instead.

Vorbisfile is not a terribly complicated library, nor should it be. As of writing, the entire Vorbisfile# library (which is just C# entry points for the native Vorbisfile libs) is less than 300 lines, and that includes the license and documentation:

https://github.com/flibitijibibo/Vorbisfile-CS/blob/master/Vorbisfile.cs

You might be wondering how much work it was to move from NVorbis to Vorbisfile, and the answer is "basically not at all." The reason why is the same reason Vorbisfile# even exists: I actually wrote a Vorbis streamer too, as part of FNA's Song implementation:

https://github.com/FNA-XNA/FNA/blob/master/src/Media/Xiph/Song.cs

And what do you know, it also uses DynamicSoundEffectInstance!

On top of moving all the real decoding work to 100% native code, you'll notice that there are a LOT of optimizations made on the managed side as well; there's a static pinned buffer that is used for all streams and no reallocation is done on our end when data is decoded, so both CPU and memory performance is already dramatically better just by using this method instead. Additionally, the data is not recast to a different type when submitted to the AL stream, so we save a bunch of cycles and memory from that as well.

But cycles and memory aren't the only problem OggStream had:

PERFORMANCE PART 2: THE DISK

Admittedly this is less of a problem with OggStream as much as it has to do with what Renaud was doing to get Vorbis data into the AL. Note this line here:

https://github.com/renaudbedard/MonoGame/blob/develop/MonoGame.Framework/Desktop/Audio/OggStream.cs#L85

Notice that File.OpenRead call? FEZ players who have rummaged through the Content/ folder may find this a bit suspicious, as all of the game's music resides in a single Music.pak file. So what is it really opening?

If 1.11 crashed the last time you played it, have a look in your save/config folders for a folder called "3rcqng1i.djk". Yes, it's exactly what you're thinking... Up until now Renaud has been dumping Vorbis blobs into a temporary folder. If you ever wondered why the game arbitarily needed a lot more disk space than what the game itself was taking up, there's your reason.

On top of totally trashing your disk (sorry SSD owners!) you're also getting a big performance hit from having to read/write from the disk so damn much, when you ideally never want to touch the disk ever during a typical frame of gameplay unless the work is extremely optimized (see: virtualtexturing). The game eventually dumps all of this back into RAM anyway, so why bother with the files?

The answer has to do with how Vorbis streams are typically done. For Vorbisfile you usually open a stream with, what else, ov_fopen:

https://xiph.org/vorbis/doc/vorbisfile/ov_fopen.html

But there's a way you can override what type of input stream is being used, simply by using the more specialized ov_open_callbacks:

https://xiph.org/vorbis/doc/vorbisfile/ov_open_callbacks.html

This is one thing FNA didn't actually need, so Renaud was nice enough to write it in with some work on my part as well:

https://github.com/flibitijibibo/Vorbisfile-CS/commit/f029451b7b38c956363200512c6502391b226378

As a side note, I like how libogg went as far as making its own types like ogg_int64_t, but still used long for one of its critical functions... Renaud was struggling to figure out why a call was crashing so hard on Windows, until we had this conversation (paraphrased):

Renaud: Looks like the first parameter is what I expect my datasource to be, and the rest is garbage
Ethan: Here's a dumb idea: What if you turn "long ibytes" in ov_open_callbacks into an int instead. Let's play the Win32 sizeof(primitivetype) game!
Renaud: Well shit, that worked
Ethan: Fucking C#. sizeof(long) != sizeof(long) between C and C# and the marshaler didn't think to adjust that.

And sure enough, something like that is actually shipping today:

https://github.com/flibitijibibo/Vorbisfile-CS/blob/master/Vorbisfile.cs#L157

Thanks again, C#.

With some work on FEZ and ov_callbacks, the game now streams entirely from RAM - the disk is never touched for Vorbis streaming. Those of you who had trouble with performance due to the music streaming should find the game to be WAY faster than it was before.

But, there's still one more thing we have to look at...

PERFORMANCE PART 3: THREADS

What would a flibitPost about performance be without incessant whining about threads?

The use of threads in OggStream is its worst feature by a considerable margin. Most of our bug reports for FEZ had very little to do with the game itself, but rather the OggStreamers burning out one of the CPU cores over something that wasn't doing any real work at all. Even the disk thrashing got less reports than this. I mean, look at this... can you even parse any of it?

https://github.com/renaudbedard/MonoGame/blob/develop/MonoGame.Framework/Desktop/Audio/OggStream.cs#L366

Because I sure can't.

So like any good fix, we're just going to remove the threads, because there's a 0 percent chance that it's actually any faster than the singlethreaded path (see: basically everything using threads nowadays).

One of the nice things about ripping off FNA's Song is that we were able to take out the thread that we use for Song as part of XNA4 accuracy:

https://github.com/FNA-XNA/FNA/blob/master/src/Media/Xiph/Song.cs#L12

We know the game's never going to hang that hard, so we can skip the arbitrary thread and update this along with all the other game components without any special treatment. It functions exactly as it did before without burning out the CPU core, and it's still faster than the old solution with the help of the performance improvements described above.

There's exactly one item in the 1.12 OggStream that has threads involved, and that's in the initial precaching of new streams. Since we use the engine's threading system, this can be turned off with the game's new singlethreaded mode, so even those threads are optional and won't matter for machines that are fast enough.

CONCLUSION

The result of all this work is that almost none of the original OggStream code is in FEZ 1.12. Just about all of it was replaced with DynamicSoundEffectInstance and code taken directly from FNA's Song, and what's left is the API that Renaud originally wrote, implemented in an entirely different way. It's faster, more reliable, and makes appropriate use of XNA's features as well as the official native libraries for decoding Ogg Vorbis files.

My recommendation to any developer wanting to use the OggStream is to do what we did instead, and use a simple Vorbisfile# stream that pushes data to a DynamicSoundEffectInstance. It'll probably take you less than a day to write, debug, and verify as working, and you'll be happier for having that much less to maintain over the years you'll be working on your game.


Further reading this week is the FNA 1.0 press release... which hasn't been written yet. I still need to finalize it for FNA's release which will be happening in the next few days, so keep an eye out for the official release announcement. In addition to that, Christmas is next Friday, so we'll be taking a week off. Try to enjoy it!
Shared publiclyView activity