4

我正在尝试将冗长的 OpenGL 绘制操作移到 GCD 队列中,这样我就可以在 GPU 运行时完成其他工作。我宁愿用 GCD 来做这件事,而不是在我的应用程序中添加真正的线程。从字面上看,我想做的就是能够

  • 不阻塞 glDrawArrays() 调用,因此当 GL 渲染变得非常缓慢时,UI 的其余部分可以保持响应。
  • 当我们无论如何都没有完成它们时丢弃 glDrawArrays() 调用(不要建立一个不断增长的帧队列)

在 Apple 的网站上,文档说:

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

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

听起来很简单。

为了简单起见,我从“OpenGL ES”游戏的“新项目”对话框中出现的 Apple 模板的新骨干版本开始。当您实例化、编译和运行它时,您应该会看到两个立方体在灰色区域上旋转。

在该代码中,我添加了一个 GCD 队列。从接口部分开始ViewController.m

dispatch_queue_t openGLESDrawQueue;

然后将它们设置为ViewController viewDidLoad

openGLESDrawQueue = dispatch_queue_create("GLDRAWINGQUEUE", NULL);

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

这不起作用。画疯了。不过,使用相同的块进行绘图可以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);
});

这段代码有效,我没有看到任何额外的清理。我无法弄清楚这段代码的作用与我的不同。不同的一件事是它看起来就像实际上所有触及 GL 上下文的东西都是从同一个 GCD 调度队列中完成的。但是,当我这样编写代码时,它并不能解决任何问题。

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

在这一点上,我对这个问题有三个理论: 1. 关于块、GCD 和 OpenGL ES 之间的交互,我犯了一个概念上的错误。2. GLKitGLKViewControllerGLKView一些绘图或操作EAGLContext之间的调用drawInRect。当我的drawInRect积木正在处理时,这种情况发生了,把事情搞砸了。3. 我依赖- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect方法的事实本身就是问题所在。我认为这种方法是,“嘿,你自动拥有一个CADisplayLink配置好了,每次它想要一帧,它就会打这个方法。做任何你想做的事。我的意思是,在这里的普通代码中,您只需发出 glDrawArrays 命令。这不像我要传回一个帧缓冲区对象或一个包含我想要在屏幕上结束的内容的 CGImageRef。我正在发出 GL 命令。但是,这可能是错误的。也许您无论如何都不能在不引起问题的情况下推迟使用这种方法进行绘图。为了验证这个理论,我将所有绘制代码​​移到了一个名为的方法drawStuff中,然后将drawRect方法的主体替换为:

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

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

此处发布了一个类似的问题,该问题有一个答案,已被赞成并接受:

调度块中的代码不起作用。当它被执行时,该帧的所有 OpenGL 状态早就被销毁了。如果您要在该块中调用 glGetError(),我相信它会告诉您同样的情况。您需要确保所有绘图代码都在该 glkView 方法中完成,以使 OpenGL 状态有效。当您执行该调度时,您实际上是将该绘图代码的执行分流到该方法的范围之外。

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

  1. 我只是关闭了对块中将比块寿命更长的东西的引用它们就像来自封闭对象范围的 Objective-C 指针。
  2. 我可以在调试器中检查它们,它们看起来很好。
  3. 我在每次 GL 操作后插入了一个 getGLError() 调用,它只返回零。
  4. 从带有 dispatch_sync 的块中绘图可以工作。
  5. 我尝试了在drawInRect方法中的哪个位置,将块保存到 ivar,然后设置 NSTimer 来调用drawStuff. 在drawStuff我只是调用块。画得很好。

NSTimer 案例异步绘制,但它不涉及从另一个线程绘制,因为 AFAIK NSTimer 调用只是在设置线程的运行循环上安排。所以它与线程有关。

谁能告诉我我在这里缺少什么?

4

1 回答 1

5

这不起作用,因为正如 borrrden 所说,GLKit在完成presentRenderbuffer:后立即调用- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect

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

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

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

于 2013-11-09T10:05:42.660 回答