JJ's blog

libglvnd 实现分析

在以前的 Linux 环境下, OpenGL 库的加载存在一个问题:多个厂商的实现没法很好地共存。具体来说,应用会用 libGL.so 这个 SONAME 去加载 OpenGL 实现,但是动态加载器的搜索路径通常是固定的(当然可以通过修改环境变量之类的方式去改变哪个 libGL.so 被加载,但是这样的方式是不现实的),因此以前没法很好地让多个厂商的实现并存,无法让多个进程选择不同的实现,甚至动态地去决定用哪个厂商的实现。
为此,现代 Linux 下的 libGL.so 的实现通常是由 libglvnd (The GL Vendor-Neutral Dispatch library) 实现的。这个实现允许多个厂商的 OpenGL 实现同时存在于机器上,允许多个进程动态地选择具体的实现。
它通过实现一个中心化的 libGL.so + 不同厂商提供的 libGLX_{vendor}.so, 根据 context 转发到对应厂商提供的 GL/GLX 实现,从而解决上述提到的问题。

┌───────────────────────────┐
│        Application        │
└─────┬────────────────────┬┘
      │                    │
      │                    │
 glXGetProcAddress         glxxx return from glXGetProcAddress
┌─────▾────────────┐     ┌─▾────────────────┐
│      libGL       ├─────▸  libGLdispatch   │
└─────┬─────────┬──┘     └─┬────────────────┘
      │         gl functions   ┌────────────────────┐
      │         └──────────┴───▸ libGLX_{vendor}.so │
      glXxxx(Display*, ...)    └─▴──────────────────┘
┌─────▾────────────┐             glX extension function
│      libGLX      ├─────────────┘ ┌────────┐
│                  ├───────────────▸ libX11 │
└──────────────────┘               └────────┘

整体架构

各库职责

  • libGL.so / libOpenGL.so 等等:和应用的交互层,将 GLX 调用转发到 libGLX,将 GL 调用转发到 libGLdispatch。
  • libGLX.so:负责 GLX 协议厂商无关部分的处理、vendor 的加载与分发调度。
  • libGLdispatch.so:负责 GL 调用的分发,通常是基于 TLS 的方式。
  • libGLX_{vendor}.so:vendor 的具体 OpenGL 实现。

Vendor 管理

Vendor 加载

  • so file name: libGLX_{vendorName}.so.0
  • export main func: __glx_Main
    用来把 libGLX 的 export function 传给 vendor, 并把 vendor 的 export function 传回给 libGLX

核心数据结构

__glXDisplayInfoHash (uthash node)
 ├─ info : __GLXdisplayInfo
 │   ├─ dpy : Display* (key)
 │   ├─ clientStrings : char*[3]         // cache str for `glXGetClientString`
 │   ├─ vendors : __GLXvendorInfo**      // 每个 screen 对应的一个支持该 screen 的 vendor
 │   ├─ vendorLock : glvnd_rwlock_t       // 保护 vendors
 │   ├─ xidVendorHash : lkdhash           // XID -> vendor 映射表
 │   │     ( XID 都是 GLXDrawable 包括 GLXWindow (`glXCreateWindow`) GLXPixmap (`glXCreateGLXPixmap` / `glXCreatePixmap`) GLXPbuffer (`glXCreatePbuffer`))
 │   ├─ glxSupported : Bool               // X Server 是否支持 GLX 扩展
 │   ├─ glxMajorOpcode : int              // GLX ext
 │   ├─ glxFirstError : int               // GLX ext
 │   └─ libglvndExtensionSupported : Bool // GLX 扩展是否支持 GLX_EXT_libglvnd
 ├─ inTeardown : Bool                     // Display 关闭中标记
 └─ extCodes : XExtCodes*                 // `XAddExtension` 返回,通过一个 dummy ext 来监听 `XCloseDisplay` 从而执行清理工作
