大家好,今天给大家介绍一下OpenGL ES的命令队列及glFinish/glFlush

我们知道,我们调用的OpenGL ES方法,都是在CPU上调用的,这些调用最终会被转换成GPU驱动指令而在GPU上执行,而CPUGPU因为是两个不同的处理器,它们之间自然是可以并行地执行各自的指令,OpenGL ES有一个命令队列用于暂存还未发送到GPU的命令,实际上我们调用的绝大多数OpenGL ES方法,只是往命令队列时插入命令而已,并不会在CPU等命令执行完,因此如果大家去测耗时,会发现OpenGL ES大多数方法,基本都不耗时,无论渲染的东西多么复杂。

我画了一个图来表示:



这里注意一个细节,这个命令队列并不是所有的线程都对应同一个,命令队列是和EGL Context对应的,而一个线程又只能同时绑定到一个EGL Context(关于EGL、GL线程、线程共享EGL Context,可以参见我的另一篇文章《OpenGL ES 高级进阶:EGL及GL线程》),因此,可以理解为命令队列是和绑定的EGL Context的线程对应的。

有时我们又希望在CPU上等待OpenGL ES命令执行完成,例如我们有时希望做多线程优化,在两个共享EGL Context的线程中,在一个线程中渲染,在另一个线程中用渲染好的纹理做其它操作等,那么在这种情况下,我们是不能像在CPU上做同步那样的,来看一段伪代码:

// thread0:
fun run() {
    ...
    // 调用glDrawXXX()渲染到texture上
    lock.notify()
    ...
}
// thread1:
fun run() {
    ...
    lock.wait()
    // 将texture拿去用
    ...
}
复制代码

代码中,我们希望在thread0完成渲染后,在thread1中将它读到bitmap中,这样会读到什么结果?基于前面的讨论,可以知道这样读到的结果是不确定的,因为thread0执行glDrawXXX()之后,并不会等待GPU真正执行了渲染,所以thread1在使用texture时,它的内容是不确定的,有可能还没开始渲染,也有可能渲染到了一半,或者是已经渲染完了。要得到正确的结果,在OpenGL ES 2.0中我们可以使用glFinsh(),在OpenGL ES 3.0中可以使用fence,后面我会写文章介绍fence,现在我们使用glFinish(),它的作用是在CPU上等待当前线程对应的命令队列里的命令执行完成,加上glFinish()后,我们就一定能得到正确的结果:

// thread0:
fun run() {
    ...
    // 调用glDrawXXX()渲染到texture上
    glFinish()
    lock.notify()
    ...
}
// thread1:
fun run() {
    ...
    lock.wait()
    // 将texture拿去用
    ...
}
复制代码

我画了个图来直观的展示:



由于glFinish()要在CPU上等待,因此会对性能造成一定的影响,如果thread0是一个主渲染线程,那就会对帧率产生影响,因此把等待放到比较次要的thread1中会比较好,但是我们把glFinish()放到thread1可以吗?来看下面这张图:



前面提到过,命令队列是每个绑定了EGL Context的线程各自有各自的,glFinish()只会等待当前线程的命令队列中的命令执行完成,也就是等待thread1的命令队列中的命令执行完成,因此是没有我们期望的效果的,在OpenGL ES 2.0中,是没有办法做到在一个线程中等待另一个线程的OpenGL命令的,在OpenGL ES 3.0中可以用fence实现。

前面说到绝大多数OpenGL ES方法是不会等待的,那么什么方法会等待呢?刚才的glFinish()就是一个,此外,还有将texture读取出来的方法glReadPixels()也会等待,另外还有eglSwapBuffers(),实际这2个方法会隐式调用glFinish()所以有时候我们常常发现,glReadPixels()会耗时,于是有些人会认为,把texture读出来的操作很耗时,实际上这个读操作并没有多耗时,耗时是在等待命令队列中的所有命令执行完成。

大家可以试一下,在glReadPixels()前如果先调glFinish()把命令队列清空,再执行glReadPixels(),会发现glReadPixels()没有想像中的那么耗时。

glReadPixels()为什么会隐式调用glFinish()?大家可以这样理解,因为glReadPixels()是要将texture读出来,如果不保证之前的渲染命令执行完,那么读出来的结果就是不确定的,而eglSwapBuffers()为什么也会隐式调用glFinish()?可以类似地这样理解,因为eglSwapBuffers()是将双buffer进行交换从而让正在接受渲染的back buffer能显示出来,如果不保证所有渲染命令执行完,是不是有可能显示出来是残缺不全的?

说到这,顺便提一下OpenGL ES的耗时测量,由于OpenGL ES大多数方法只是往命令队列里插入命令而不等待执行完成,因此要测量一段OpenGL ES操作的代码真正的耗时,需要在前后加上glFinish()

glFinish()
val startTime = System.currentTimeMillis()
// 一顿OpenGL ES操作
glFinish()
val duration = System.currentTimeMillis() - startTime
复制代码

在前面也加glFinish()是为了将之前的命令先执行完,不要干扰我们的测量。

glFinish()类似的还有一个方法是glFlush(),它的作用是将命令队列中的命令全部刷到GPU,但并不等它执行完成,因此有一些操作希望它能快些执行,但又不是特别急切到马上等它执行完成,这时候就可以用glFlush()

感谢阅读!