July 7, 2024 by Reion

Mesa EGL Draft

Mesa 是一个 OpenGL API 和规范实现的图形库,提供了许多图形 API 的实现,例如 OpenGL ES、Vulkan,这个项目是由一群开源社区的各路大神开发和维护,旨在为开源社区提供一个高质量、稳定的图形库,以及促进图形应用程序的开发,并且可在 Linux、macOS、Windows 等不同的操作系统中运行。 目前为止,Linux 桌面发行版上几乎都需要运行 Mesa,成为了 Linux 上最重要的图形库。

基础背景知识

OpenGL

OpenGL 全称为:【Open Graphics Library】,中文译名开放式图形库,是用于渲染 2D、3D 矢量图形的应用程序编程接口(API),OpenGL 本身并不仅仅是一个 API,同时也是一个规范,这个规范严格规定了每个函数该如何执行,输出值等,具体内部实现如何,由 OpenGL 库的开发者来自行决定,因为 OpenGL 并没有规定具体的实现细节,只要其功能和输出结果与规范相匹配即可。 通常 OpenGL 是为桌面工作站而设计的,提供大量的功能和高级特性,用于游戏,工程、科研等高性能图形应用。

OpenGL ES

OpenGL ES 全称是:【OpenGL for Embedded Systems】,专门针对嵌入式系统和移动设备等资源受限环境而设计的,是 OpenGL 的一个子集。 OpenGL ES 保留了与 OpenGL API 和语义,但是实现上和功能上有所不同;例如:多重纹理、多边形偏移、直线宽度等等,这些在 Embedded Systems 中很少使用,因为会增加 API 的复杂度和实现难度。 OpenGL ES 精简了一些功能的实现方式,对纹理压缩格式和着色器编译器的支持进行了限制和简化,以适应移动设备和嵌入式系统的硬件特性。 可以理解成是 OpenGL 的一个轻量版或者说是一个精简版。

EGL

EGL 全称 【Embedded System Graphics Library】,是一种用于管理 OpenGL ES 或其他的图形 API 与本地窗口系统(例如 X Window Manager、Wayland Compositor)之间交互的 API,EGL 是提供了标准的接口,用于不同平台和操作系统之间管理和创建图形表面,上下文和渲染目标,以及处理设备事件和窗口管理的功能。 通常的情况下,OpenGL ES 和 EGL 被结合使用在 Embedded 设备受限的环境中进行图形渲染,EGL 负责与窗口系统进行交互,并且管理着 OpenGL ES 表面、上下文,从而实现高效的图形渲染。 对此,EGL 是 OpenGL ES 嵌入式图形 API 的一个重要组成部分,能够使大家能够容易地将图形渲染功能与窗口系统集合,在受限环境中实现高效渲染。 所以 OpenGL 做到跨平台,很大程度上归功于 EGL。

为什么一直强调高效渲染?

因为在嵌入式设备中,通常硬件资源受限,图形渲染性能往往需要高效的实现才能够满足设备的性能和功能要求,以下是一些点:

  • 资源管理:EGL 负责管理 OpenGL ES 表面和上下文,可以控制他们的生命周期,从而减少不必要的内存消耗和资源占用,提供性能和稳定性。
  • 显示和交互维度:EGL 可以与窗口系统进行交互,处理设备事件和窗口管理的能力,在需要的时候进行更新内容。
  • 多重上下文:EGL 支持多个 OpenGL ES 上下文的共存,可以在不同的线程或进程中同时执行多个渲染任务,提高渲染效率和并行性。
  • 平台兼容性:EGL 提供了一个标准的接口,可以在不同平台和操作系统之间实现图形渲染功能,从而提高开发效率和跨平台性。

DRM

DRM 【Direct Rendering Manager】是一个内核模块,提供了对显卡的访问,使得用户空间的应用程序可以直接访问显卡并进行图形渲染,DRM 还提供了对其他功能,如显存管理和加速功能的控制等。

DRI

DRI (Direct Rendering Infrastructure)是用户空间的库和驱动程序,利用 DRM 提供的功能来实现图形渲染,DRI 提供了 OpenGL 等图形 API 的实现,使得应用程序可以通过 DRI 来直接访问显卡来进行渲染。

抽象层设计思想

在 mesa 中,由于是 C 语言编写,没有面向对象的概念,主要使用了结构体和函数指针等基本特性来实现抽象数据类型和多态性。 EGL 模块中的每个对象都是一个结构体,例如 EGLDisplay、EGLSurface、EGLContext 等。这些结构体包含了对象的属性和方法,例如对象句柄、引用计数等。 为了实现多态性,EGL 模块中使用了函数指针来实现虚函数的概念。例如,每个 EGLDisplay 对象都有一个指向 EGLDisplayOps 结构体的指针,该结构体包含了操作 EGLDisplay 对象的函数指针,例如 initialize、terminate 等。这样,在调用 EGLDisplay 对象的函数时,可以通过指针调用正确的操作函数,从而实现多态性和动态绑定。

