You've probably seen this: You start a game. After the loading is complete, the game runs smooth for a brief period of time, then it suddenly starts stuttering significantly for a few seconds, culminating in a automated banner: "Welcome back to GameCenter". If it's an action game, you may just have lost a life to the stutter.
Last week, I tried to investigate this, and a potential cause was revealed to me: GameCenter is running in the same NSRunLoop as everything else, on the main thread. Hence, when it connects, performs the SSL authentication, encryption, decryption, it blocks the main thread. Apparently, this costs enough time to delay the rendering.
Not only was this cause revealed to me, but also a potential solution: Put the entire OpenGL rendering into a separate thread, so it's not tied to the temper of the NSRunLoop and it's many potential input sources. So I set out to try this.
Multithreading OpenGL Requirements
Writing a multithreaded OpenGL renderer isn't trivial. And due to my perfectionism, I wanted to do it right. That means:
- Clean implementation
- Fires exactly at display refresh, using CADisplayLink
- As simple as possible, trying to avoid any low-level multithreading if possible
- Since a single EAGLContext can only be used on one thread at a time, ideally everything should be only on the secondary thread, and no code should run on the main thread.
These requirements led me to my first approach.
I love Grand Central Dispatch (GCD). It's a great way to parallelize and defer code execution. And some tests I conducted showed that the overhead caused by blocks is tiny.
Hence, I created a new serial queue (as suggested by the Apple iOS OpenGL documentation), and essentially queued all calls to OpenGL in blocks on that queue, which runs on a different thread. One of the first problems that arose was that setting up CADisplayLink on that thread doesn't work, because the GCD queue threads don't have a NSRunLoop, which is what CADisplayLink uses. Hence, my display link callback wouldn't be called at all.
However, since CADisplayLink doesn't call any OpenGL code by itself, I moved it onto the main thread, and then dispatched the draw event onto the rendering queue from there. Now the callback got triggered at 60hz, as expected. But the rendering didn't work. I'm pretty sure I enforced the right EAGLContext on every draw event. And after a couple of dispatch_asyncs, the [context presentRenderbuffer:] function would stall for 1s at a time. I fiddled around a lot with this, but couldn't get it to work.
If change the rendering queue to run on the main thread (by using the main GCD queue), it magically worked well. But everything was executed on the main thread, which set me back to beginning. That's as far as I've gotten with the GCD approach.
Using a separate NSThread
My second attempt then involved an NSThread. The setup was very simple: When the GLView was created, I created the new thread, and inside I started a NSRunLoop. Then, I set up the CADisplayLink to run on this run loop. And it worked. The CADisplayLink fired reliably and the scene was rendered correctly. However, there was a small issue: There seems to be no (reliable) way to terminate the run loop. Hence, once started, I couldn't stop the rendering anymore. That's not really what I needed.
A classical approach
This is as far as I've gotten. The next thing I want to try is to create an NSThread that runs a very simple loop. It sleeps until a semaphore is fired, then renders one frame, and then sleeps again. Then, on the main thread, I run the display link, and trigger the semaphore whenever the display link fires. This is very old school, but at least I can turn it off at any time, and it appears to match all the requirements I have.
What seemed like a nice little afternoon project starts to cost a lot of time now. However, considering that moving the rendering to a separate thread seems like the best solution for the GameCenter login stuttering problem, and any other stutter that is caused by high runloop latency, it seems worth the effort.
Once I've found a good solution, I plan to make it open-source, and also include my performance monitor thingie that I described in my last post.
To close this post, I'd like to ask everyone out there: Have you written a threaded renderer on iOS? How did you make it work? Did it work reliably?