EGL和GL线程
EGL是连接OpenGL ES与本地窗口系统的桥梁。
OpenGL是跨平台的,但是不同平台上的窗口系统是不一样的,它就需要一个东西帮助OpenGL与本地窗口系统进行对接、管理及执行GL命令等。
OpenGL ES的平台无关性正是借助EGL实现的,EGL屏蔽了不同平台的差异(Apple提供了自己的EGL API的iOS实现,自称EAGL)。
EGL可以创建渲染表面EGLSurface,同时提供了图形渲染上下文EGLContext,用来进行状态管理,接下来OpenGL ES就可以在这个渲染表面上绘制。
GL线程是一个与EGL环境绑定了的线程,绑定后可以在这个线程中执行GL操作。
比如将原视频进行OpenGL ES特效渲染然后编码保存,或者是解码原视频然后进行OpenGL ES特效渲染再显示出来。
编码时需要将要编码的帧渲染到MediaCodec给你的一块surface上,而这些操作需要有EGL才能做,而解码时是解码到一块你自己指定的surface上,此时你也没有一个现成的EGL环境,如果你想解码出来先用OpenGL ES做些特效处理再显示出来,那么这时也需要EGL环境。
GLContext
在我们的程序运行时,每一个方法的调用都是在CPU上的。 OpenGL也不例外,与普通调用的区别在于这些调用会被转换为GPU驱动指令在GPU上执行,而CPU和GPU作为两个不同的处理单元,它们之间的指令是并行执行的。
OpenGL的设计模式是Client/Server,这里Client是指CPU、Server是指GPU。
在执行gl命令时会有一个队列用于暂存还未发送到GPU的命令,实际上我们调用的绝大多数OpenGL方法,只是在往命令队列中插入命令,在CPU中并不一定会等待命令执行完。

