26
votes

I'm writing a 3D model viewer application as a hobby project, and also as a test platform to try out different rendering techniques. I'm using SDL to handle window management and events, and OpenGL for the 3D rendering. The first iteration of my program was single-threaded, and ran well enough. However, I noticed that the single-threaded program caused the system to become very sluggish/laggy. My solution was to move all of the rendering code into a different thread, thereby freeing the main thread to handle events and prevent the app from becoming unresponsive.

This solution worked intermittently, the program frequently crashed due to a changing (and to my mind bizarre) set of errors coming mainly from the X window system. This led me to question my initial assumption that as long as all of my OpenGL calls took place in the thread where the context was created, everything should still work out. After spending the better part of a day searching the internet for an answer, I am thoroughly stumped.

More succinctly: Is it possible to perform 3D rendering using OpenGL in a thread other than the main thread? Can I still use a cross-platform windowing library such as SDL or GLFW with this configuration? Is there a better way to do what I'm trying to do?

So far I've been developing on Linux (Ubuntu 11.04) using C++, although I am also comfortable with Java and Python if there is a solution that works better in those languages.

UPDATE: As requested, some clarifications:

  • When I say "The system becomes sluggish" I mean interacting with the desktop (dragging windows, interacting with the panel, etc) becomes much slower than normal. Moving my application's window takes time on the order of seconds, and other interactions are just slow enough to be annoying.
  • As for interference with a compositing window manager... I am using the GNOME shell that ships with Ubuntu 11.04 (staying away from Unity for now...) and I couldn't find any options to disable desktop effects such as there was in previous distributions. I assume this means I'm not using a compositing window manager...although I could be very wrong.
  • I believe the "X errors" are server errors due to the error messages I'm getting at the terminal. More details below.

The errors I get with the multi-threaded version of my app:

XIO: fatal IO error 11 (Resource temporarily unavailable) on X server ":0.0" after 73 requests (73 known processed) with 0 events remaining.

X Error of failed request: BadColor (invalid Colormap parameter) Major opcode of failed request: 79 (X_FreeColormap) Resource id in failed request: 0x4600001 Serial number of failed request: 72 Current serial number in output stream: 73

Game: ../../src/xcb_io.c:140: dequeue_pending_request: Assertion `req == dpy->xcb->pending_requests' failed. Aborted

I always get one of the three errors above, which one I get varies, apparently at random, which (to my eyes) would appear to confirm that my issue does in fact stem from my use of threads. Keep in mind that I'm learning as I go along, so there is a very good chance that in my ignorance I've something rather stupid along the way.

SOLUTION: For anyone who is having a similar issue, I solved my problem by moving my call to SDL_Init(SDL_INIT_VIDEO) to the rendering thread, and locking the context initialization using a mutex. This ensures that the context is created in the thread that will be using it, and it prevents the main loop from starting before initialization tasks have finished. A simplified outline of the startup procedure:

1) Main thread initializes struct which will be shared between the two threads, and which contains a mutex.
2) Main thread spawns render thread and sleeps for a brief period (1-5ms), giving the render thread time to lock the mutex. After this pause, the main thread blocks while trying to lock the mutex.
3) Render thread locks mutex, initializes SDL's video subsystem and creates OpenGL context.
4) Render thread unlocks mutex and enters its "render loop".
5) The main thread is no longer blocked, so it locks and unlocks the mutex before finishing its initialization step.

Be sure and read the answers and comments, there is a lot of useful information there.

6
Not sure about X, but on Win32 this is allowed. The context can only be active on one thread, but it sounds like you're respecting that rule.Ben Voigt
That's the impression I got from my research. Ideally I would really like to have my app be cross-platform...if for no other reason than I want to deal with the windows API as little as possible =Prjacks
Can't blame you for that... although I've found these aspects of the Win32 API (event dispatch) to be far superior. I wish Linux had a MsgWaitForMultipleObjectsEx equivalent (maybe with a shorter name). Like poll, but capable of waiting on files, child processes, timers, threads, mutexes, and UI messages.Ben Voigt
You should be careful in step 1 of your solution. There is a chance that the "brief period" is not long enough so to be sure you should use a flag indicating if the render thread has initialised. In the main thread, check this flag and if it is not set then the thread should wait for a signal/broadcast from the render thread.DrYap
@BenVoigt I know this is an old thread, but you shouldn't compare linux and the win32 api. You should compare the win32 api and x11RamblingMad

6 Answers

7
votes

As long as the OpenGL context is touched from only one thread at a time, you should not run into any problems. You said even your single threaded program made your system sluggish. Does that mean the whole system or only your own application? The worst that should happen in a single threaded OpenGL program is, that processing user inputs for that one program gets laggy but the rest of the system is not affected.

If you use some compositing window manager (Compiz, KDE4 kwin), please try out what happens if you disable all compositing effects.

When you say X errors do you mean client side errors, or errors reported in the X server log? The latter case should not happen, because any kind of kind of malformed X command stream the X server must be able to cope with and at most emit a warning. If it (the X server) crashes this is a bug and should reported to X.org.

If your program crashes, then there's something wrong in its interaction with X; in that case please provide us with the error output in its variations.

3
votes

What I did in a similar situation was to keep my OpenGL calls in the main thread but move the vertex arrays preparation to a separate thread (or threads).

Basically, if you manage to separate the cpu intensive stuff from the OpenGL calls you don't have to worry about the unfortunately dubious OpenGL multithreading.

It worked out beautifully for me.

3
votes

Just in case - the X-Server has its' own sync subsystem. Try following while drawing: man XInitThreads - for initialization
man XLockDisplay/XUnlockDisplay -- for drawing (not sure for events processing);

1
votes

I was getting one of your errors:

../../src/xcb_io.c:140: dequeue_pending_request: Assertion `req == 
    dpy->xcb->pending_requests' failed. Aborted

