Wednesday 12 August 2015

OpenGL Multithreading Basics

Hacks are bad

I recently wanted to put some effects in to my latest project that required render targets, including a text glowing effect. On other occasions where I had needed render-to-texture I had been able to pre-cache it without much issue on the main thread but to create the glow effect in this way would have lead to a hack in my code and a bad taste in my mouth. I wanted the effect to be created and cached on one of my worker threads, ideally. I started looking into what would be required to allow multiple threads performing graphical operations asynchronously. It would also be beneficial and less restrictive, from a high level coding point of view, in the future to have this functionality in-place for other effects and graphical operations.

Pthreads to the rescue

The current Graphics module was not thread safe, mostly because OpenGL is not thread safe. I started writing a very simple and basic mutex locking system that wrapped pthreads around a more elegant and minimal API.

I littered my graphics code with critical section entry/exit points and began the process of working through all the deadlocks from where I had consecutive lock calls. This wasn't that difficult but it started to become less obvious which lock calls were calling the deadlock gradually. To make this go quicker I added some assert functionality to check that a lock call didn't precede a previous lock call without an unlock first. I used pthread_key values to store a variable to say if the thread was locked. Following the assert on 'already locked' the rest of the errors were solved within minutes.

OpenGL thread context management

Next was to fix the random OpenGL crashes and improper rendering I was seeing due to improper OpenGL context management. An OpenGL context can only be used on a single thread at a time. Graphics critical sections must lock the graphics mutex and then set the OpenGL context and then unset the context (by setting it to 0 on iOS) before unlocking the mutex.

If you do not manage the OpenGL context in this way crashes and odd random rendering issues will start to happen. My text rendering was not working until I wrapped calls to it with critical sections.

The OpenGL context was set and unset through critical sections and everything seemed to work but I was slightly unsatisfied with the sheer number of critical section entries and exits in my code. To somewhat clean and simplify the code and also cut down on the number of mutex operations, I altered functions to simply assert that I was in the graphics critical section. I moved the mutex lock/unlock to higher level code. I minimised the code a little further by using instances of a class where constructor and destructor made calls to enter and exit critical sections, if required.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
void JDXCriticalSection::Enter (JDXKeyedMutex &keyedMutex)
{
    ASSERT_MESSAGE (!GetEntered (keyedMutex), "Cannot enter critical section on the same thread having not not exited first.");
    
    int* thisThreadVal = (int*) pthread_getspecific (keyedMutex.m_threadKey);
    
    if (nullptr == thisThreadVal)
    {
        thisThreadVal = new int (1);
    }
    else
    {
        ASSERT ((*thisThreadVal) == 2);
    }
    
    *thisThreadVal = 1;
    pthread_setspecific (keyedMutex.m_threadKey, thisThreadVal);
    
    pthread_mutex_lock (&keyedMutex.m_mutex);
}


void JDXCriticalSection::Exit (JDXKeyedMutex &keyedMutex)
{
    ASSERT_MESSAGE (GetEntered (keyedMutex), "Cannot exit critical section that has not being entered on the same thread");
    
    int* thisThreadVal = (int*) pthread_getspecific (keyedMutex.m_threadKey);
    
    ASSERT ((*thisThreadVal) == 1);
    
    *thisThreadVal = 2;
    pthread_setspecific (keyedMutex.m_threadKey, thisThreadVal);
    
    pthread_mutex_unlock (&keyedMutex.m_mutex);
}


bool JDXCriticalSection::GetEntered (JDXKeyedMutex &keyedMutex)
{
    int* thisThreadVal = (int*) pthread_getspecific (keyedMutex.m_threadKey);
    
    bool threadValueNotSet = (thisThreadVal == nullptr);
    if (threadValueNotSet)
    {
        return false;
    }
    
    bool threadValueSetEntered = ((*thisThreadVal) == 1);
    if (threadValueSetEntered)
    {
        return true;
    }
    
    return false;
}

No comments:

Post a Comment