SDL_Renderer calls from different thread possible?


#1

Hello, everyone.

One question keeps bothering me and I seek your wisdom.
Wall of text incoming, forgive me.

The question is about multithread drawing.
Just to note that I’ve read a lot of materials here in the forums and stackoverflow and this is not one of those
“I’m trying to do multithread rendering in my game for better speed 101”.

I have an linux ubuntu based application (a game basically), which also needs to communicate constantly
with several other hardware devices.

For more than a year I’ve kept everything on the main thread
(communications with hardware devices + event polling + update game state + rendering) while maintaining concrete 60 FPS.

Until now I uploaded all the textures to the GPU at program startup.
The problem came when I needed to use several really big textures (8192x8192px).
I had to implement dynamically texture loading/unloading. So I did and it worked.

Now the problem is when I need to upload one of those huge textures to the GPU (8192x8192px) with SDL_CreateTextureFromSurface() the program hangs for huge amount of time ~1000ms (no wonder with such huge texture).
When the call to SDL_CreateTextureFromSurface() returns control - the hardware communications that I need to support timed out long time ago.

So after so much time I finally decided to try and move the rendering to a secondary thread.
I think it worked (it is now completely finished but still…).

Main thread creates the window, spawns a rendering thread and sleeps for some time.
The secondary(renderer thread) creates the SDL_Renderer and sleeps on a condition_variable.
Main thread awakes and continues execution.

Main thread is responsible for event polling, updating the game state and maintaining the communication with the hardware devices.
No rendering related operation are ever made from main thread (it only stores draw “commands” in a commandQueue and drawDataBuffer).
2 copies of the commandQueue and drawDataBuffer are used so no almost zero-locking is achieved.
After all draw “commands” are stored the main thread locks on a mutex, switches the commandQueue pointers and wakes the second(rendering) thread that was sleeping on a condition_variable.
Rendering thread wakes, walks the commandQueue and performs rendering related code after which sleep again.
The process repeats for every frame.

SDL/OpenGL requires all rendering calls to come exactly from 1 thread.
The thread that created the SDL_Renderer(as far as I know?)
(or the thread that owns the current OpenGL context if you are directly using OpenGL).

If you are still reading this here comes my real question:
How come I can make successful calls from my main thread to SDL_CreateTextureFromSurface(), while it is the other thread that is related to rendering operations(the one that created the SDL_Renderer)?

Does the application suffer a penalty for “context switching” of some kind when I make calls from main thread?

Bonus question:
Is there a problem with me having the drawing thread as a second thread, while I keep the main thread to update the game state?
Is there some kind of penalty for not performing drawing in the main thread (in my case from the secondary rendering thread)?
Do you think I should “swap” their places -> draw in the main thread and make the secondary thread updating the game state and if yes - why?

Regards


#2

FWIW, that’s what I do: all rendering operations are done in the ‘main’ thread and what you call “updating the game state” (in my case it’s a language interpreter so that means computation, non-graphical input/output and pretty much everything else) in a secondary thread.

As for why, it’s because SDL2 isn’t explicit about precisely what the thread constraints are. Do rendering calls have to be made from the thread containing main(), from the thread that created the window, or from the thread that created the renderer? Is it even permitted to create the window and/or the renderer in a different thread? These things simply aren’t documented (as far as I know).

What’s even worse is that it can be platform-dependent. For example it seems that it’s normally OK to call SDL_SetWindowTitle() from another thread, but not (reliably) in MacOS. Similarly on most platforms it’s OK to call SDL_StartTextInput() and SDL_StopTextInput() from another thread, but not in iOS. And even SDL_ShowSimpleMessageBox(), which is supposed to be “callable at any time” in order to report an error, is thread specific on some platforms. :disappointed:

So in the absence of unambiguous documentation I do what seems safest: create my window and renderer in the main() thread. That’s not necessarily what I would prefer, and I have still come unstuck because of lack of clarity over what constitutes a ‘rendering call’ (I believe that in general calls involving surfaces are not thread-dependent but those involving textures are, but even that isn’t entirely consistent as you have found).


#3

In the case of SDL_Render specifically, it’s documented to only work on the main thread. The top of the header file says this:

These functions must be called from the main thread.
See this bug for details: http://bugzilla.libsdl.org/show_bug.cgi?id=1995

For full cross-platform compatibility you can assume any windowing-related function can only be called on the main thread as well. Some platforms do allow you to call widowing functions on other threads (though they still aren’t thread-safe even then), but not every platform.


#4

I had a slightly similar problem, but not in SDL_CreateTextureFromSurface but the loading from file itself. It wont help with slow speeds of SDL_CreateTextureFromSurface, but eh, talking bout it anyways.

What i found to be relatively safe to do for resource loading is doing as much as safely possible on a secondary thread (in my case i realised this via a thread pool that worked some sort of message queue ).
Load the files into a surface from a different thread and hand it to the main thread to do the conversion to SDL_Texture. If you then limit the number of textures to convert every frame or set some hard time constraints limiting the amount of textures loaded, you can somewhat reduce the lags it introduces.


#5

@rtrussell, thank you for sharing your experience. Much appreciated.

@slime, thank you for being concrete about stating “For full cross-platform compatibility you can assume any windowing-related function can only be called on the main thread as well”.
About SDL_Renderer functions “must” be called from the main thread:
I’ve seen it, but I hoped that what I did will work too (creating the SDL_Renderer in second thread and perform rendering operations from there). At least it worked fine on the linux ubuntu that I am using.
Guess I will have to “swap” the threads places after all.

@mkalte, awesome idea, thank you.
I am already doing this in my application and it works flawlessly. Spawning N-1 worker threads calling IMG_Load() and transferring the created SDL_Surface’s with a thread safe queue to the Rendering thread, which uploads them to the GPU with SDL_CreateTextureFromSurface().
This works OK for small to medium textures, but when I faced this monster of 8192x8192px(please don’t ask me why I have such big texture :D) the call to SDL_CreateTextureFromSurface() took about a ~1000ms of time. No easy workaround for this, unless I try to “chop” to the huge textures to several small ones and then at run-time try to “combine” it again together.

Thank you all for your replies.
If someone else wants to share his/hers experience - feel free to do so.


#6

Whilst 8192x8192 might once have been considered a “monster” I really don’t think it is exceptional these days. 50+ Megapixel images from still cameras are commonplace, and ‘8K’ TV (7680x4320 pixels at up to 60 frames a second) is the next ‘big thing’.