__glXVendorNameHash (uthash node)
 ├─ vendor : __GLXvendorInfo
 │   ├─ vendorID (incremental ID 1+)              // managed by libGLdispatch
 │   ├─ name (char*) (key)
 │   ├─ dlhandle (dlopen)
 │   ├─ dynDispatch (winsys vendor table)   // GLX 动态表, managed in libGLX
 │   │     (哈希: dispatch index -> 本 vendor 的实现地址)
 │   ├─ glDispatch (__GLdispatchTable)      // OpenGL 全局表, managed & allocated in libGLdispatch
 │   │   ├─ currentThreads
 │   │   ├─ stubsPopulated                  // # of func addr added into table
 │   │   ├─ getProcAddress                  // libGLX!VendorGetProcAddressCallback
 │   │   ├─ getProcAddressParam             // &vendor
 │   │   └─ table                           // func_addr[], the value stored in TLS
 │   ├─ glxvc -> &imports
 │   ├─ patchCallbacks (opt, points to &outer patchCallbacks if supported)
 │   └─ staticDispatch (GLX 静态入口指针集,从 vendor's getProcAddress 拿到的函数)
 ├─ imports : __GLXapiImports (vendor!__glx_Main 填充的回调函数表)
 └─ patchCallbacks : __GLdispatchPatchCallbacks (若支持 patch, subset of &imports)
__GLXcontextInfoRec
 ├─ context : __GLXcontextRec * (key) (vendor created, opaque structure)
 ├─ vendor : __GLXvendorInfo *
 ├─ currentCount : int                      // init 0, inc by `glXMakeCurrent`
 └─ deleted : Bool

TLS 与线程状态

TLS in libGLdispatch, 这里是 GLX 的情况,还有 EGL 的情况,此处不再列举。

__GLXThreadStateRec
 ├─ glas : __GLdispatchThreadState
 │   ├─ tag : int                               // GLX / EGL
 │   ├─ threadDestroyedCallback
 │   └─ priv : __GLdispatchThreadStatePrivateRec
 │       ├─ threadState : __GLdispatchThreadState // &glas
 │       ├─ vendorID : int                      // &vendor->vendorID
 │       └─ dispatch : __GLdispatchTable        // &vendor->glDispatch
 ├─ currentVendor : __GLXvendorInfo *
 ├─ currentDisplay : Display *
 ├─ currentDraw : GLXDrawable
 ├─ currentRead : GLXDrawable
 └─ currentContext : __GLXcontextInfoRec *

函数分发机制

GLX 函数分发

一部分 glX function 是 vendor-neutral 的,直接由 libGLX 实现:

  • glXQueryExtension
  • glXQueryVersion
  • glXGetProcAddress
  • glXGetProcAddressARB
  • glXGetCurrentContext
  • glXGetCurrentDrawable
  • glXGetCurrentReadDrawable
  • glXGetCurrentDisplay

其它 glX function 是 vendor 相关的,libGLX 会分发 + 可能的合并逻辑(比如是 Display 相关,多个 Screen 可能是不同 vendor)。
vendor 查找逻辑主要包括下面几个为 key 的哈希表:

  • Screen
  • GLXFBConfig
  • GLXDrawable
  • GLXContext

分发方式主要包括:

  • 一部分 glX function 是 vendor 实现的 "静态的" dispatch (load vendor lib 的时候 query 得到的)。两步分发,第一从 Screen 找到 vendor, 然后直接调用 vendor 对应的实现,见 LookupVendorEntrypoints.
  • 另外的 glX extension function, 除了几个特定的 extension 函数 glXGetProcAddressARB, glXImportContextEXT, glXFreeContextEXT, glXCreateContextAttribsARB 因为涉及到 libGLX 的内容,和前一部分一样由 libGLX 提供外,其他的 extension 函数,会去查看是否有任意 vendor 提供了该 extension,是的话会直接转发给该 vendor. 即使有多个 vendor 也只会使用第一个。如果某个 extension 当前没有任意已加载 vendor 提供,会生成一个临时的跳板,指向一个 no-op function。等到后续加载 vendor 的时候,会再去尝试匹配,如果成功,则跳转到 vendor 的实现。

代码分析:

  • 由 libGLX 实现的 glX 代码可以直接 export, LOCAL_GLX_DISPATCH_FUNCTIONS.
  • 而 glX extension functions 不需要 libGLX 参与(除了几个特殊的),只需要记录一下其地址就行,这就是 dispatchIndexList
  • 但是有一个需求是在 vendor lib 没有加载之前就要提供这些 extension function 的地址,为了解决这个问题 glx_entrypoint_start 被引入提供一个临时跳板。

源码实现划分:

libglx.c - Public API 接入层

  • 导出所有 PUBLIC 的 GLX 函数符号
  • 应用程序直接调用的接口
  • 上下文生命周期管理
  • 线程状态管理

libglxmapping.c - Vendor 分发调度层

  • 管理 vendor 库的加载 (dlopen)
  • 维护 XID → vendor 的映射表
  • 与 libGLdispatch 交互
  • 动态生成 dispatch stubs

libglxproto.c - X11 协议通信层

  • 直接使用 Xlib 发送/接收 X11 请求
  • 封装底层的 GLX 协议通信
  • 查询 X server 端信息

GLX 函数在 libGL 中的转发

libGL.so 也提供了不用 glXGetProcAddress 获取的大部分 GLX function 的 ELF export,其中有一部分是上面提到的静态函数,这部分 libGLX.so export 了一个函数指针数组 __GLXGL_CORE_FUNCTIONS, libGL 直接转发到对应的函数指针。
这些函数包括 GLX 本体的 <= 1.4 version 的函数,去掉 1.2 的 glXGetCurrentDisplay(疑似是一个 bug) ,加上了 "GLX_ARB_get_proc_address" extension 定义的 glXGetProcAddressARB.

call __GLXGL_CORE_FUNCTIONS@libGLX + offset

然后由于历史原因, libGL 也 export 了许多 GLX 的 extension 函数,这部分是通过类似于替用户 query 然后 cache 的方式转发:

if (libGL!cache_ptr != 0) call cache_ptr
else {
  cache_ptr = libGLX!__glXGLLoadGLXFunction(name)
  if (cache_ptr) call cache_ptr
  else return default
}

GL 函数分发

同样 libGL.so 提供了大部分 GL function 的 ELF export, 其代码类似于:

libGL.so:
section wtext: ; libglvnd custom section
func_ptr *dispatch_table = *(func_ptr**)(tls_base + tls_offset(_glapi_tls_Current));
jmp dispatch_table[slot] -> libGLX_vendor.so

dispatch_table 中的值是 &vendor->glDispatch->table, 调用 vendor library 和 libGLX 之前的 API 中的 getProcAddress 函数拿到的地址。

但是 glXGetProcAddress 获取的地址和 export 的地址不一样,其区别是,query 得到的地址是 libGLdispatch.so 的 wtext section。而 libGL.so export 的是自己 libGL.so 的 wtext section,尽管两者的机器码是一样的。

GL Extension 函数

和 glX extension 也是类似的,dynamic_stub_names 是用来维护已出现的 extension function 的名字。
不同的是两点:

  • gl 的函数都是和一个 context 绑定的,没有 context 就是 noop 的实现。而 context 是和 vendor 对应的,也就是每个 vendor 一份 dispatch table vendor->glDispatch.
  • 因为上一点 dispatch 是和 vendor 定义的,就不存在前面 glX 中的 vendor lazy load 的问题,所以没有临时的跳板,遇到一个新的 extension (遇到的定义是 user query address) 就会加载这个函数,没有就是用默认的 noop 实现。
    这里有一个点是,因为返回给 user 的是 TLS 索引的跳板(在没有 vendor assembly patch 的情况下)。所以所有已经创建 context 的 vendor 都需要去获取这个地址,更新其 dispatch table, 而不仅仅是当前调用的线程就行。

和 GLX extension 函数不同,大部分 GL extension 函数是厂商特定,libGL 没有提供 ELF export, 只能通过 glXGetProcAddress 获得 libGLdispatch 中的跳板地址。

性能优化

Stub Patching 机制

每个 wrapper library + libGLdispatch 都会有一份 stubPatchCallbacks 负责修改自己的代码段中的 stub.
然后这些都会把自身的 callback 注册到 libGLdispatch (__glDispatchRegisterStubCallbacks, 只有一份代码)。从而 libGLdispatch 知道有几个地方需要 patch.
之后 vendor 可以选择去修改 entrypoint 的机器码从而提高效率。即直接改写 libGL.so & libGLdispatch.so 的 wtext section 中的机器码。
不过 libglvnd 不支持多线程不同 vendor 的 context,除非 vendor 检查到这种情况主动不 patch 或者用环境变量 disable.

多线程锁优化

libGLdispatch.so 通过使用 dlsym(dlopen(NULL), "pthread_xxx") 的方式来判断 exe 是否引用了 pthread 来动态的决定是否使用锁来保证多线程安全,从而优化单线程程序性能。