and a whole host of different ones as well. Turns out that SDL_PollEvent needs an a pointer with initialized memory. So this fails:

SDL_Event *event;
SDL_PollEvent(event);

while this works:

SDL_Event event;
SDL_PollEvent(&event);

In case anyone else runs across this from google.

1
votes

This is half an answer and half a question.

Rendering in SDL in a separate thread is possible. It works usually on any OS. What you need to do is, that you make sure you make the GL context current when the render thread takes over. At the same time, before you do so, you need to release it from the main thread, e.g.:

Called from the main thread:

void Renderer::Init()
{
#ifdef _WIN32
    m_CurrentContext = wglGetCurrentContext();
    m_CurrentDC      = wglGetCurrentDC();
    // release current context
    wglMakeCurrent( nullptr, nullptr );
#endif
#ifdef __linux__
    if (!XInitThreads())
    {
        THROW( "XLib is not thread safe." );
    }
    SDL_SysWMinfo wm_info;
    SDL_VERSION( &wm_info.version );
    if ( SDL_GetWMInfo( &wm_info ) ) {
        Display *display = wm_info.info.x11.gfxdisplay;
        m_CurrentContext = glXGetCurrentContext();
        ASSERT( m_CurrentContext, "Error! No current GL context!" );
        glXMakeCurrent( display, None, nullptr );
        XSync( display, false );
    }
#endif
}

Called from the render thread:

void Renderer::InitGL()
{
    // This is important! Our renderer runs its own render thread
    // All
#ifdef _WIN32
    wglMakeCurrent(m_CurrentDC,m_CurrentContext);
#endif
#ifdef __linux__
    SDL_SysWMinfo wm_info;
    SDL_VERSION( &wm_info.version );
    if ( SDL_GetWMInfo( &wm_info ) ) {
        Display *display = wm_info.info.x11.gfxdisplay;
        Window   window  = wm_info.info.x11.window;
        glXMakeCurrent( display, window, m_CurrentContext );
        XSync( display, false );
    }
#endif
    // Init GLEW - we need this to use OGL extensions (e.g. for VBOs)
    GLenum err = glewInit();
    ASSERT( GLEW_OK == err, "Error: %s\n", glewGetErrorString(err) );

The risks here is, that SDL does not have a native MakeCurrent() function, unfortunately. So, we have to poke around a little in SDL internals (1.2, 1.3 might have solved this by now).

And one problem remains, that for some reason, I run into a problem when SDL is shutting down. Maybe someone can tell me how to safely release the context when the thread terminates.

1
votes
  1. C++, SDL, OpenGl:::
    1. on main thread: SDL_CreateWindow( );
    2. SDL_CreateSemaphore( );
    3. SDL_SemWait( );
    4. on renderThread: SDL_CreateThread( run, "rendererThread", (void*)this )
    5. SDL_GL_CreateContext( )
    6. "initialize the rest of openGl and glew"
    7. SDL_SemPost( ) //unlock the previously created semaphore
    8. P.S: SDL_CreateThread( ) only takes functions as its first parameter not methods, if a method is wanted than you simulate a method/function in your class by making it a friend function. this way it will have method traits while still able to be used as a functor for the SDL_CreateThread( ).
    9. P.S.S: inside of the "run( void* data )" created for the thread, the "(void*)" this is important and in order to re-obtain "this" inside of the function this line is needed "ClassName* me = (ClassName*)data;"