Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does Apple suggest do dispatch OpenGL commands in a serial background queue when this inevitably leads to crashes?

They suggest:

When using GCD, use a dedicated serial queue to dispatch commands to OpenGL ES; this can be used to replace the conventional mutex pattern.

I don't understand this recommendation. There is this conflict that I cannot solve:

When the app delegate of an app receives the -applicationWillResignActive call, it must immediately stop calling any OpenGL function.

If the app continues to call an OpenGL function after -applicationWillResignActive returned, the app will crash.

If I follow Apple's recommendation to call OpenGL functions in a serial background queue, I am faced with this seemingly unsolvable problem:

1) After I receive -applicationWillResignActive I must immediately stop calling any further OpenGL functions.

2) But because the serial queue is in the middle of processing a block of code in the background, sometimes the block of code would finish executing after -applicationWillResignActive returns, and the app crashes.

Here is an illustration showing the concurrent "blocks". The main thread receives a full stop message and has to prevent further calls to OpenGL ES. But unfortunately these happen in a background queue which cannot be halted in flight of working on a block:

|_____main thread: "STOP calling OpenGL ES!"_____|
 _____|_____drawing queue: "Draw!"_____|_____drawing queue: "Draw!"_____|

Technically I found no way to halt the background queue instantly and avoid further calls to OpenGL in the background. A submitted block of code once running keeps running.

The only solution I found was to NOT call OpenGL ES functions in the background. Instead, call them on the main thread to guarantee that they will never be called after the app lost access to the GPU.

So if it is okay to call OpenGL ES functions in the background, how do you ensure that they never get called after the app resigned active?

like image 222
openfrog Avatar asked Nov 02 '13 11:11

openfrog


2 Answers

Just wait in applicationWillResignActive for the queue to finish all enqueued operations using a dispatch group or a similar mechanism.

You can find an example in the documentation:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();

// Add a task to the group
dispatch_group_async(group, queue, ^{
   // Some asynchronous work
});

// Do some other work while the tasks execute.

// When you cannot make any more forward progress,
// wait on the group to block the current thread.
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

// Release the group when it is no longer needed.
dispatch_release(group);
like image 141
Sven Avatar answered Nov 15 '22 20:11

Sven


Beyond Sven's suggestion of using a dispatch group, I've gone a simpler route in the past by using a synchronous dispatch into your rendering serial queue within -applicationWillResignActive:

// Tell whatever is generating rendering operations to pause

dispatch_sync(openGLESSerialQueue, ^{
    [EAGLContext setCurrentContext:context];
    glFinish();

    // Whatever other cleanup is required
});

The synchronous dispatch here will block until all actions in the serial queue have finished, then it will run your code within that block. If you have a timer or some other source that's triggering new rendering blocks, I'd pause that first, in case it puts one last block on the queue.

As an added measure of safety, I employ glFinish() which blocks until all rendering has finished on the GPU (the PowerVR GPUs like deferring rendering as much as they can). For extremely long-running rendered frames I've occasionally seen crashes due to frame rendering still going even after all responsible OpenGL ES calls have finished. This prevents that.

I use this within a few different applications, and it pretty much eliminates crashes due to rendering when transitioning to the background. I know others use something similar with my GPUImage library (which also uses a serial background dispatch queue for its rendering) and it seems to work well for them.

The one thing you have to be cautious of, with this or any other solution that blocks until a background process is done, is that you open yourself up to deadlocks if you're not careful. If you have any synchronous dispatches back to the main queue within blocks on your background queue, you're going to want to remove those or try to make them asynchronous. If they're waiting on the main queue, and the main queue is waiting on the background queue, you'll freeze up quite nicely. Usually, it's pretty straightforward to avoid that.

like image 20
Brad Larson Avatar answered Nov 15 '22 20:11

Brad Larson