但并不是所有的线程都对应同一个命令队列,命令队列是和EGL Context对应的,而一个线程又只能同时绑定到一个EGL Context。
因此可以理解为命令队列是和绑定的EGL Context的线程对应的。
CommandQueue:在CPU和GPU分别有各自的CommanQueue,当一个OpenGL命令在CPU执行时,会先添加到CPU的CommanQueue,在CPU切换到burst mode后,CPU CommanQueue中的命令被依次发送到GPU CommanQueue,GPU会在合适的时机执行CommanQueue里的命令。
GLContext: OpenGL的状态机,保存有渲染管线用到的状态和资源,OpenGL命令的宿主环境。
GLContext是和线程强相关的,这是因为绝大部分驱动的设计中,都是一个线程一个CommanQueue,而这个线程中的当前GLContext中发生的命令才会被添加到CommanQueue。
绝大多数OpenGL命令在CPU上是异步执行的,在CPU上是同步执行的。
这一切得益于CommanQueue的存在,让CPU可以更高效地完成命令发送,毕竟总线的资源是珍贵的,尤其在移动端的总线带宽是非常小的(内存64/128bit,显存128/256bit)。
但这样的代价是在渲染有问题时,在CPU无法定位到真正的现场。
为了让大家直接感觉一下EGL所起的作用,我们来试试几段代码,一段是我们很熟悉的GLSurfaceView的Renderer的代码,我们可以在回调中做GL操作,比如这里我们创建一个texture:
glSurfaceView.setRenderer(object : GLSurfaceView.Renderer {
override fun onDrawFrame(gl: GL10?) {
}
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
}
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
val textures = IntArray(1)
GLES30.glGenTextures(textures.size, textures, 0)
val imageTexture = textures[0]
}
})
如果你查看这个texture的值,会发现它大于0,也就是创建成功了,另外也可以通过OpenGL ES的方法GLES30.glIsTexture(imageTexture)来判断一个texture是不是合法的texture。
如果我们把上述创建texture的操作放到主线程中会怎样?
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val textures = IntArray(1)
GLES30.glGenTextures(textures.size, textures, 0)
val imageTexture = textures[0]
}
}
我们会发现,这时创建出来的texture是0,也就是创建失败了,其实不只是创建texture失败,其它GL操作一律会失败。
如果放到一个子线程中呢?
效果是一样的,也会失败,为什么?
原因就是在一个没有EGL环境的线程中调用了OpenGL ES API。
那如何让一个线程拥有EGL环境?创建EGL环境,步骤还不少,一共有以下几个步骤:
- 获取显示设备
eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY)
这里获取的是default的显示设备,大多数情况下我们都是获取default,因为大多数情况下设备只有一个屏幕,本文也只讨论这种情况。
- 初始化显示设备
val version = IntArray(2)
EGL14.eglInitialize(eglDisplay, version, 0, version, 1)
这里初始化完成后,会返回给我们支持的EGL的最大和最小版本号。
这里初始化完成后,会返回给我们支持的EGL的最大和最小版本号。
- 选择config
val attribList = intArrayOf(
EGL14.EGL_RED_SIZE,
8,
EGL14.EGL_GREEN_SIZE,
8,
EGL14.EGL_BLUE_SIZE,
8,
EGL14.EGL_ALPHA_SIZE,
8,
EGL14.EGL_RENDERABLE_TYPE,
EGL14.EGL_OPENGL_ES2_BIT or EGLExt.EGL_OPENGL_ES3_BIT_KHR,
EGL14.EGL_NONE
)
val eglConfig = arrayOfNulls<EGLConfig>(1)
val numConfigs = IntArray(1)
EGL14.eglChooseConfig(
eglDisplay,
attribList,
0,
eglConfig,
0,
eglConfig.size,
numConfigs,
0
)
这步操作告诉系统我们期望的EGL配置,然后系统返回给我们一个列表,按配置的匹配程度排序,因为系统不一定有我们期望的配置,因此要通过查询让系统返回尽可能接近的配置。
attribList是我们期望的配置,我们这里的配置是将RGBA颜色深度设置为8位,并将OpenGL ES版本设置为2和3,表示同时支持OpenGL 2和OpenGL 3,最后以一个EGL14.EGL_NONE作为结束符。
eglConfig是返回的尽可能接近我们期望的配置的列表,通常我们取第0个来使用,即最符合我们期望配置。
- 创建EGL Context
eglContext = EGL14.eglCreateContext(
eglDisplay,
eglConfig[0],
EGL14.EGL_NO_CONTEXT,
intArrayOf(EGL14.EGL_CONTEXT_CLIENT_VERSION, 3, EGL14.EGL_NONE),
0
)
eglDisplay即是之前创建的显示设备,注意第三个参数,它是指定一个共享的EGL Context,共享后,2个EGL Context可以相互使用对方创建的texture等资源,默认情况下是不共享的,但不是所有资源都能共享,例如program就是不共享的。
- 创建EGL Surface
val surfaceAttribs = intArrayOf(EGL14.EGL_NONE)
eglSurface = EGL14.eglCreatePbufferSurface(
eglDisplay,
eglConfig[0],
surfaceAttribs,
0
)
EGL Surface是什么东西?可以理解成是一个用于承载显示内容的东西,这里有2种EGL Surface可以选择,一种是window surface,一种是pbuffer surface。
如果我们创建这个EGL环境是为了跟一块Surface绑定,例如希望给Surface View创建一个EGL环境,使用OpenGL ES渲染到Surface View上,那么就要选择window surface,对应的创建方法为:
EGL14.eglCreateWindowSurface(
eglDisplay,
eglConfig[0],
surface,
surfaceAttribs,
0
)
其中surface就是这个Surface View对应的surface。如果我们不需要渲染出来看,那么就可以创建一个pbuffer surface,它不和surface绑定,不需要传surface给它,这也称为离屏渲染,本文中将创建pbuffer surface。
这里提一个细节,现在的surface所对应的buffer一般都是双buffer,以便于一个buffer正在显示的时候,有另一个buffer可用于渲染, 正在显示的buffer称为front buffer,正在渲染的buffer称为back buffer。
如果要渲染到surface上,必须在渲染后调EGL14.eglSwapBuffers(eglDisplay, eglSurface) 将显示buffer和渲染buffer进行交换才会生效,否则会一直渲染到back buffer上,这个buffer无法变成front buffer显示到surface上。
- 绑定EGL 前面的几个步骤,我们把一些需要创建的东西都创建好了,下面就要将EGL绑定到线程上让它具体有EGL环境:
EGL14.eglMakeCurrent(
eglDisplay,
eglSurface,
eglSurface,
eglContext
)
注意,一个线程只能绑定一个EGL环境,如果之前绑过其它的,后面又绑了一个,那就会是最后绑的那个。至此,就能让一个线程拥有EGL环境了,此后就可以顺利地做GL操作了。
好,我们看一下我们的例子代码:
Thread {
val egl = EGL()
egl.init()
egl.bind()
val textures = IntArray(1)
GLES30.glGenTextures(textures.size, textures, 0)
val imageTexture = textures[0]
assert(GLES30.glIsTexture(imageTexture))
egl.release()
}.start()
代码很简单,只是为了验证是否有了EGL环境,就不写复杂的操作了,EGL就是对前文所述的操作的一个封装类,init()方法对应了获取显示设备、初始化显示设备、选择config、创建EGL Context和创建EGL Surface,bind()方法对应了eglMakeCurrent()。
共享上下文