以下是一个例子: dri2_egl_display_vtbl 结构体的定义(src/egl/drivers/dri2/egl_dri2.h):

struct dri2_egl_display_vtbl {
   /* mandatory on Wayland, unused otherwise */
   int (*authenticate)(_EGLDisplay *disp, uint32_t id);

   /* mandatory */
   _EGLSurface* (*create_window_surface)(_EGLDisplay *disp, _EGLConfig *config,
                                         void *native_window,
                                         const EGLint *attrib_list);

   /* optional */
   _EGLSurface* (*create_pixmap_surface)(_EGLDisplay *disp, _EGLConfig *config,
                                         void *native_pixmap,
                                         const EGLint *attrib_list);
   ......
   /* mandatory */
   EGLBoolean (*swap_buffers)(_EGLDisplay *disp, _EGLSurface *surf);

   ......
};

定义 dri2_egl_display_vtbl (src/egl/drivers/dri2/platform_wayland.c)

static const struct dri2_egl_display_vtbl dri2_wl_swrast_display_vtbl = {
   .authenticate = NULL,
   .create_window_surface = dri2_wl_create_window_surface,
   .create_pixmap_surface = dri2_wl_create_pixmap_surface,
   .destroy_surface = dri2_wl_destroy_surface,
   .create_image = dri2_create_image_khr,
   .swap_buffers = dri2_wl_swrast_swap_buffers,
   .get_msc_rate = dri2_wayland_get_msc_rate,
   .get_dri_drawable = dri2_surface_get_dri_drawable,
};

dri2_wl_swrast_swap_buffers 是一个静态函数,定义如下:

static EGLBoolean
dri2_wl_swrast_swap_buffers(_EGLDisplay *disp, _EGLSurface *draw)
{
   struct dri2_egl_display *dri2_dpy = dri2_egl_display(disp);
   struct dri2_egl_surface *dri2_surf = dri2_egl_surface(draw);

   if (!dri2_surf->wl_win)
      return _eglError(EGL_BAD_NATIVE_WINDOW, "dri2_swap_buffers");

   dri2_dpy->core->swapBuffers(dri2_surf->dri_drawable);
   return EGL_TRUE;
}

dri2_wl_swrast_display_vtbl 赋值,在 dri2_initialize_wayland_drm 时

   /* Fill vtbl last to prevent accidentally calling virtual function during
    * initialization.
    */
   dri2_dpy->vtbl = &dri2_wl_display_vtbl;

调用 swap_buffers(egl_dri2.c)

static EGLBoolean
dri2_swap_buffers(_EGLDisplay *disp, _EGLSurface *surf)
{
   struct dri2_egl_display *dri2_dpy = dri2_egl_display(disp);
   __DRIdrawable *dri_drawable = dri2_dpy->vtbl->get_dri_drawable(surf);
   _EGLContext *ctx = _eglGetCurrentContext();
   EGLBoolean ret;

   if (ctx && surf)
      dri2_surf_update_fence_fd(ctx, disp, surf);
   ret = dri2_dpy->vtbl->swap_buffers(disp, surf);

   /* SwapBuffers marks the end of the frame; reset the damage region for
    * use again next time.
    */
   if (ret && dri2_dpy->buffer_damage &&
       dri2_dpy->buffer_damage->set_damage_region)
      dri2_dpy->buffer_damage->set_damage_region(dri_drawable, 0, NULL);

   return ret;
}

API 入口点

src/egl/main/eglapi.c

平台抽象相关

(include/EGL/eglplatform.h) EGL 三剑客,分别是 EGLNativeDisplayType (EGL 原生显示类型)、EGLNativePixmapType (原生 Pixmap 类型)、EGLNativeWindowType (原生窗口类型),用于表示与 EGL 交互的不同对象。

  • EGLNativeDisplayType 表示 EGL 与底层显示系统之间的原生显示连接,例如 X11 或 Wayland
  • EGLNativePixmapType 表示一个底层图像对象,通常用于离屏渲染或抓取屏幕内容
  • EGLNativeWindowType 表示底层窗口对象,通常用于在窗口中进行渲染和显示 这三个类型在 EGL 中非常重要,因为它们提供了一个与窗口系统交互的标准化接口,使得 EGL 可以在不同的平台上工作,并与窗口系统无缝集成。在使用 EGL 进行图形开发时,我们通常需要使用这三个类型来表示与系统交互的不同对象。

三剑客定义:

