出处:电子设备中的画家|王烁 于 2017 年 7 月 9 日发表,原文链接(http://geekfaner.com/shineengine/blog3_OpenGLESv2_2.html)
上节回顾
上一节我们初步学习了 OpenGL ES、EGL、GLSL 的相关概念,了解了它们的功能,以及它们之间的关联。我们知道了 EGL 是绘制 API(比如 OpenGL ES)与 底层平台窗口系统之间的接口,用于与手机设备打交道,比如获取绘制 buffer。 而 OpenGL ES 与 GLSL 的主要功能,就是往这块 buffer 上绘制图片。由于绘制的第一步就是获取绘制 buffer,而这完全通过 EGL 来实现的,那么这一节,我们来仔细研究一下,EGL 是如何跟手机产生关联,并如何从手机那里获取一块 buffer 用于绘制。
EGL API 总览
上一节,我们提到 OpenGL ES 其实是一个图形学库,由 109 个 API 组成,只要明白了这 109 个 API 的意义和用途,就掌握了 OpenGL ES。
这个道理在 EGL 上也同样适用。EGL 包含了 34 个 API。
首先,有 7 个 API,用于与手机关联并获取手机支持的配置信息。我们知道现在手机种类是各种各样,从操作系统来说,有 iOS、Android 等。同是 Android 手机,手机品牌和型号也是各种各样。当然我们不排除有两款手机,只是外形不同,而内部却完全一样,但是总的来说,大部分手机与手机的内部,还是存在着一定的区别的。所以,当我们使用 EGL 与某款手机硬件进行关联的时候,首先要做的,就是查看一下这款手机支持什么样的配置信息。而所谓的配置信息,就是手机支持多少种格式的绘制 buffer,每种格式对应着的 RGBA 是如何划分的,以及是否支持 depth、stencil 等。关于 RGBA 的格式,等我们在讲纹理图片的时候 再详细进行说明。而关于 depth、stencil,我们也将在讲 OpenGL ES 相应 API 的时 候进行解释说明。
然后,有 16 个 API,用于根据需要生成手机支持的 surface 和 context,对 surface 和 context 进行关联,并将 surface 对应的绘制 buffer 显示到手机屏幕上。 当我们知道了手机支持什么样格式的绘制 buffer 之后,就要根据我们所写的图形程序的需要,去对这些格式进行筛选,找到一个能满足我们需求,且手机支持的格式,然后通过 EGL 生成一块该格式的绘制 buffer。生成绘制 buffer 的过程,其实是我们通过 API 生成一块 surface,surface 是一个抽象概念,但是这个 surface 包含了绘制 buffer,假如我们所选择的格式是支持 RGBA、depth、stencil 的。那 么 surface 对应的绘制 buffer 有一块 Color buffer,到时候会用于保存图片的颜色信息,保存的方式在上一节我们做过介绍,就是 buffer 会对应上百万个像素点, 每个像素点有自己的颜色值,将这些颜色值按照像素点的顺序保存在 color buffer 中,就形成了一张完整的 color buffer。绘制 buffer 还有一块 depth buffer,depth buffer 也按照同样的方法,按照顺序保存了所有像素点的 depth 值;以及一块 stencil buffer,同理可知,stencil buffer 也是按照顺序保存了所有像素点的 stencil 值。
EGL 还会根据格式生成一块 context,context 也是一块 buffer。我们知道 OpenGL ES 是状态集,那么在绘制中会牵扯到各种各样的状态,这些状态全部都有默认值,可以通过 OpenGL ES 对这些状态进行改变。这些状态值就会保存在 context 中。比如 OpenGL ES 所用到的混合模式、纹理图片、program 还有各种 BO 等信息。
当然 EGL 可以创建多个 surface 和 context,每个 surface 在创建的时候就是 包含了对应的绘制 buffer,每个 context 创建的时候内部都是默认值。然后我们可以根据自己的需要选择启动任意一套 surface 和 context,然后对选中的 surface 和 context 进行操作。一个进程同一时间只能启动有相同格式的一块 surface 和一块对应于 OpenGL ES 的 context,一块 context 同时也只能被一个进程启动。
之后,有 3 个 API,用于指定使用哪个版本的 OpenGL ES,并与 OpenGL ES 建立关联。由于 EGL 生成的绘制 buffer 终归还是要提供给 OpenGL ES 使用,所以, 需要通过 EGL 来指定使用哪个版本的 OpenGL ES。
OpenGL ES 发展到现在已经有了好几个版本,从最早的 1.1 版本(在 1.1 中 使用的还是固定管线,还不存在 shader 的概念),到现在最普遍的 2.0 版本(2.0 版本中将 OpenGL ES 发展为上一节我们详细介绍的管线,加入了可编程模块 shader),以及目前市面上很多手机已经支持的 3.0 和 3.1 版本(shader 依然存在, 只是变的更加复杂)。所以在 EGL 中需要指定使用哪个版本的 OpenGL ES。
再之后,有 6 个 API,用于操作 EGL 上纹理,以及与多线程相关的高级功能。 纹理图片,又称纹理贴图,一般都是在 OpenGL ES 中,当顶点已经固定,具体形状已经成型的时候,将其贴上去,把虚拟的形状变成一个可以看见的物体。比如我们用 OpenGL ES 绘制,用顶点坐标勾勒出一个球形,然后把世界地图作为纹理贴上去,那么这个球看上去就变成了地球。所以按照理解纹理应该就是在绘制的时候进行使用,但是在 EGL 中偶尔也会使用到。
EGL 是用于在手机中生成绘制 buffer 提供给 OpenGL ES 进行绘制的,那么有时候也会设计到多线程操作,每个 thread 可以拥有自己的 surface 和 context,但是也要满足刚才我们所说的限制。一个 thread 同一时间只能启动有相同格式的 一块 surface 和一块对应于 OpenGL ES 的 context,一块 context 同时也只能被一 个 thread 启动。
最后还有 2 个 API,分别是用于初始化某个版本的 EGL,以及检测在执行上述 EGL API 的时候是否产生错误和产生了什么错误。
这些 API 有一些属于基本 API,就是在任何手机图形程序中都会使用到的 API, 这一届会把这一部分的 API 做详细介绍。剩下一些属于进阶版的 API,由于我们 这几节课主要还是为了讲解 OpenGL ES,那些进阶版的 API 会放到后面的课程进行补充学习。
EGL API 详解
EGLint eglGetError(void);
当我们调用 EGL 的 API 的时候,大部分 API 可以通过返回值判断这个 API 执 行的成功还是失败。比如当返回值的类型是 EGLBoolean 的时候,返回 EGL_TRUE 代表着成功,而 EGL_FALSE 代表着失败。而比如当创建 context 的时候,返回一个正常的 context 代表着成功,返回 EGL_NO_CONTEXT 则代表着失败。但是,当失败的时候,可能我们还需要更详细的信息,来判断为什么失败,是传入参数有误,还是发生了别的冲突之类的情况。所以我们需要 eglGetError 这个函数,它的功能是用于返回当前 thread 如果 EGL 的 API 出错的话,最近一个错误所对应的错误代码。
这个函数的输入为空。因为这个 API 是针对目前结果进行判断的,所以不需要任何输入。其实在 GPU driver 中,当执行 EGL API 的时候,如果出错了,会将错误代码写在寄存器中,然后通过这个函数直接去到寄存器去取即可,所以不需要任何输入。
这个函数的输出是可以用来判断详细错误信息的错误代码。比如当返回 EGL_SUCCESS 的时候,说明截至到目前为止,所有的 EGL API 都运行正常,没有出错。除此之外还有 15 个错误代码,标志着 15 种错误情况,这 15 种情况等我们说到对应 API 的时候再进行具体的解释说明。
有一种特殊情况,假如在调用这个 API 之前,出现过不止一次错误,那么调用这个 API 获取的将是最近一次错误的错误代码,并将该错误代码的标记重置。然后再调用一次,获取的将是倒数第二次错误的错误代码,以此类推,直至所有被标记的错误代码全部被重置后,再调用这个 API,则返回EGL_SUCCESS。
EGLDisplay eglGetDisplay(EGLNativeDisplayType display_id);
这个函数的功能是用于从 EGL 运行的操作系统中获取一个 Display 的 handle。
这个函数的输入是 display_id,这个 display_id 我们可以看作是从操作系统中得知的 Display 的 ID。假如一个手机是由多个显示屏,那么不同的 ID 可能就会对应于不同的屏幕,而究竟哪个屏幕对应哪个 ID,我们需要从操作系统中得知, 然后将它传给这个函数。并且由于 EGL 可以运行在多种操作系统上,所以针对不同的操作系统,这里的输入值 display_id 的格式也不同。
但无论是任何操作系统,如果 display_id 为 EGL_DEFAULT_DISPLAY,会得到一个默认的 Display。
这个函数的输出是用于显示图片绘制的 Display 的 handle。绝大多数 EGL 的 API 都会与这个 Display 的 handle 有关,所有 EGL 相关的对象,比如 surface、context 等,都与这个值有关,且存在于这个 display 的命名空间中。大多数情况下,这 个 display 对应着一块物理的屏幕。
另外,无论调用这个函数多少次,只要 display_id 不变,那么返回值就不变, 如果没有一个 Display 是对应这个 display_id 的,那么就会返回 EGL_NO_DISPLAY,不报任何错误。
EGLBoolean eglInitialize(EGLDisplay dpy, EGLint *major, EGLint *minor);
这个函数的功能是用于针对某 display 初始化某版本的 EGL。
这个函数输入的第一个参数是 EGLDisplay,我们刚才已经说了,EGL 的绝大 多数 API都会与这个 Display 的 handle 相关,EGL 所有的对象,都存在于这个 display 的命名空间中。所以我们需要针对这个 display,对 EGL 进行初始化,初始化的时候还需要指定 EGL 的版本。因为发展到现在 EGL 也是存在很多版本,目前使用的比较多的是 EGL1.4 版本。所以这个函数的第二个和第三个参数,也就是用于指定 EGL 的版本号,其中 major 对应着大号码,minor 对应着小号码,所以 1.4 在 这里,major 是 1,minor 是 4。而当 major 和 minor 为 NULL 的时候,则不对 EGL 进行初始化。
这个函数的输出是对 EGL 初始化的结果,当 EGL 初始化成功的时候,返回 EGL_TRUE。失败的话返回 EGL_FALSE。我们刚才在说 API eglGetError 的时候,就说了,成功的情况只有一个,但是错误的情况却千千万,仅靠一个返回值 EGL_FALSE 是无法判断失败在哪里了。所以我们还需要借用 eglGetError 这个 API 来抓取错误代码,进行判断。假如这个函数返回 EGL_FALSE 的时候,我们调用 eglGetError 来看下错误代码,假如获取的错误代码是 EGL_BAD_DISPLAY,则说明第一个输入参数 dpy 并非一个合法的 EGLDisplay;假如获取的错误代码是 EGL_NOT_INITIALIZED,则说明虽然 dpy 是合法的 EGLDisplay,但是依然无法针对 其初始化 EGL。
另外,可以针对一个已经初始化 EGL 过的 display 重新进行初始化,唯一的 结果就是返回 EGL_TRUE,并且更新 EGL 的版本号。一个在某个 thread 已经初始 化 EGL 过的 display,可以直接使用在另外一个 thread 中,无需再进行初始化。
EGLBoolean eglGetConfigs(EGLDisplay dpy, EGLConfig *configs, EGLint config_size, EGLint *num_config);
这个函数的功能是用于获取某 display 支持的配置信息。刚才已经介绍过, 不同的手机支持的配置信息可能是不同的。假如一个手机具备两个屏幕,那么这两个屏幕分别对应于两个 display,这两个 display 支持的配置信息可能也是不同的。那么在这里,我们就以 display 为单位,查询这个 display 支持的配置信息。
这个函数输入的第一个参数是 EGLDisplay。用于查看特定 Display 的配置信息。第二个参数是一个指针,一般会被预留一定的空间,内部为空,在调用这个 API 的时候,将 display 支持的配置信息存储在这个指针中,这个参数与第三个参数相关。第三个参数是我们想要获取的手机配置信息的最大个数,假如 display 支持 50 种配置信息,而我们只想要 20 个,那么第三个参数传入 20,则第二个参数中的指针只会保存 20 个配置信息的内容,如果 display 支持 50 个配置信息, 但是我们想要 100 个,那么第三个参数传入 100,第二个参数中的指针还是只会保存 50 个配置信息的内容。第四个参数是 display 支持配置信息的个数,一般会传入一个变量的地址,然后将配置信息的个数写在变量中。所以虽然看上去是 4 个输入参数,其实应该是两个输入参数,两个输出参数,第一和第三位输入参数, 第二和第四个参数经过这个 API,把 display 对应的配置信息,get 出来了。
这个函数的输出是获取配置信息的结果,当成功的时候,返回 EGL_TRUE。 失败的话返回 EGL_FALSE。比如,当输入参数 dpy 是一个合法的 EGLDisplay,但是这个 display 并没有通过 eglInitialize 被初始化的时候,返回 EGL_FALSE,通过 eglGetError 获取的错误代码为 EGL_NOT_INITIALIZED。如果第四个参数传入的不是一个变量的地址,而是 NULL,那么,返回 EGL_FALSE,通过 eglGetError 获取的错误代码为 EGL_BAD_PARAMETER。
另外,如果第二个参数传入的不是一个指针,而是 NULL,那么就不会有配置信息返回,不过第四个参数依然会返回 display 支持的配置信息的数量。
EGLBoolean eglChooseConfig(EGLDisplay dpy, const EGLint *attrib_list, EGLConfig *configs, EGLint config_size, EGLint *num_config);
这个函数的功能是用于获取与需求匹配,且某 display 支持的配置信息。刚才已经介绍过如何获取 display 支持的配置信息,那么 display 可能支持多种配置信息,但我们在写程序的时候,其实只需要选择一种与我们所写的图形程序匹配的即可。那么我们会制定一个需求,这个需求写在一个指针中,以 key-value 对的形式存在,key 是 EGLConfig 的属性,比如 EGL_RED_SIZE,代表所需要配置信息中红色分量的尺寸,EGL_STENCIL_SIZE,代表所需要配置信息中 stencil 分量 的尺寸, EGL_RENDERABLE_TYPE,代表所需要配置信息中的绘制 API 类型。value 就是这些 key 所对应的我们所需要的值。这里的需求信息并非需要把 EGLConfig的属性完全定义一遍,只需要定义一些我们需要的信息,然后通过 eglChooseConfig 这个 API,会遍历该 display 所支持的所有配置信息,然后获取到所有与需求信息匹配,且 display 支持的配置信息。
这个函数输入的第一个参数是 EGLDisplay。用于查看特定 Display 的配置信息。第二个参数是一个保存了需求信息的指针,指针中已经定义好了需求。第三个参数是一个指针,一般会被预留一定的空间,内部为空,然后用于在这个 API 中,将与需求信息符合,且 display 支持的配置信息存储在这个指针中,这个参数与第四个参数相关。第四个参数是我们想要获取的匹配配置信息的最大个数, 假如 display 中匹配配置信息有 50 个,而我们只想要 20 个,那么第四个参数传入 20,第三个参数中的指针只会保存 20 个配置信息的内容,如果 display 中匹配配置信息有 50 个,但是我们想要 100 个,那么第四个参数传入 100,第三个参数中的指针还是只会保存 50 个配置信息的内容。第五个参数是 display 匹配配置信息的个数,一般会传入一个变量的地址,然后将匹配配置信息的个数写在变量中。所以虽然看上去是五个输入参数,其实应该是三个输入参数,两个输出参数, 第一和第二和第四位输入参数,第三和第五个参数经过这个 API,把 display 匹配的配置信息,get 出来了。
这个函数的输出是获取配置信息的结果,当成功的时候,返回 EGL_TRUE。 失败的话返回 EGL_FALSE。比如,当第二个输入参数,在制定需求的时候,使用到了一个非法的 EGLConfig 的属性,或者某个 EGLConfig 属性对应的 value 不识别或者超出了范围,那么在执行了这个 API 之后,返回 EGL_FALSE,通过 eglGetError 获取的错误代码为 EGL_BAD_ATTRIBUTE 。
另外,在定义第二个参数,书写需求的时候,EGLConfig 的属性与 value 应该 一一对应,然后在结尾的地方写上 EGL_NONE。如果在需求中,没有定义到某个 EGLConfig 属性,那么就按照默认值处理。如果在需求中,针对某个 EGLConfig 属 性,对应的 value 为 EGL_DONT_CARE,则在匹配 display 配置信息的时候忽略这个属性,EGL_DONT_CARE 这个值可以制定给任何属性,除了 EGL_LEVEL。
如果第二个参数传入的不是一个指针,而是 NULL,或者第二个参数的第一个值就是 EGL_NONE,那么就按照 EGLConfig 默认的标准对配置信息进行选择和排序。
我们来简单的介绍一下这个默认的标准,刚才我们介绍了,如果在需求中, 没有定义到某个 EGLConfi 属性,那么就按照默认值处理。比如假如我们没有在需求中定义 EGL_RED_SIZE,那么其实相当于我们需求的 EGL_RED_SIZE 为 0,然后在手机的配置信息中,我们会过滤出所有 EGL_RED_SIZE 大于等于 0 的配置信息,依此类推,当所有的属性都过滤完毕之后,如果没有配置信息满足要求,那么依然返回 EGL_TRUE,但是最后一个参数将返回 0。如果超过一条配置信息满足要求,那么我们需要对这些配置信息进行排序, 由于配置信息有很多属性,我们会按照属性来对配置信息进行排序,且属性与属性之间也是有优先级的,优先级最高的是属性 EGL_CONFIG_CAVEAT,但是由于这个属性默认值为 EGL_DONT_CARE,那么我们除非特别需求,否则就会跳过这个属性。假如我们对这个属性进行特殊需求,比如在需求中定义这个属性对应的 value 为 NULL,那么就会对手机配置信息中的这个属性进行排序,排序的顺序 是 EGL_NONE、EGL_SLOW_CONFIG, EGL_NON_CONFORMANT_CONFIG。优先级第二的属性是 EGL_COLOR_BUFFER_TYPE,在这里我们就不对这些属性的默认值、 优先级、排序方式进行一一说明了。
EGLBoolean eglBindAPI(EGLenum api);
这个函数的功能是设置当前 thread 的绘制 API,后面创建的 surface 和 context 要与这个 API 相匹配。
这个函数输入的参数是绘制 API。比如 EGL_OPENGL_API、EGL_OPENGL_ES_API, 或者 EGL_OPENVG_API。需要注意的是,从这里开始 OpenGL 的 API 与 OpenGL ES 的 api 将开始分离,OpenGL 的 API 主要用于 PC 端的绘制,OpenGL ES 的 API 主 要用于移动端的绘制,所以在这里,我们一般是传入 EGL_OPENGL_ES_API 这个参数。OpenVG 是另外一种绘制 API,在这里我们用不到,也就不进行详细的描述。
这个函数如果成功,则返回 EGL_TRUE。如果失败,则返回 EGL_FALSE 。 我们可以通过错误代码判断失败的原因。假如传入参数不是刚才我们所介绍的三种 API,或者设备不支持我们传入的参数 API,那么错误代码为 EGL_BAD_PARAMETER。
EGLSurface eglCreateWindowSurface(EGLDisplay dpy, EGLConfig config, EGLNativeWindowType win, const EGLint *attrib_list);
这个函数的功能是用于根据需求,创建一个 on-Screen 的 rendering surface, 可以提供给绘制 API,比如 OpenGL ES 进行绘制。surface 一共有三种,这个只是其中的一种,另外还有两种分别是通过 eglCreatePbufferSurface 以 及 eglCreatePixmapSurface 来创建。我们对这三种 surface 进行一下对比。EGL 和 OpenGL ES 支持两种绘制模式,back buffer 和 single buffer,windows surface 和 pbuffersurface 都是使用的 back buffer,顾名思义,也就是一块显存(GPU)中的 buffer,当绘制完毕的时候,由于 windows surface 于 window 有关联,那么可以使用 eglswapbuffer 将其转移到 window 上进行显示。而 pbuffer 于 window 没有关联,也就无法显示。而 pixmapsurface 是使用的 single buffer,single buffer 可以看作是保存在系统内存中的位图,OpenGL ES 不支持将其转移到 windows 进行显示,所以 pixmapsurface 也是不可显示的。
由于另外两种 surface 都是不可显示的 surface,且使用的比较少,这里也就不具体介绍它们的创建函数。
这个函数输入的第一个参数是 EGLDisplay。用于指定一个特定的 Display 进行 surface 创建。第二个参数是用于创建 surface 的配置信息,一般我们会把 eglChooseConfig 得到的已经匹配好的配置信息传入,刚才我们也已经知道了,匹配好的配置信息可能有很多个,不过它们已经排好序,那么我们就可以直接取第一个作为这里的输入参数。第三个参数是一个平台相关的参数,是 native window 的 handle。第四个参数,类似于 eglChooseConfig 的第二个参数,是需求信息, 格式也类似,都是 key-value 对,刚才的 key 是 EGLConfig 的属性,这里的 key 是 EGL_RENDER_BUFFER 等属性,它也是提供了一个接口,可以给一些特殊的平台创建 surface 的时候规定一些特殊的属性,类似 EGL extension。这里我们拿一个属性进行解释,比如 EGL_RENDER_BUFFER,它就定义了绘制 API 绘制的时候应该会绘制到哪个 buffer 中,可以绘制到 single buffer,也可以绘制到 back buffer, 我们已经知道了 windowsurface 创建的是 backbuffer,那么如果绘制到 single buffer,则相当于直接绘制到屏幕上,如果绘制到 back buffer,那么就会先绘制到 back buffer,再通过 eglswapbuffer 转移到屏幕上。
这个参数也可以直接写成 NULL,或者第一个值就是 EGL_NONE,那么所有的属性对应的 value 就按照默认值处理,比如 EGL_RENDER_BUFFER 的默认值为 back buffer。
这个函数如果成功,则输出是创建的 rendering surface 的 handle。如果失败, 则返回 EGL_NO_SURFACE。同样的,我们也可以通过错误代码判断失败的原因。 假如是第三个参数 native window 的 handle 与第二个参数 display 的配置信息 EGLConfig 不匹配,那么错误代码为 EGL_BAD_MATCH。 如果第二个参数,在配置信息中显示不支持绘制到 window,也就是在配置信息的属性 EGL_SURFACE_TYPE 中不包含 EGL_WINDOW_BIT 这一位的时候,当然默认这个属性是包含这一位的。错误代码也是 EGL_BAD_MATCH。
如果第二个参数,我们需求中的 color 和 alpha 信息与第四个参数我们额外的需求不匹配,错误代码也是 EGL_BAD_MATCH。这种情况确实很有可能发生, 比如我们在 eglChooseConfig 的时候没有强调使用什么样子的 color 或 alpha 信息, 然后根据手机自动匹配,可能匹配一个普通的 color 和 alpha 信息,但是我们在 这个 API 中又通过第四个参数增加了对 color 和 alpha 的需求,那么很有可能就不匹配,也就出现了这种错误。
其他的,如果第二个参数 EGLConfig 不是一个合法的 config,那么错误代码 EGL_BAD_CONFIG。
如果第三个参数 win 不是一个合法的 native window 的 handle,那么错误代 码 EGL_BAD_NATIVE_WINDOW。
如果已经使用第三个参数的 win 创建过一个 windowsurface,或者是其他的任何情况导致在创建 windowsurface 的时候分配资源失败,那么错误代码 EGL_BAD_ALLOC。
EGLContext eglCreateContext(EGLDisplay dpy, EGLConfig config, EGLContext share_context, const EGLint *attrib_list);
这个函数的功能是用于根据需求,针对当前的绘制 API 创建一个 rendering context。
rendering surface 需要与 rendering context 进行搭配使用的,我们知道 context 中是可以保存 OpenGL ES 状态集信息的,所以如果一对 surface 和 context 兼容, 那么 context 就可以使用自己内部保存的信息往 rendering surface 上进行绘制。
通过这个 API 可以在 context 中针对绘制 API 初始化一套状态集。
这个函数输入的第一个参数是 EGLDisplay。用于针对哪个 Display 进行 context 的创建。第二个参数是用于创建 context 的配置信息,一般我们会把 eglChooseConfig 得到的已经匹配好的配置信息传入,和创建 surface 的时候类似, 匹配好的配置信息可能有很多个,不过它们已经排好序,那么我们一般会直接取第一个作为这里的输入参数。第三个参数可以是另外一个 context 的 handle,那么新创建的 context 就可以与该 context 共享所有可以共享的数据,如果该 context 之前已经与其他 context 进行了共享,那么它们三个或者多个之间,都可以进行共享,在 OpenGL ES 对应的 Context 之间,共享的东西可以有纹理、program 和 BO 等信息。如果第三个参数为 NULL,那么该 context 暂时不与其他 context 共享。 第四个参数,类似于 eglCreateWindowSurface 的第四个参数,是需求信息,格式也类似,都是 key-value 对,这里的 key 只有一个,就是 EGL_CONTEXT_CLIENT_VERSION 属性,该属性只针对 OpenGL ES API 对应的 context 有效,也就是用于指定该 context 是针对 OpenGL ES 的哪个版本。如果 value 为 1, 则针对 OpenGL ES 1.x 版本,如果 value 为 2,则针对 OpenGL ES 2.x 版本,如果 value 为 3,则针对 OpenGL ES 3.x 版本。这个参数也可以直接写成 NULL,或者第一个值就是 EGL_NONE,那么属性对应的 value 就按照默认值处理,而 EGL_CONTEXT_CLIENT_VERSION 的默认值为 1。
这个函数如果成功,则输出是创建的 rendering context 的 handle。如果失败, 则返回 EGL_NO_CONTEXT 。同样的,我们也可以通过错误代码判断失败的原因。 假如当前绘制 API 为 EGL_NONE 的时候,也就是当前设备不支持 OpenGL ES,且没有设置当前的绘制 API 的时候,错误代码为 EGL_BAD_MATCH。 如果第二个参数,不是一个合法的 EGLConfig,或者不支持参数四指定的具体版本的绘制 API,则错误代码为 EGL_BAD_CONFIG。这种情况确实很有可能发生,比如我们在 eglChooseConfig 的时候没有强调使用什么样子的 EGL_RENDERABLE_TYPE,那么默认为 EGL_OPENGL_ES_BIT,但是参数四指定了具体的版本,也就是如果参数四指定的是 1,那么在 config 中 EGL_RENDERABLE_TYPE 需要是 EGL_OPENGL_ES_BIT;如果参数四指定的是 2,那么在 config 中 EGL_RENDERABLE_TYPE 需要是 EGL_OPENGL_ES2_BIT;如果参数四指定的是 3,那么在 config 中 EGL_RENDERABLE_TYPE 需要是 EGL_OPENGL_ES3_BIT。 假如第三个参数 share context,不是 NULL ,而是一个合法的但与当前绘制 API不匹配的 context,错误代码为 EGL_BAD_CONTEXT。
如果第三个参数制定的 share_context 是针对另外一个 display 的话,那么错误代码为 EGL_BAD_MATCH。
如果没有足够的资源用于生成这个 context,那么错误代码为EGL_BAD_ALLOC。
EGLBoolean eglMakeCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx);
这个函数的功能是用于 enable surface 和 context,也就是将一个指定的 context绑定到当前的绘制thread上,与读、写的surface关联上。make current 之后,就可以调用 OpenGL ES 的 API 对 context 中的状态集进行设定,然后进而往 surface 中绘制内容,再从 surface 中把内容读取出来。
总结一下,一个 native window handle 只能创建一个 rendering surface,而一个 display 可以创建多个 rendering surface。context 与 native window 无关,也就是 display 可以创建多个 context,每个 context 对应一种绘制 API,只要 surface 和 context 的格式匹配,两者就可以进行关联,但是同一时间,一个 surface 只能 和一个 context 进行关联,一个 thread 中,一种绘制 API 也只能有一个 context。
这个函数输入的第一个参数是 EGLDisplay。用于指定操作特定的 Display。第二个参数是一个 surface,该 surface 用于写入以及其他除了读取和复制之外的所有操作。第三个参数也是一个 surface,该 surface 用于读取和复制。需要注意的是第二个参数和第三个参数可以是同一个参数,一般我们也会将它们设置为同一个参数。第四个参数就是传入我们将会 enable 的 context,如果当前 thread 已经有了一个相同绘制 API 的 context,那么之前的这个 context 就会先进行 flush 操 作,把未执行的命令全部执行完毕,然后将其设置为 disable 状态,再把新传入的 context 设置为 enable 状态。
这个函数如果成功,则返回 EGL_TRUE。如果失败,则返回 EGL_FALSE。同样的,我们也可以通过错误代码判断失败的原因。
假如第一个参数 dpy 不是一个合法的 EGLDisplay handle,那么错误代码EGL_BAD_DISPLAY。
假如第二个或者第三个参数的 surface 与 context 不匹配的话,错误代码为EGL_BAD_MATCH。
假如第二个、第三个、第四个参数的 surface 和 context 中的其中一个目前被别的 thread 使用,那么错误代码为 EGL_BAD_ACCESS。
假如第二个或者第三个参数的 surface 不是合法的 surface,那么错误代码为EGL_BAD_SURFACE。
如果第四个参数 ctx 不是一个合法的 context,那么错误代码为EGL_BAD_CONTEXT。
假如第二个或者第三个参数的 surface 对应的 windows 不再合法,那么错误代码为 EGL_BAD_NATIVE_WINDOW。
假如第二个或者第三个参数的 surface 不匹配,则错误代码 EGL_BAD_MATCH。
假如当前 thread 目前针对该绘制 API 已经有 context,且该 context 存在未 flush的命令,而且旧的 surface 又突然变成不合法的了,那么错误代码 EGL_BAD_CURRENT_SURFACE。
第二个或者第三个参数的 surface 在 context 需要的时候,会分配 buffer,但是假如无法针对这个 API 再分配 buffer 了,则错误代码 EGL_BAD_ALLOC。一旦分配成功,这个 buffer 会一直伴随着 surface,直到 surface 被删除。
假如这个 API 调用成功,但是之后,draw surface 被破坏掉了,那么剩下的绘制命令还是会执行,context 的内容依然会被更新,但是写入 surface 的内容会变成未定义。
假如这个 API 调用成功,但是之后,read surface 被破坏掉了,那么读取的数据(比如使用 glReadPixel 读取)为未定义。
如果想要释放当前的 context,也就是将当前的 context disable,那么将第二个和第三个参数设置为 EGL_NO_SURFACE,第四个参数设置为 EGL_NO_CONTEXT 即可。设置完之后原本 context 中未运行的绘制 API 会被 flush,然后将 context 设置为 disable。在运行这种情况的时候,第一个参数 dpy 可以被传入一个未初 始化的 display,而除了这种情况,假如 dpy 传入一个合法的,但是未初始化的 display,错误代码为 EGL_NOT_INITIALIZED。
如果 context 为 EGL_NO_CONTEXT,但是 surface 不是 EGL_NO_SURFACE。或者 surface 是 EGL_NO_SURFACE,而 context 不是 EGL_NO_CONTEXT,那么错误代 码 EGL_BAD_MATCH。
OpenGL ES 中有一个概念叫做视口,视口会有一个视口大小的,这个视口大小是用于表示在一定尺寸的绘制 buffer 中,有多大一块空间会被显示出来。当这个 API 运行成功,且 current context 是针对 OpenGL ES 的 API 的时候,这个视口大小和裁剪大小都会被设置为 surface 的尺寸。关于视口和裁剪,我们会在下面讲 OpenGL ES API 的时候进行详细说明,这里只要知道 makecurrent 之后,由于 surface 对应 windows,那么 surface 从 window 获取到尺寸,然后再被 context 得到,作为 OpenGL ES 绘制的某个值的初始状态。
EGLBoolean eglSwapBuffers(EGLDisplay dpy, EGLSurface surface);
当 OpenGL ES 把内容绘制到 surface 上之后,可以通过这个函数,把 surface 中 color buffer 的内容显示出来。我们还记得 surface 中可能有 color buffer、depth buffer、stencil buffer,而被展示的只是 color buffer。也就是通过这个函数,让我们看到了手机上不停变换显示的图片。
这个函数输入的第一个参数是 EGLDisplay。用于指定特定的 display 进行显示。第二个参数是一个 surface,就是指定显示特定的 surface 中的内容。
这个函数如果成功,则返回 EGL_TRUE。如果失败,则返回 EGL_FALSE。
假如这个 surface 的属性 EGL_SWAP_BEHAVIOR 不是 EGL_BUFFER_PRESERVED, 那么 surface 中 color buffer 的内容为未定义。
EGLBoolean eglTerminate(EGLDisplay dpy);
这个函数的功能是用于将特定 display 对应的 EGL 相关的资源释放,比如与 这个 display 关联的 surface、context 等。如果某个 surface 和 context 在被释放的时候,依然被使用着,那么它们并没有被真正的释放掉。如果继续使用它们用于 OpenGL ES 绘制的话,并不会导致程序瘫痪,而只会使得绘制结果不确定,并且绘制命令出错。只有当使用 API eglReleaseThread,把整个 thread 释放掉,或者使用 eglMakeCurrent 把该 surface 和 context 设置为非当前使用的资源,它们才会被真正的释放掉。
当调用了 API eglTerminate 之后,surface 和 context 等会变成 invalid,如果在eglTerminate 之后,通过其他的 EGL API 再继续使用这些资源,会得到错误代码 EGL_BAD_SURFACE 或者 EGL_BAD_CONTEXT。
这个 API 与 eglInitialize 相对应。
这个函数输入的第一个参数是 EGLDisplay。用于特定释放哪个 Display 对应的资源。
这个函数的输出是资源释放的结果,当成功的时候,返回 EGL_TRUE。失败的话返回 EGL_FALSE。比如,当输入参数 dpy 不是一个合法的 EGLDisplay 的时候, 返回 EGL_FALSE,通过 eglGetError 获取的错误代码为 EGL_BAD_DISPLAY 。
如果一个 display 已经被 terminate 了,或者尚未 init。那么它本身其实并没有相对应的 EGL 的相关资源,这个时候对这个 display 进行 terminate 其实并没有意义,虽然是被允许的,但是唯一的结果就是会返回 EGL_TRUE。
如果一个 display 已经被 terminate 了,这个时候可以对它进行 re-init,只是 re-init 之后,那些已经被标记为删除的资源不变,依然保持标记为删除,使用 它们仍然是不合法的。
总结一下,一个 display 随时随地可以被初始化或者终止。而所有的 display 都是起始于终止状态。当调用了 eglInitialize,且成功之后,display 会变成初始成功;调用了 eglTerminate,且成功之后,display 就变成终止状态。
在 display 处 于 终 止 状 态 , 只 有 eglMakeCurrent 和 eglReleaseThread , eglInitialize 和 eglTerminate 这 4 个 EGL 的 API 还可以被正常工作,而前两个 API 是用于进一步的清除这些资源。而除了这 4 个 api,如果再调用任何其他 api,虽然 display 本身还是 valid,但是这个时候它已经被终止了,那么会产生错误代码 EGL_NOT_INITIALIZED。
EGL API 总结
EGL 的 API 还有很多,这一节只是把其中最重要也是最常用的 11 个 API 拿出来进行了讲解,最后总结一下 EGL 使用的大概流程如下:
先获取 display 的 handle,对 display 进行 EGL 初始化。从设备上获取匹配的配置信息,再绑定一个绘制 API 用于之后的绘制。根据获取 display 的 handle、 配置信息以及当前绘制 API 生成 surface 和 context,再把它们绑定在一起,绑定在当前 thread 上,下面就可以使用绘制 API 进行绘制。绘制完成之后,可以把绘制的 surface 中的 color buffer 拿出来显示。最后,记得把 display 上 EGL 相关资源进行释放。
除此之外还有很多重要的 API,比如和配置信息相关的 eglGetConfigAttrib, 用于获取配置信息中具体属性对应的值。比如对 surface 和 context 用完之后的删除 API eglDestroySurface 和 eglDestroyContext 等。
这些内容等我们以后讲 EGL 补充学习的时候再进行详细说明。
本节教程就到此结束,希望大家继续阅读我之后的教程。
谢谢大家,再见!
写在末尾的话
在Git项目ShineEngine中,使用的是Android Studio进行开发,采取了sdk+ndk的方式,java层使用了控件GLSurfaceView,所以没有使用这完整的11个EGL API。
其中,创建了GLSurfaceView,然后通过GLSurfaceView.EGLConfigChooser接口获取一个合适的EGLConfig(其中使用到了eglChooseConfig),然后通过GLSurfaceView.EGLContextFactory接口创建一个合适的Context(其中使用到了eglCreateContext)。由于将这个GLSurfaceView直接进行显示了,所以在项目中看不到eglGetDisplay、eglInitialize、eglBindAPI、eglCreateWindowSurface、eglMakeCurrent这些API。这样EGL的环境,依然已经配置完毕。
由于采用了ndk的方式,所以GL API写在了c文件中,所以在Cmake中需要在target_link_libraries中加入GLESv2库。目前程序按计划运行成功。