简体   繁体   English

如何从iOS上的Grand Central Dispatch Queue异步绘制到GLKit的OpenGL ES上下文

[英]How to draw to GLKit's OpenGL ES context asynchronously from a Grand Central Dispatch Queue on iOS

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. 我正在尝试将冗长的OpenGL绘制操作移动到GCD队列中,这样我就可以在GPU磨合的同时完成其他工作。 I would much rather prefer to do this with GCD vs. adding real threading to my app. 宁愿喜欢用GCD与增加实际线程我的应用程序来做到这一点。 Literally all I want to do is be able to 字面上我想做的就是能够

  • Not block on a glDrawArrays() call so the rest of the UI can stay responsive when GL rendering gets very slow. 不会阻止glDrawArrays()调用,因此当GL渲染变得非常慢时,UI的其余部分可以保持响应。
  • Drop glDrawArrays() calls when we aren't finishing them anyway (don't build up a queue of frames that just grows and grows) 当我们还没有完成它们时放下glDrawArrays()调用(不要建立一个只增长和增长的帧队列)

On Apple's website, the docs say: 在Apple的网站上,文档说:

GCD and NSOperationQueue objects can execute your tasks on a thread of their choosing. GCD和NSOperationQueue对象可以在他们选择的线程上执行您的任务。 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: 对于OpenGL ES应用程序,这意味着:

  • Each task must set the context before executing any OpenGL ES commands. 每个任务必须在执行任何OpenGL ES命令之前设置上下文。
  • Two tasks that access the same context may never execute simultaneously. 访问相同上下文的两个任务可能永远不会同时执行。
  • Each task should clear the thread's context before exiting. 每个任务都应该在退出之前清除线程的上下文。

Sounds pretty straightforward. 听起来很简单。

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. 为了简化这个问题,我首先介绍了一个新的骨架版Apple模板,它出现在“OpenGL ES”游戏的“New Project”对话框中。 When you instantiate it, compile, and run, you should see two cubes rotating on a grey field. 当您实例化它,编译并运行时,您应该看到在灰色字段上旋转的两个立方体。

To that code, I have added a GCD queue. 对于该代码,我添加了一个GCD队列。 Starting with the interface section of ViewController.m : ViewController.m的接口部分开始:

dispatch_queue_t openGLESDrawQueue;

Then setting those up in ViewController viewDidLoad : 然后在ViewController viewDidLoad设置它们:

openGLESDrawQueue = dispatch_queue_create("GLDRAWINGQUEUE", NULL);

Finally, I make these very small changes to the drawInRect method that CADisplayLink ends up triggering: 最后,我做这些非常小的改动drawInRect是CADisplayLink最终触发方式:

- (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);
}

This does not work. 这不起作用。 The drawing goes crazy. 绘图变得疯狂。 Drawing with the same block with dispatch_sync() works fine, though. 但是使用dispatch_sync()绘制相同的块可以正常工作。