/* EGL 1.2 types, renamed for consistency in EGL 1.3 */
typedef EGLNativeDisplayType NativeDisplayType;
typedef EGLNativePixmapType  NativePixmapType;
typedef EGLNativeWindowType  NativeWindowType;

X11

#elif defined(__unix__) || defined(USE_X11)

/* X11 (tentative)  */
#include <X11/Xlib.h>
#include <X11/Xutil.h>

typedef Display *EGLNativeDisplayType;
typedef Pixmap   EGLNativePixmapType;
typedef Window   EGLNativeWindowType;

Wayland

#elif defined(WL_EGL_PLATFORM)

typedef struct wl_display     *EGLNativeDisplayType;
typedef struct wl_egl_pixmap  *EGLNativePixmapType;    // 已被弃用
typedef struct wl_egl_window  *EGLNativeWindowType;

wl_egl_pixmap 弃用原因 Kristian Høgsberg (5):

  • gbm: Reject buffers that are not wl_drm buffers in gbm_bo_import()
  • gbm: Use the kms dumb ioctls for cursor instead of libkms
  • egl/wayland: Update to Wayland 0.99 API
  • wayland: Remove 0.85 compatibility #ifdefs
  • wayland: Drop support for ill-defined, unused wl_egl_pixmap https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/docs/relnotes/9.0.1.rst 提交详细:https://gitlab.freedesktop.org/mesa/mesa/-/commit/e20a0f14b5fdbff9afa5d0d6ee35de8728f6a200

GBM(Generic Buffer Manager)

#elif defined(__GBM__)

typedef struct gbm_device  *EGLNativeDisplayType;
typedef struct gbm_bo      *EGLNativePixmapType;
typedef void               *EGLNativeWindowType;

Wayland

wl_display

wl_display 是一个重要的核心对象,代表了 Wayland 服务器的连接点,充当了 Client 和 Server 之间的通信接口。 wl_display主要有以下几个功能:

  1. 管理连接:wl_display 用于管理 Client 与 Wayland Compositor 的连接,包括连接的建立、维护和关闭等操作
  2. 分配对象:Wayland 协议中定义了各种对象(如 wl_surface、wl_seat 等),Client 需要通过 wl_display 向 Wayland Compositor 请求分配这些对象
  3. 接收事件:wl_display也负责接收服务器发送给客户端的事件,如wl_surface的状态改变、输入设备的状态改变等事件
  4. 多线程支持:wl_display 支持多线程访问,多个线程可以同时与 Compositor 进行通信,从而提高了客户端的并发性能

wl_egl_window

wl_egl_window (wayland库中的 egl/wayland-egl-backend.h)

struct wl_egl_window {
    const intptr_t version;

    int width;
    int height;
    int dx;
    int dy;

    int attached_width;
    int attached_height;

    void *driver_private;
    void (*resize_callback)(struct wl_egl_window *, void *);
    void (*destroy_window_callback)(void *);

    struct wl_surface *surface;
};

Debug

#0  0x00007fffe2fb2d30 in eglSwapBuffers () from /lib/x86_64-linux-gnu/libEGL.so.1
#1  0x00007ffff003b217 in QtWaylandClient::QWaylandGLContext::swapBuffers (this=0x5555557f9620, surface=0x5555555a1c70)
    at /home/user/ocean/qtwayland/src/hardwareintegration/client/wayland-egl/qwaylandglcontext.cpp:518
#2  0x00007ffff069ca3e in QSGRenderThread::syncAndRender (this=0x5555557a5630, grabImage=0x0)
    at /home/user/ocean/qtdeclarative/src/quick/scenegraph/qsgthreadedrenderloop.cpp:870
#3  0x00007ffff069d6d7 in QSGRenderThread::run (this=0x5555557a5630) at /home/user/ocean/qtdeclarative/src/quick/scenegraph/qsgthreadedrenderloop.cpp:1043
#4  0x00007ffff6e35271 in QThreadPrivate::start(void*) () from /lib/x86_64-linux-gnu/libQt5Core.so.5
#5  0x00007ffff6758ea7 in start_thread (arg=<optimized out>) at pthread_create.c:477
#6  0x00007ffff69ecdef in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95

wl_drm

wl_drm 是 mesa 制定的扩展协议 Chromium 对于 ozone/wayland 的支持是增加了 wl_drm 协议,主要是考虑到并不是所有 Wayland Compositor 都对 zwp_linux_dmabuf 的支持,exo 是支持的 (components / exo / wayland / zwp_linux_dmabuf.cc) https://chromium.googlesource.com/chromium/src.git/+/cd3bebdd6b95d6be0393df4017cf7d4c2f251e2b

初始化

@startuml