共享上下文时,可以跨线程共享哪些资源?
可共享的资源:
- 纹理
- shader
- program着色器程序
- buffer类对象,如VBO、EBO、RBO等
不可以共享的资源:
- FBO帧缓冲区对象(不属于buffer类)
- VAO顶点数组对象(不属于buffer类)
在不可以共享的资源中,FBO和VAO属于资源管理型对象,FBO负责管理几种缓冲区,本身不占用资源,VAO负责管理VBO或EBO,本身也不占用资源。
注意:每个线程同时只能绑定(eglMakeCurrent)一个Context,但可以按顺序切换不同Context。
glFinish()、glFlush()、glFence()
但是在跨线程的共享中也会带来一些同步问题,比如两个线程对同一个纹理进行操作,thread0在纹理上绘制,thread1使用thread0提供的纹理,这时候如果thread0的线程没有渲染完,而thread1就开始使用,就会造成黑屏或者花屏的问题。
要解决这个问题有一个简单的方案就是使用glFinish(),glFinish()是一个同步方法,运行时会等待该线程中所有的GL命令(包括所有改变GL状态的命令,所有改变连接状态的命令和所有改变Frame buffer内容的命令)都执行完,从而保证绘制的完整性。
与glFinish()比较相近的还有一个glFlush()方法,它的作用是将命令队列中的命令全部刷到GPU,但并不等它执行完成。
在实际使用中,各个厂家的GPU实现差异很大,但都要求符合OpenGL标准,glFlush可以异步实现,也可以同步实现,但glFinish必须同步实现,所以调用glFlush并返回时,只能说是渲染命令都下发了,但不能保证一定完成,而glFinish返回时是保证了命令已经执行完成的。
glFinish虽然能保证命令的完整执行,但是会对性能有一定的影响,因为它会使得GPU和CPU之间的并行性丧失。
一般,我们提交给驱动的任务被分组,然后被送到硬件上(再缓冲区交换的时候),如果调用glFinish,就强制驱动将命令送到GPU。然后CPU等待直到被传送的命令全部执行完成。这样在GPU工作的整个期间内,CPU没有工作(至少在这个线程上)。而在CPU工作时(通常是在对命令分组),GPU没有工作,因此造成性能上的下降。
有些厂商在实现中已经做了一些保证操作完整性的处理(比如高通部分Soc),并不需要使用到glFinish,过度的使用glFinish反而降低了渲染的效率,相比于glFinish在OpenGL 3.0中有了更好的方案glFence。
GLFence 的原理是在同步过程中创建一个 Fence 同步对象并将 Fence 命令插入 GL 命令队列中,当 Fence 命令满足同步对象的指定条件时,GL 发出解除信号,解除由glWaitSync 或 glClientWaitSync命令造成的阻塞。
同时,glFenceSync 或相关 Fence 命令的执行不会影响其他 GL 状态。
glFence 中包含两个 Sync 命令 glWaitSync 和 glClientWaitSync,glWaitSync 是在 GPU 上等待,而 glClientWaitSync 是在 CPU 上等待,实际使用中只有 glClientWaitSync 起到了阻止闪屏的作用。
如果说OpenGL ES是画笔,那Android的Surface就是画布。我们是可以使用OpenGL ES将图像画到 Surface中去的。但是OpenGL并不能直接在Android的Surface中画画,需要借助 EGLSurface 来将 Android的Surface关联起来。