Let's double check Apple's list: 让我们仔细检查Apple的清单:

  • Each task must set the context before executing any OpenGL ES commands. 每个任务必须在执行任何OpenGL ES命令之前设置上下文。
    • 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. 这是Objective-C对象指针,它的生命周期比块长,所以它们应该关闭得很好。 Also, I can check them in the debugger and they are fine. 另外,我可以在调试器中检查它们,它们很好。 Also, when I draw from dispatch_sync, it works. 此外,当我从dispatch_sync绘制时,它的工作原理。 So this does not appear to be the problem. 所以这似乎不是问题所在。
  • Two tasks that access the same context may never execute simultaneously. 访问相同上下文的两个任务可能永远不会同时执行。
    • 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. 设置完成后访问GL上下文的唯一代码是此方法中的代码,该代码依次在此块中。 Since this a serial queue, only one instance of this should ever be drawing at a time anyway. 由于这是一个串行队列,因此无论如何一次只能绘制一个这样的实例。 Further, if I add a synchronized(self.context){} block, it doesn't fix anything. 此外,如果我添加一个synchronized(self.context){}块,它不会修复任何东西。 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 NSLog() messages it was spitting out), but it didn't fix the drawing. 另外,在绘图速度很慢的其他代码中,我添加了一个信号量来跳过在前一个没有完成时向队列添加块并且它丢帧很好(根据它正在吐出的NSLog()消息)但是它没有不修理图纸。 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. 但是,有一些我看不到的GLKit代码可能会以主线程中无法理解的方式操纵上下文。 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. 这是我现在评价第二高的理论,尽管synchronized()不会改变问题并且OpenGL Profiler不会显示任何线程冲突。
  • Each task should clear the thread's context before exiting. 每个任务都应该在退出之前清除线程的上下文。
    • I'm not totally clear on what this means. 我不清楚这意味着什么。 The GCD thread's context? GCD线程的上下文? 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? 我们正在吸引的EAGLContext? I don't know what else we could do. 我不知道我们还能做些什么。 Certainly not actually glClear it, that will just erase everything. 当然不会实际上是glClear它,只会抹去一切。 Also, there's some code in Sunset Lake's Molecules that renders that looks like this: 此外, Sunset Lake的Molecules中有一些代码可以呈现如下:

Code: 码:

dispatch_async(openGLESContextQueue, ^{
    [EAGLContext setCurrentContext:context];        
    GLfloat currentModelViewMatrix[9];
    [self convert3DTransform:&currentCalculatedMatrix to3x3Matrix:currentModelViewMatrix];
    CATransform3D inverseMatrix = CATransform3DInvert(currentCalculatedMatrix);
    GLfloat inverseModelViewMatrix[9];
    [self convert3DTransform:&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);
});

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. 有一点不同的是,从字面上看,触及GL上下文的所有内容都是从同一个GCD调度队列中完成的。 However, when I make my code like this, it doesn't fix anything. 但是,当我像这样编写代码时,它并没有修复任何问题。

The last thing that's different is that this code does not appear to use GLKit. 最后一点不同的是这段代码似乎没有使用GLKit。 The code above (along with the code I'm actually interested in) does use GLKit. 上面的代码(以及我真正感兴趣的代码) 确实使用了GLKit。

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. 在这一点上,我有三个关于这个问题的理论:1。我对块,GCD和OpenGL ES之间的交互做了一个概念上的错误。 2. GLKit's GLKViewController or GLKView do some drawing to or manipulation of the EAGLContext in between calls to drawInRect . 2. GLKit的GLKViewControllerGLKView一些绘图或操纵EAGLContext在调用之间drawInRect While my drawInRect blocks are being worked on, this happens, messing things up. 当我的drawInRect块正在处理时,会发生这种情况,弄乱了。 3. The fact that I'm relying on the - (void)glkView:(GLKView *)view drawInRect:(CGRect)rect method is ITSELF the problem. 3.我依赖的事实- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect方法是- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect的问题。 I think of this method as, "Hey, you automatically have a CADisplayLink 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 drawStuff and then replaced the body of the drawRect method with: 我认为这个方法是,“嘿,你自动配置了一个CADisplayLink ,每次想要一个框架,它都会触及这个方法。不管你想要什么。我的意思是,在普通代码中你只需要发出glDrawArrays这不是说我要传回一个帧缓冲对象或一个CGImageRef,其中包含我想要在屏幕上显示的内容。我正在发出GL命令。但是,这可能是错误的。也许你不能推迟绘图这个方法无论如何都没有引起问题。为了测试这个理论,我将所有绘制代码​​移动到一个名为drawStuff的方法中,然后将drawRect方法的主体替换为:

[NSTimer scheduledTimerWithTimeInterval:10 target:self selector:@selector(drawStuff) userInfo:nil repeats:NO];

The app comes up, displays the color the view is glClear 'd to for ten seconds, and then draws like normal. 应用程序出现,显示视图为glClear的颜色十秒钟,然后绘制正常。 So that theory doesn't look too strong either. 所以这个理论看起来也不太强大。