qtwayland -> EGL: 1.eglInitialize()
EGL -> EGL: dri2_initialize()
EGL -> EGL: dri2_initialize_wayland()
EGL -> EGL: dri2_initialize_wayland_drm()
@enduml
  1. eglInitialize() 对传过来的 EGLDisplay 进行识别,然后对全局 _eglDriver->Initialize() 进行初始化
  2. dri2_initialize() dri2_initialize 是 egl_dri2 模块的初始化函数,会将 EGLDisplay 指针通过 dri2_egl_display() 转换成 dri2_egl_display 结构体,接下来判断这个结构体的 Platform 枚举类型(表示当前该客户端运行的平台),常用的有以下几种类型:
  • _EGL_PLATFORM_X11
  • _EGL_PLATFORM_WAYLAND
  • _EGL_PLATFORM_ANDROID (Android 并非都是用 Mesa) 不同的平台会调用不同的函数,例如:
  • X11 下调用 dri2_initialize_x11()
  • Wayland 下调用 dri2_initialize_wayland()
  1. dri2_initialize_wayland() 主要做的一件事就是判断是否需要强制使用软件渲染,如果使用强制软渲染则会接下来调用 dri2_initialize_wayland_swrast(),否则会调用 dri2_initialize_wayland_drm(),这个是否开启软渲染是受 LIBGL_ALWAYS_SOFTWARE 环境变量控制。
  2. dri2_initalize_wayland_drm() 初始化 Wayland 环境:
  • 与 Server 端进行连接,调用 wl_display_connect() 得到 wl_display
  • wl_display_create_queue() 创建消息队列
  • wl_proxy_create_wrapper() 创建代理包装以使用队列分配线程安全
  • 通过 wl_proxy_set_queue() 将代理分配给队列,queue 和 wrapper 相绑定
  • wl_display_get_registry
  • wl_registry_add_listener
  • loader_get_user_preferred_fd
  • _eglAddDevice
  • drmGetNodeTypeFromFd
  • loader_get_driver_for_fd
  • dri2_load_driver
  • dri2_create_screen
  • dri2_setup_extensions
  • dri2_setup_screen
  • dri2_wl_setup_swap_interval

eglMakeCurrent

TODO

eglSwapBuffers

其作用是将绘制的图像交换到屏幕上显示

  1. 通过 _eglGetCurrentContext() 获取当前的 EGL 上下文 _EGLContext
  2. 检查 Display,通过 _eglLockDisplay() 锁定 _EGLDisplay。这个函数将检查传入的 Display 是否合法,如果合法则返回一个指向 _EGLDisplay 的指针,否则返回 NULL
  3. 查找 Surface,通过 _eglLookupSurface() 查找 _EGLSurface
  4. 检查 Surface 是否合法,通过 _EGL_CHECK_SURFACE 宏检查表面是否合法。如果表面不合法,则返回 EGL_FALSE
  5. 检查 Surface 是否绑定到 CurrentContext,如果没有绑定或者不存在,则返回 EGL_BAD_SURFACE 错误
  6. 检查 Suface 类型是否为 EGL_WINDOW_BIT,如果不是直接返回 EGL_TRUE
  7. 根据 EGL 1.5 spec,检查 Lost 属性值,为真返回 EGL_BAD_NATIVE_WINDOW 错误
  8. 调用驱动程序的 SwapBuffers() 将绘制的图像交换到屏幕上
  9. 重置损坏区域和缓存
   /* EGL_KHR_partial_update
    * Frame boundary successfully reached,
    * reset damage region and reset BufferAgeRead
    */
   if (ret) {
      surf->SetDamageRegionCalled = EGL_FALSE;
      surf->BufferAgeRead = EGL_FALSE;
   }
@startuml
!theme cerulean-outline

Client -> eglapi: eglSwapBuffers
eglapi -> egl_dri2: dri2_swap_buffers
egl_dri2 -> platform_wayland: swap_buffers

@enduml

Qt 是如何加载 EGL 并通过 EGL 与窗口系统交互

客户端创建了 wl_display,强转成 EGLDisplay *,然后调用 eglInitialize() 进行初始化 代码位置: src/hardwareintegration/client/wayland-egl/ QWaylandEglClientBufferIntegration::initialize()

qtwayland 中对 egl 窗口交互的类 QWaylandEglWindow,也就是说每一个 QWindow 在底层的抽象实现是 QWaylandEglWindow,每一个 QWaylandEglWindow 都是直接对 wl_egl_window 和 eglSurface 进行创建和销毁,相关 EGL API :

  • wl_egl_window_create()
  • wl_egl_window_destroy()
  • eglCreateWindowSurface()
  • eglDestroySurface()

QWaylandGLContext 两大 API: eglMakeCurrent() eglSwapInterval() eglSwapBuffers()

规范

https://registry.khronos.org/EGL/specs/