FNA-XNA / FNA

FNA - Accuracy-focused XNA4 reimplementation for open platforms
https://fna-xna.github.io/
2.63k stars 266 forks source link

Right way to use threading in ModernGLDevice #222

Closed BurgerBob closed 5 years ago

BurgerBob commented 5 years ago

In my game, I use a background thread to load, among other things, textures that will be used in the next level, while playing the current level.

Without changing #define options, I get performance problems on the main thread, because texture creation on the background thread is calling ForceToMainThread, and then the actions are executed after the buffer swap command, hanging the main thread if I loaded too many textures during the frame.

The documentation on threading defines is not very helpful, it talks about me feeling bad, things being flexible or dangerous, things that hang if I am being wreckless, etc ... https://github.com/FNA-XNA/FNA/wiki/6:-FNA-Build-Options

My question is: what is the right way to have a loading thread that only creates or destroys textures (nothing else, all the rendering commands are sent on the main thread) ? If I #define DISABLE_THREADING, am I allowed to load textures on a background thread ?

Thanks.

flibitijibibo commented 5 years ago

DISABLE_THREADING will rightfully crash if you attempt to do GL calls on a thread. THREADED_GL may work but "may" is not the same thing as "will".

If you're going to introduce threads you need to be absolutely sure that you are legitimately solving a real performance problem. Doesn't matter what scale you're at, if synchronous loading is fast enough to where a freeze frame between scenes doesn't matter, just live with it. Otherwise you need to seriously examine what part of your loading process is actually slow.

So, for example, what you're hitting is each GL call blocking the load thread one at a time. This is mandatory in a generic situation because we don't know what the rest of your engine depends on and where the memory is going, so we have to make sure each call happens immediately and in order, or else we risk memory we haven't uploaded/downloaded yet getting taken from us, resulting in various errors.

So what's actually slow? Is it the disk reading? Is it the XNB decompression? Is it a large texture with a dozen mip layers? Is it the allocation of a bunch of byte[]s causing a GC stutter during gameplay? What is it that's so slow that you're having to take an entire CPU core just to get another texture in memory?

Ultimately if you're going to start invading on the graphics driver's territory (which is already running asynchronously) you will need to take each and every step in your engine's resource loading seriously, including the building of the content in the first place. You will need to make your GL calls work on the main thread and have everything else run asynchronously, which means getting the resource loading thread to inform the main thread what GL resources it needs, then load the resources on the thread, then push the loaded data to the GL back on the main thread. Maybe you just need a Queue of loading delegate calls that's cycled through once per frame (similar to the OpenGLDevice GC queues), maybe your data's consistent enough to have static graphics resources which you can refresh with SetData calls using pinned memory.

Or you can just load the data when moving to the level and suck up the half-second load time. Should you decide not to heed the THREADED_GL warning (whether you use the define or not), pray you don't end up like these guys.

BurgerBob commented 5 years ago

Thanks a lot.

If I understand well, I should avoid calling OpenGL commands on a background thread, and disable threading for better performance and reliability. So I should synchronise the loading thread and the main thread myself, so that most of the work is done on the loading thread, then only the GL commands are called on the main thread, doing only CreateTexture2D and SetTextureData2D once per frame for exemple.

Texture loading requires reading on the disk, and in my case decompressing the data because the texture is in a zip archive. That is a task that is suited to be done on the background thread. I can do that and write the data in a byte[] in memory.

One annoyance is that the files are in xnb format. With the way the Texture2DReader is implemented, reading the xnb header, the texture data, creating the texture and setting the data are all done sequentially in the read method.

I guess I can bypass the content pipeline, and re-implement the xnb header reading, read the data with standard IO methods, and then SetData on the texture on the main thread ...

flibitijibibo commented 5 years ago

You may not even need to do the XNB stuff - if you pack to a zip you may be able to get away with something like Texture2D.FromStream/DDSFromStreamEXT using the zip stream directly, and read in the image data without the ContentManager at all. (Of course, if you really want to use ContentManager, you can make a ZIPContentManager that overrides OpenStream, ReadAsset, etc...)

BurgerBob commented 5 years ago

The problem is that Texture2D.FromStream still does the loading, CreateTexture and SetData in the same method.

It seems that bypassing the content manager entirely would be beneficial in my engine. The only XNA types I have to support are Texture2D, SpriteFont, Effect, Song and SoundEffect. It would be pretty trivial to load and construct them myself with some copy pasting from the content manager. It would allow me to remove a nasty hack I had to do to be able to unload each asset independently : I am wrapping each asset in a separate ContentManager.

flibitijibibo commented 5 years ago

Funnily enough the only one of those that requires ContentManager is SpriteFont; you have these functions to work with:

And if this transition away from ContentManager means not using SpriteFont anymore, that's also a pretty big win!

BurgerBob commented 5 years ago

Ok thanks a lot :)

We use spritefont very sporadically in the game, most of our texts involve rasterizing in a Texture2D with System.Drawing.

I am going to keep one ContentManager around to load these 2-3 SpriteFonts, because I am not going to re-implement these ContentReader.ReadObject.

flibitijibibo commented 5 years ago

If you want to kill two ugly birds with one pretty stone (System.Drawing is extremely unpleasant to port and SpriteFont is just plain ugly) consider SharpFont or stb_truetype to handle both cases:

https://github.com/Robmaister/SharpFont https://github.com/nothings/stb/blob/master/stb_truetype.h

flibitijibibo commented 5 years ago

Closing due to old age but I think I answered this...

BurgerBob commented 5 years ago

Thanks :)