There is a similar question posted here that has one answer, which is upvoted and accepted: 这里发布了一个类似的问题 ,其中有一个答案,被赞成并被接受:

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. 当它被执行时,该帧的所有OpenGL状态将很久以来被破坏。 If you were to put a call to glGetError() in that block, I'm sure it would tell you the same. 如果您在该块中调用glGetError(),我相信它会告诉您相同的内容。 You need to make sure that all your drawing code is done in that glkView method for the OpenGL state to be valid. 您需要确保所有绘图代码都在该glkView方法中完成,以使OpenGL状态有效。 When you do that dispatch, you're essentially shunting the execution of that drawing code out of the scope of that method. 当您执行该调度时,您实际上是将该绘图代码的执行分离出该方法的范围。

I don't see why this should be true. 我不明白为什么这应该是真的。 But: 但:

  1. I'm only closing over references to things in the block that are going to outlive the block, and they're things like Objective-C pointers from the enclosing object scope. 我只是在关闭的块要活得比块的东西引用,他们喜欢的东西从封闭对象范围Objective-C的指针。
  2. I can check them in the debugger, they look fine. 我可以在调试器中检查它们,它们看起来很好。
  3. I inserted a getGLError() call after every GL operation and it never returns anything but zero. 我在每次GL操作后都插入了一个getGLError()调用,它永远不会返回任何零。
  4. Drawing from a block with dispatch_sync works. 使用dispatch_sync从块中绘图有效。
  5. I tried a think where in the drawInRect method, I save the block to an ivar and then set a NSTimer to call drawStuff . 我试着想一下在drawInRect方法中,我将块保存到ivar然后设置NSTimer来调用drawStuff In drawStuff I just invoke the block. drawStuff我只是调用块。 Draws fine. 画得很好。

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. NSTimer情况是异步绘制的,但它不涉及从另一个线程绘制,因为AFAIK NSTimer调用只是在设置线程的runloop上进行了调度。 So it has to do with threads. 所以它与线程有关。

Can anyone clue me in on what I'm missing here? 谁能让我知道我在这里缺少什么?

This is not working because as borrrden says, GLKit is calling presentRenderbuffer: immediately after - (void)glkView:(GLKView *)view drawInRect:(CGRect)rect completes. 这不起作用,因为正如borrrden所说,GLKit正在调用presentRenderbuffer:紧接着- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect完成。

This is working in your case of using a timer, because the drawStuff method is being called on the main thread at the beginning of a draw cycle. 这适用于使用计时器的情况,因为drawStuff方法是在绘制周期开始时在主线程上调用的。 Your - (void)glkView:(GLKView *)view drawInRect:(CGRect)rect effectively does nothing but schedule this to happen on the main thread again in another 10 seconds, and the previously scheduled drawing call is then rendered at the end of the drawInRect: method. 你的- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect实际上没有做任何事情,只是在另一个10秒内再次在主线程上安排这个,然后在结束时渲染先前安排的绘图调用drawInRect:方法。 This is doing nothing except delaying the drawing by 10 seconds, everything is still occurring on the main thread. 除了将绘图延迟10秒之外,这没有任何作用,主线程上仍然发生了一切。

If you want to go the route of rendering off the main thread, GLKit is not going to be a good match. 如果你想要渲染主线程的路线,GLKit将不会是一个很好的匹配。 You are going to need to set up your own thread with a runloop, hook up a CADisplayLink to this runloop, and then render from this queue. 你将需要建立自己的线程有runloop,挂钩一CADisplayLink这个runloop,然后从这个队列中呈现。 GLKViewController is configured to use the main runloop for this, and will always present the render buffer at the end of each frame, which will cause havoc with whatever you are doing on a different thread. GLKViewController被配置为使用主runloop,并且将始终在每个帧的末尾呈现渲染缓冲区,这将对您在不同线程上执行的操作造成严重破坏。

Depending on your GL needs you may find it simpler doing all the GL stuff on the main thread, and doing the "other stuff" off the main thread. 根据您的GL需求,您可能会发现在主线程上执行所有GL内容并在主线程上执行“其他内容”更简单。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM