问题的提出
当在代码里需要用到 OpenGL 的时候,需要增加对 OpenGL/EGL 库的引用,以 BootAnimation
开机动画为例,在其 Android.bp 里就有如下的内容:
cc_library_shared {
name: "libbootanimation",
defaults: ["bootanimation_defaults"],
srcs: ["BootAnimation.cpp"],
shared_libs: [
"libui",
"libjnigraphics",
"libEGL", // EGL 库
"libGLESv1_CM", // OpenGL ES 库
"libgui",
],
}
但是这里有一个问题,我们都知道,实际上 OpenGL ES API 的真正实现都是由 vendor 厂商(例如高通/MTK)来做的,由此就引出了下面的几个问题:
- libEGL/libGLES 这些库的内容是什么?
- libEGL/libGLES 这些库是怎么与真正的,vendor 实现的库是什么关系?他们之间是怎么关联起来的?
接下来我们一个一个看。
详解
在 frameworks/native/opengl/libs/
下,会编译出 4 个库:
- libEGL
- libGLESv1_CM
- libGLESv2
- libGLESv3
其中后面的 3 个库就是所谓的 wrapper 库(从 Android.bp
里的注释可以看到):
//##############################################################################
// Build the wrapper OpenGL ES 1.x/2.x/3.x library
//
libEGL
这个库依赖的几个 cpp 文件,作用分别是:
eglApi.cpp
在 eglApi.cpp
里,暴露了一系列的的 EGL API,而这些 API 都会统一跳转到 egl_connection_t
里的 platform
下的实现。
egl_connection_t
的定义是在 frameworks/native/opengl/libs/EGL/egldefs.h
里:
struct egl_connection_t {
enum { GLESv1_INDEX = 0, GLESv2_INDEX = 1 };
inline egl_connection_t()
: dso(nullptr),
libEgl(nullptr),
libGles1(nullptr),
libGles2(nullptr),
systemDriverUnloaded(false) {
const char* const* entries = platform_names;
EGLFuncPointer* curr = reinterpret_cast<EGLFuncPointer*>(&platform);
while (*entries) {
const char* name = *entries;
EGLFuncPointer f = FindPlatformImplAddr(name);
if (f == nullptr) {
// If no entry found, update the lookup table: sPlatformImplMap
ALOGE("No entry found in platform lookup table for %s", name);
}
*curr++ = f;
entries++;
}
}
void* dso; // 实际的类型是 Loader::driver_t,在 egl.egl_init_drivers_locked() 里通过 Loader::open() 赋值
gl_hooks_t* hooks[2]; // 在 Loader::initialize_api() 里赋值
// 一般情况下,实际的意义就是 GLESv1/GLESv2 里所有的函数指针实现
EGLint major;
EGLint minor;
EGLint driverVersion;
egl_t egl; // 跟上面的 hooks 一样,也是 vendor 实现的 libEGL 里所有的函数指针实现
// Functions implemented or redirected by platform libraries
platform_impl_t platform; // 在上面的构造函数进行初始化
void* libEgl; // libEGL wrapper 库 dlopen 返回的 handle
void* libGles1; // libGLESv1_CM wrapper 库 dlopen 返回的 handle
void* libGles2; // libGLESv2 wrapper 库 dlopen 返回的 handle
bool systemDriverUnloaded;
bool useAngle; // Was ANGLE successfully loaded
};
在 egl_connection_t
的构造函数里,最主要的工作就是使用 FindPlatformImplAddr()
初始化 platform
里的各个 GLES/EGL 的函数指针(类型为 platform_impl_t
,内容是 platform_entries.in
里包含的函数指针)。而 FindPlatformImplAddr()
的实现是在 egl_platform_entries.cpp
egl_platform_entries.cpp
egl_platform_entries.cpp
最重要的内容是通过 FindPlatformImplAddr()
,对 egl_connection_t
里的 platform_impl_t
的各个函数指针进行初始化:
EGLFuncPointer FindPlatformImplAddr(const char* name) {
static const bool DEBUG = false;
if (name == nullptr) {
ALOGV("FindPlatformImplAddr called with null name");
return nullptr;
}
for (int i = 0; i < NELEM(sPlatformImplMap); i++) {
if (sPlatformImplMap[i].name == nullptr) {
ALOGV("FindPlatformImplAddr found nullptr for sPlatformImplMap[%i].name (%s)", i, name);
return nullptr;
}
if (!strcmp(name, sPlatformImplMap[i].name)) {
ALOGV("FindPlatformImplAddr found %llu for sPlatformImplMap[%i].address (%s)",
(unsigned long long)sPlatformImplMap[i].address, i, name);
return sPlatformImplMap[i].address;
}
}
ALOGV("FindPlatformImplAddr did not find an entry for %s", name);
return nullptr;
}
FindPlatformImplAddr()
初始化的原理是,将传递过来的 API 函数名,在 sPlatformImplMap
里找到相同的函数名并返回对应的函数指针。
这里需要说一下 egl_platform_entries.cpp
里最重要的一个变量: sPlatformImplMap
。首先他的类型是 implementation_map_t
数组:
struct implementation_map_t {
const char* name;
EGLFuncPointer address;
};
typedef __eglMustCastToProperFunctionPointerType EGLFuncPointer;
typedef void (*__eglMustCastToProperFunctionPointerType)(void);
implementation_map_t
的内容很简单,就是一个函数名以及其对应实现的函数指针。
而 sPlatformImplMap
的内容就是 platform_entries.in
里全部 86 个 OpenGLES/EGL 函数名和对应的实现,而这些实现也都是在 egl_platform_entries.cpp
里。
在编写 OpenGL 相关逻辑的时候,首先必要的操作是 EGL 的初始化,类似于下面的代码:
EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
eglInitialize(display, nullptr, nullptr);
EGLConfig config = getEglConfig(display);
EGLSurface surface = eglCreateWindowSurface(display, config, s.get(), nullptr);
EGLContext context = eglCreateContext(display, config, nullptr, nullptr);
EGLint w, h;
eglQuerySurface(display, surface, EGL_WIDTH, &w);
eglQuerySurface(display, surface, EGL_HEIGHT, &h);
if (eglMakeCurrent(display, surface, surface, context) == EGL_FALSE)
return NO_INIT;
Loader.cpp
sequenceDiagram
autoNumber
App ->> eglApi: eglGetDisplay()
eglApi ->> egl: egl_init_drivers()
Note right of egl: egl_init_drivers_locked() 这个函数应该很重要,需要重点看。
egl ->> +egl: egl_init_drivers_locked()
egl ->> +Loader: open()
Note right of Loader: 0. 注意,下面提到的驱动指的是 EGL/libGLESv1_CM/libGLESv2
Note right of Loader: 1. 如果使能 ANGLE 或者是 updated driver,需要先将原有的驱动重置
Loader ->> Loader: unload_system_driver()
Note right of Loader: 2. 优先加载 ANGLE
Loader ->> Loader: attempt_to_load_angle()
Note right of Loader: 3. 然后再加载 updated driver
Loader ->> Loader: attempt_to_load_updated_driver()
Note right of Loader: 4. 然后再加载 vendor(例如高通)的驱动
Loader ->> Loader: attempt_to_load_system_driver()
Note right of Loader: 5. 上面的都找不到,加载默认的 libGLESv1_CM/libGLESv2/libGLES_v3/libEGL
Loader ->> Loader: attempt_to_load_system_driver()
Loader->> -egl: open()
egl ->> -egl: egl_init_drivers_locked()
在 Loader::initialize_api()
里,会
driver_t
定义在 Loader.cpp
里,其完整的定义如下:
struct driver_t {
explicit driver_t(void* gles);
~driver_t();
// returns -errno
int set(void* hnd, int32_t api);
void* dso[3];
};
其中的 dso
的意义是 EGL/GLESv1_CM/GLESv2 这几个库在 dlopen()
以后返回的 handle。而 [[#eglApi cpp]] 里提到的 egl_connection_t
里的 dso
成员变量,其真正类型就是 driver_t
,是在 egl.egl_init_drivers_locked()
里赋值的:
static EGLBoolean egl_init_drivers_locked() {
......
cnx->dso = loader.open(cnx);
GLES_CM/GLES2
这两个文件夹结构是一样的,一个 .cpp 文件和两个 .in 文件。其中两个 .in 文件的作用定义了 OpenGLES 1.0/2.0 的标准 API 和拓展 API;而 .cpp 文件的作用就是在不同平台下解析宏的定义,最终将前面的两个 .in 文件解析为当前平台可用的函数。而为了能够在不同的平台下解析宏的定义,.cpp 做了下面的事情:
- 定义不同平台下,下面几个宏的定义:
API_ENTRY
CALL_GL_API_INTERNAL_CALL
CALL_GL_API_INTERNAL_SET_RETURN_VALUE
CALL_GL_API_INTERNAL_DO_RETURN
这里重点看一下 ARM64 下下这几个宏的实现:
tpidr_el0
根据 官方文档 的描述:
EL0 Read/Write Software Thread ID Register
TPIDR_EL0 是用户读写线程标识符寄存器(User Read and Write Thread ID Register),pthread 库用来存放每线程数据的基准地址,存放每线程数据的区域通常被称为线程本地存储(Thread Local Storage,TLS)。
mrs
MRS 指令的格式为:
MRS {条件} 通用寄存器 程序状态寄存器
MRS 指令用于将程序状态寄存器的内容传送到通用寄存器中。该指令一般用在以下两种情况:
- 当需要改变程序状态寄存器的内容时,可用 MRS 将程序状态寄存器的内容读入通用寄存器,修改后再写回程序状态寄存器。
- 当在异常处理或进程切换时,需要保存程序状态寄存器的值,可先用该指令读出程序状态寄存器的值,然后保存。
ldr
所以上面的宏的意义其实就是:找到 TLS 的地址,然后再偏移到 TLS_SLOT_OPENGL 。此时就已经是在 egl_connection_t.hooks
的基地址了,然后再按照特定的函数进行偏移,就能找到目标函数的函数指针并跳转了。
而上面的几个宏的后 3 个是用来定义平台通用的两个宏 CALL_GL_API
和 CALL_GL_API_RETURN
:
有了这上面的这两个宏跟 API_ENTRY
,就可以来解析前面提到的 .in 文件的。因此,紧接着 .cpp 将前面的两个 .in 给 include 进来:
extern "C" {
}
这些就把两个 .in 文件里的各个 OpenGL ES 的标准 API 和拓展 API,给解析成一系列的函数了。而函数的内容非常的简单,那就是通过一系列的,平台相关的汇编代码,跳转到线程的 OpenGL TLS 区域,而这个 TLS 区域在代码设置 EGL 环境调用 eglMakeCurrent()
的时候,进入系统的实现 eglMakeCurrentImpl()
的时候,使用 setGLHooksThreadSpecific()
:
setGLHooksThreadSpecific(c->cnx->hooks[c->version]);
设置为真正的 Open GLES 的 vendor 实现了。
总结
因此我们终于可以理解 libGLESv1_CM.so
和 libGLESv2.so
这些 wrapper 库的作用是什么了:
首先这些 wrapper 库肯定是不包含任何 OpenGL ES API 的真正实现的,因为这些实现都是交给芯片厂商依据自己的 GPU 有单独的,而且一般是闭源的实现。而这些 wrapper 库的作用是,如他的名字一样,作为一个中间层,承上启下:给 App 侧一个通用的 so 可以用,然后最终再转到真正的实现。
因此,这么一通操作下来以后,完整的流程其实就变得非常清晰了。一句话总结:
App 在调用
eglGetDisplay()
的时候,就会启动 EGL/OpenGL ES 的初始化,这个初始化主要都是在Loader.cpp
里进行的,最主要的就是将所有的 OpenGL ES API 的 vendor 实现的函数指针保存在egl_connection_t
里的hooks
。
然后在eglMakeCurrent()
里,使用setGLHooksThreadSpecific(c->cnx->hooks[c->version])
将上面的hooks
保存在当前渲染线程的 TLS 中。
最后,当 App 在执行其他的 OpenGL ES 的 API 的时候,首先都是调用到 wrapper 库,然后 wrapper 库里通过汇编代码找到渲染线程的 TLS 里的 TLS_SLOT_OPENGL 偏移就是前面保存的hooks
。然后再跳转到对应函数的偏移,就跳转到真正,vendor 侧的 OpenGL ES 实现了。
Author: simowce
Permalink: https://blog.simowce.com/how-opengl-egl-load/
本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可。