Note that there are some explanatory texts on larger screens.

plurals
  1. POHow to draw to GLKit's OpenGL ES context asynchronously from a Grand Central Dispatch Queue on iOS
    primarykey
    data
    text
    <p>I'm trying to move lengthy OpenGL draw operations into a GCD queue so I can get other stuff done while the GPU grinds along. I would <em>much rather</em> prefer to do this with GCD vs. adding real threading to my app. Literally all I want to do is be able to </p> <ul> <li>Not block on a glDrawArrays() call so the rest of the UI can stay responsive when GL rendering gets very slow.</li> <li>Drop glDrawArrays() calls when we aren't finishing them anyway (don't build up a queue of frames that just grows and grows)</li> </ul> <p>On Apple's website, the docs say:</p> <blockquote> <p>GCD and NSOperationQueue objects can execute your tasks on a thread of their choosing. They may create a thread specifically for that task, or they may reuse an existing thread. But in either case, you cannot guarantee which thread executes the task. For an OpenGL ES app, that means:</p> <ul> <li>Each task must set the context before executing any OpenGL ES commands.</li> <li>Two tasks that access the same context may never execute simultaneously.</li> <li>Each task should clear the thread’s context before exiting.</li> </ul> </blockquote> <p>Sounds pretty straightforward.</p> <p>For the sake of simplicity in this question, I'm starting with a new, bone-stock version of the Apple template that comes up in the "New Project" dialog for "OpenGL ES" game. When you instantiate it, compile, and run, you should see two cubes rotating on a grey field.</p> <p>To that code, I have added a GCD queue. Starting with the interface section of <code>ViewController.m</code>:</p> <pre><code>dispatch_queue_t openGLESDrawQueue; </code></pre> <p>Then setting those up in <code>ViewController</code> <code>viewDidLoad</code>:</p> <pre><code>openGLESDrawQueue = dispatch_queue_create("GLDRAWINGQUEUE", NULL); </code></pre> <p>Finally, I make these very small changes to the <code>drawInRect</code> method that CADisplayLink ends up triggering:</p> <pre><code>- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect { void (^glDrawBlock)(void) = ^{ [EAGLContext setCurrentContext:self.context]; glClearColor(0.65f, 0.65f, 0.65f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glBindVertexArrayOES(_vertexArray); // Render the object with GLKit [self.effect prepareToDraw]; glDrawArrays(GL_TRIANGLES, 0, 36); // Render the object again with ES2 glUseProgram(_program); glUniformMatrix4fv(uniforms[UNIFORM_MODELVIEWPROJECTION_MATRIX], 1, 0, _modelViewProjectionMatrix.m); glUniformMatrix3fv(uniforms[UNIFORM_NORMAL_MATRIX], 1, 0, _normalMatrix.m); glDrawArrays(GL_TRIANGLES, 0, 36); }; dispatch_async(openGLESDrawQueue, glDrawBlock); } </code></pre> <p>This does not work. The drawing goes crazy. Drawing with the same block with <code>dispatch_sync()</code> works fine, though.</p> <p>Let's double check Apple's list:</p> <ul> <li>Each task must set the context before executing any OpenGL ES commands. <ul> <li>Ok. I'm setting the context. It's Objective-C object pointers that have a lifetime longer than the block anyway, so they should get closed over fine. Also, I can check them in the debugger and they are fine. Also, when I draw from dispatch_sync, it works. So this does not appear to be the problem.</li> </ul></li> <li>Two tasks that access the same context may never execute simultaneously. <ul> <li>The only code accessing the GL context once it's set up is the code in this method, which is in turn, in this block. Since this a serial queue, only one instance of this should ever be drawing at a time anyway. Further, if I add a <code>synchronized(self.context){}</code> block, it doesn't fix anything. Also, in other code with very slow drawing I added a semaphore to skipping adding blocks to the queue when the previous one hadn't finished and it dropped frames fine (according to the <code>NSLog()</code> messages it was spitting out), but it didn't fix the drawing. HOWEVER, there is the possibility that some of the GLKit code that I can't see manipulates the context in ways I don't understand from the main thread. This is my second-highest rated theory right now, despite the fact that synchronized() doesn't change the problem and OpenGL Profiler doesn't show any thread conflicts.</li> </ul></li> <li>Each task should clear the thread’s context before exiting. <ul> <li>I'm not totally clear on what this means. The GCD thread's context? That's fine. We're not adding anything to the queue's context so there's nothing to clean up. The EAGLContext that we're drawing to? I don't know what else we could do. Certainly not actually glClear it, that will just erase everything. Also, there's some code in <a href="http://www.sunsetlakesoftware.com/molecules" rel="nofollow noreferrer">Sunset Lake's Molecules</a> that renders that looks like this:</li> </ul></li> </ul> <p>Code:</p> <pre><code>dispatch_async(openGLESContextQueue, ^{ [EAGLContext setCurrentContext:context]; GLfloat currentModelViewMatrix[9]; [self convert3DTransform:&amp;currentCalculatedMatrix to3x3Matrix:currentModelViewMatrix]; CATransform3D inverseMatrix = CATransform3DInvert(currentCalculatedMatrix); GLfloat inverseModelViewMatrix[9]; [self convert3DTransform:&amp;inverseMatrix to3x3Matrix:inverseModelViewMatrix]; GLfloat currentTranslation[3]; currentTranslation[0] = accumulatedModelTranslation[0]; currentTranslation[1] = accumulatedModelTranslation[1]; currentTranslation[2] = accumulatedModelTranslation[2]; GLfloat currentScaleFactor = currentModelScaleFactor; [self precalculateAOLookupTextureForInverseMatrix:inverseModelViewMatrix]; [self renderDepthTextureForModelViewMatrix:currentModelViewMatrix translation:currentTranslation scale:currentScaleFactor]; [self renderRaytracedSceneForModelViewMatrix:currentModelViewMatrix inverseMatrix:inverseModelViewMatrix translation:currentTranslation scale:currentScaleFactor]; const GLenum discards[] = {GL_DEPTH_ATTACHMENT}; glDiscardFramebufferEXT(GL_FRAMEBUFFER, 1, discards); [self presentRenderBuffer]; dispatch_semaphore_signal(frameRenderingSemaphore); }); </code></pre> <p>This code works, and I don't see any additional cleanup. I can't figure out what this code is doing differently than mine. One thing that's different is it looks like literally everything that touches the GL context is being done from the same GCD dispatch queue. However, when I make my code like this, it doesn't fix anything.</p> <p>The last thing that's different is that this code does not appear to use GLKit. The code above (along with the code I'm actually interested in) <em>does</em> use GLKit. </p> <p>At this point, I have three theories about this problem: 1. I am making a conceptual error about the interaction between blocks, GCD, and OpenGL ES. 2. GLKit's <code>GLKViewController</code> or <code>GLKView</code> do <em>some</em> drawing to or manipulation of the <code>EAGLContext</code> in between calls to <code>drawInRect</code>. While my <code>drawInRect</code> blocks are being worked on, this happens, messing things up. 3. The fact that I'm relying on the <code>- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect</code> method is ITSELF the problem. I think of this method as, "Hey, you automatically have a <code>CADisplayLink</code> configured, and every time it wants a frame, it'll hit this method. Do whatever the hell you want. I mean, in normal code here you just issue glDrawArrays commands. It's not like I'm passing back a framebuffer object or a CGImageRef containing what I want to end up on the screen. I'm issuing GL commands. HOWEVER, this could be wrong. Maybe you just can't defer drawing in this method in anyway without causing problems. To test this theory, I moved all the draw code into a method called <code>drawStuff</code> and then replaced the body of the <code>drawRect</code> method with:</p> <pre><code>[NSTimer scheduledTimerWithTimeInterval:10 target:self selector:@selector(drawStuff) userInfo:nil repeats:NO]; </code></pre> <p>The app comes up, displays the color the view is <code>glClear</code>'d to for ten seconds, and then draws like normal. So that theory doesn't look too strong either.</p> <p>There is <a href="https://stackoverflow.com/questions/10373526/how-to-use-dispatch-async-in-glkview">a similar question posted here</a> that has one answer, which is upvoted and accepted:</p> <blockquote> <p>The code in the dispatch block isn't going to work. By the time it gets executed, all of the OpenGL state for that frame will have long since been destroyed. If you were to put a call to glGetError() in that block, I'm sure it would tell you the same. You need to make sure that all your drawing code is done in that glkView method for the OpenGL state to be valid. When you do that dispatch, you're essentially shunting the execution of that drawing code out of the scope of that method.</p> </blockquote> <p>I don't see why this should be true. But: </p> <ol> <li>I'm only closing over references to things in the block that <em>are</em> going to outlive the block, and they're things like Objective-C pointers from the enclosing object scope. </li> <li>I can check them in the debugger, they look fine. </li> <li>I inserted a getGLError() call after every GL operation and it never returns anything but zero. </li> <li>Drawing from a block with dispatch_sync works.</li> <li>I tried a think where in the <code>drawInRect</code> method, I save the block to an ivar and then set a NSTimer to call <code>drawStuff</code>. In <code>drawStuff</code> I just invoke the block. Draws fine.</li> </ol> <p>The NSTimer case draws asynchronously, but it does not involve drawing from another thread since AFAIK NSTimer invocations just get scheduled on the setting thread's runloop. So it has to do with threads.</p> <p>Can anyone clue me in on what I'm missing here?</p>
    singulars
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
    1. This table or related slice is empty.
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload