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

Jeb*_*Jeb 4 opengl-es grand-central-dispatch ios objective-c-blocks

我正在尝试将冗长的OpenGL绘制操作移动到GCD队列中,这样我就可以在GPU磨合的同时完成其他工作.我会更愿意倾向于与GCD与增加实际线程我的应用程序来做到这一点.字面上我想做的就是能够

  • 不会阻止glDrawArrays()调用,因此当GL渲染变得非常慢时,UI的其余部分可以保持响应.
  • 当我们还没有完成它们时放下glDrawArrays()调用(不要建立一个只增长和增长的帧队列)

在Apple的网站上,文档说:

GCD和NSOperationQueue对象可以在他们选择的线程上执行您的任务.他们可以专门为该任务创建一个线程,或者他们可以重用现有的线程.但在任何一种情况下,您都无法保证哪个线程执行任务.对于OpenGL ES应用程序,这意味着:

  • 每个任务必须在执行任何OpenGL ES命令之前设置上下文.
  • 访问相同上下文的两个任务可能永远不会同时执行.
  • 每个任务都应该在退出之前清除线程的上下文.

听起来很简单.

为了简化这个问题,我首先介绍了一个新的骨架版Apple模板,它出现在"OpenGL ES"游戏的"New Project"对话框中.当您实例化它,编译并运行时,您应该看到在灰色字段上旋转的两个立方体.

对于该代码,我添加了一个GCD队列.从接口部分开始ViewController.m:

dispatch_queue_t openGLESDrawQueue;
Run Code Online (Sandbox Code Playgroud)

然后将它们设置为ViewController viewDidLoad:

openGLESDrawQueue = dispatch_queue_create("GLDRAWINGQUEUE", NULL);
Run Code Online (Sandbox Code Playgroud)

最后,我对drawInRectCADisplayLink最终触发的方法进行了这些非常小的更改:

- (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);
}
Run Code Online (Sandbox Code Playgroud)

这不起作用.绘图变得疯狂.但是,使用相同的块绘制dispatch_sync()工作正常.

让我们仔细检查Apple的清单:

  • 每个任务必须在执行任何OpenGL ES命令之前设置上下文.
    • 好.我正在设置上下文.这是Objective-C对象指针,它的生命周期比块长,所以它们应该关闭得很好.另外,我可以在调试器中检查它们,它们很好.此外,当我从dispatch_sync绘制时,它的工作原理.所以这似乎不是问题所在.
  • 访问相同上下文的两个任务可能永远不会同时执行.
    • 设置完成后访问GL上下文的唯一代码是此方法中的代码,该代码依次在此块中.由于这是一个串行队列,因此无论如何一次只能绘制一个这样的实例.此外,如果我添加一个synchronized(self.context){}块,它不会修复任何东西.此外,在绘图速度非常慢的其他代码中,我添加了一个信号量,以便在前一个没有完成时跳过向队列添加块并且它丢帧很好(根据NSLog()它正在吐出的消息),但它没有修复图纸.但是,有一些我看不到的GLKit代码可能会以主线程中无法理解的方式操纵上下文.这是我现在评价第二高的理论,尽管synchronized()不会改变问题并且OpenGL Profiler不会显示任何线程冲突.
  • 每个任务都应该在退出之前清除线程的上下文.
    • 我不清楚这意味着什么.GCD线程的上下文?没关系.我们没有在队列的上下文中添加任何东西,所以没有什么可以清理的.我们正在吸引的EAGLContext?我不知道我们还能做些什么.当然实际上并不是glClear它,只会抹去一切.此外,Sunset Lake的Molecules中有一些代码可以呈现如下:

码:

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);
});
Run Code Online (Sandbox Code Playgroud)

此代码有效,我没有看到任何额外的清理.我无法弄清楚这段代码的作用与我的不同.有一点不同的是,从字面上看,触及GL上下文的所有内容都是从同一个GCD调度队列中完成的.但是,当我像这样编写代码时,它并没有修复任何问题.

最后一点不同的是这段代码似乎没有使用GLKit.上面的代码(以及我真正感兴趣的代码)确实使用了GLKit.

在这一点上,我有三个关于这个问题的理论:1.我对块,GCD和OpenGL ES之间的交互做了一个概念上的错误.2. GLKit GLKViewController或者在调用之间GLKView一些绘制或操纵.当我的积木正在进行时,这种情况发生了,搞砸了.我依赖这种方法的事实是ITSELF的问题.我认为这种方法是,"嘿,你自动拥有一个EAGLContextdrawInRectdrawInRect- (void)glkView:(GLKView *)view drawInRect:(CGRect)rectCADisplayLink配置,每次它想要一个帧,它会点击这个方法.做你想要的任何事情.我的意思是,在普通代码中你只需要发出glDrawArrays命令.这不像是我传回一个帧缓冲对象或一个CGImageRef,其中包含我想要在屏幕上显示的内容.我正在发布GL命令.但是,这可能是错的.也许你无论如何都不能推迟这种方法的绘制而不会引起问题.为了测试这个理论,我将所有绘制代码​​移动到一个调用的方法中drawStuff,然后用以下drawRect方法替换方法体:

[NSTimer scheduledTimerWithTimeInterval:10 target:self selector:@selector(drawStuff) userInfo:nil repeats:NO];
Run Code Online (Sandbox Code Playgroud)

应用程序出现,显示视图的颜色glClear十分钟,然后像平常一样绘制.所以这个理论看起来也不太强大.

这里发布了一个类似的问题,其中有一个答案,被赞成并被接受:

调度块中的代码不起作用.当它被执行时,该帧的所有OpenGL状态将很久以来被破坏.如果您在该块中调用glGetError(),我相信它会告诉您相同的内容.您需要确保所有绘图代码都在该glkView方法中完成,以使OpenGL状态有效.当您执行该调度时,您实际上是将该绘图代码的执行分离出该方法的范围.

我不明白为什么这应该是真的.但:

  1. 我只是在收盘在块的东西引用要活得比块,和他们喜欢的东西从封闭对象范围Objective-C的指针.
  2. 我可以在调试器中检查它们,它们看起来很好.
  3. 我在每次GL操作后都插入了一个getGLError()调用,它永远不会返回任何零.
  4. 使用dispatch_sync从块中绘图有效.
  5. 我试着想一下在drawInRect方法中,我将块保存到ivar然后设置NSTimer来调用drawStuff.在drawStuff我只是调用块.画得很好.

NSTimer情况是异步绘制的,但它不涉及从另一个线程绘制,因为AFAIK NSTimer调用只是在设置线程的runloop上进行了调度.所以它与线程有关.

谁能让我知道我在这里缺少什么?

Tar*_*ark 5

这是行不通的,因为正如borrrden所说,GLKit presentRenderbuffer:- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect完成后立即打电话.

这适用于使用计时器的情况,因为该drawStuff方法是在绘制周期开始时在主线程上调用的.你- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect实际上什么都不做,只是在另一个10秒内再次在主线程上安排这个,然后在drawInRect:方法结束时呈现先前安排的绘图调用.除了将绘图延迟10秒之外,这没有任何作用,主线程上仍然发生了一切.

如果你想要渲染主线程的路线,GLKit将不会是一个很好的匹配.您将需要使用runloop设置自己的线程,连接CADisplayLink到此runloop,然后从此队列进行渲染.GLKViewController被配置为使用主runloop,并且将始终在每个帧的末尾呈现渲染缓冲区,这将对您在不同线程上执行的操作造成严重破坏.

根据您的GL需求,您可能会发现在主线程上执行所有GL内容并在主线程上执行"其他内容"更简单.