OpenGL/EGL 初始化流程详解

知其所以然

问题的提出

当在代码里需要用到 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 做了下面的事情:

  1. 定义不同平台下,下面几个宏的定义:
  • API_ENTRY
  • CALL_GL_API_INTERNAL_CALL
  • CALL_GL_API_INTERNAL_SET_RETURN_VALUE
  • CALL_GL_API_INTERNAL_DO_RETURN

这里重点看一下 ARM64 下下这几个宏的实现:

#elif defined(__aarch64__)

    #define API_ENTRY(_api) __attribute__((naked,noinline)) _api

    #define CALL_GL_API_INTERNAL_CALL(_api, ...)                    \
        asm volatile(                                               \
            "mrs x16, tpidr_el0\n"                                  \
            "ldr x16, [x16, %[tls]]\n"                              \
            "cbz x16, 1f\n"                                         \
            "ldr x16, [x16, %[api]]\n"                              \
            "br  x16\n"                                             \
            "1:\n"                                                  \
            :                                                       \
            : [tls] "i" (TLS_SLOT_OPENGL_API * sizeof(void*)),      \
              [api] "i" (__builtin_offsetof(gl_hooks_t, gl._api))   \
            : "x0", "x1", "x2", "x3", "x4", "x5", "x6", "x7", "x16" \
        );

    #define CALL_GL_API_INTERNAL_SET_RETURN_VALUE \
        asm volatile(                             \
            "mov w0, wzr \n"                      \
            :                                     \
            :                                     \
            : "w0"                                \
        );

    #define CALL_GL_API_INTERNAL_DO_RETURN \
        asm volatile(                      \
            "ret \n"                       \
            :                              \
            :                              \
            :                              \
        );

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_APICALL_GL_API_RETURN

#define CALL_GL_API(_api, ...) \
    CALL_GL_API_INTERNAL_CALL(_api, __VA_ARGS__) \
    CALL_GL_API_INTERNAL_DO_RETURN

#define CALL_GL_API_RETURN(_api, ...) \
    CALL_GL_API_INTERNAL_CALL(_api, __VA_ARGS__) \
    CALL_GL_API_INTERNAL_SET_RETURN_VALUE \
    CALL_GL_API_INTERNAL_DO_RETURN

有了这上面的这两个宏跟 API_ENTRY ,就可以来解析前面提到的 .in 文件的。因此,紧接着 .cpp 将前面的两个 .in 给 include 进来:

extern "C" {
#pragma GCC diagnostic ignored "-Wunused-parameter"
#include "gl2_api.in"
#include "gl2ext_api.in"
#pragma GCC diagnostic warning "-Wunused-parameter"
}

这些就把两个 .in 文件里的各个 OpenGL ES 的标准 API 和拓展 API,给解析成一系列的函数了。而函数的内容非常的简单,那就是通过一系列的,平台相关的汇编代码,跳转到线程的 OpenGL TLS 区域,而这个 TLS 区域在代码设置 EGL 环境调用 eglMakeCurrent() 的时候,进入系统的实现 eglMakeCurrentImpl() 的时候,使用 setGLHooksThreadSpecific()

setGLHooksThreadSpecific(c->cnx->hooks[c->version]);

设置为真正的 Open GLES 的 vendor 实现了。

总结

因此我们终于可以理解 libGLESv1_CM.solibGLESv2.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 国际许可协议 进行许可。