Graphics Stack总结(二) Mesa漫游
回顾
前一篇文章中我们对Linux graphics stack有了一个快速介绍,接下来我将解释为什么我们称之为graphics driver in Linux实际上是三个不同drivers的组合:
- the user space X server DDX driver, which handles 2D graphics.
- the user space 3D OpenGL driver, that can be provided by Mesa.
- the kernel space DRM driver.
现在我们知道Mesa在哪个位置了,而我们接下来对它会进行详细了解。
DRI drivers and non-DRI drivers
正如解释的那样,Mesa 通过提供OpenGL API的实现来handles(处理) 3D graphics。Mesa OpenGL drivers实际上也通常被叫做DRI drievrs。请记住这一点,毕竟DRI架构生来就是被精确的enable OpenGL drivers在Linux中的高效实现的,正如前文介绍的那样,DRI/DRM在Mesa中是OpenGL drivers的building blocks。
这里也会有OpenGL API的其他实现可以获得。Hardware供应商会提供Linux drivers,这些Linux drivers是他们自己对于OpenGL API的实现,通常是以一些binary的形式。举个例子,如果你有NVDIA的GPU并且安装了NVDIA的专有driver,那么在其中也会包含NVDIA自己的libGL.so
注意,在Linux中创建不遵循DRI架构的graphics drivers也是可以的。举个例子,NVDIA的专有driver安装了一个kernel module,其实现了DRM的相似功能,但是它的API是与DRM不同的,是由NVDIA自己设计的。所以显然,相对应的user space的drivers(DDX 和 OpenGL)也会用NVDIA自行设计的API与NVDIA kernel space driver通信,而不是DRM的API。
Mesa, the framework
你可能已经注意到了,当我谈论到Mesa的时候我通常说"drivers",复数。这是因为Mesa本身并不是一个单个driver,而是一个project,这个Project包含了多个drivers(这是因为Mesa project有好几种OpenGL API的实现)。
事实上,Mesa作为framework来说是OpenGL的最佳的实现,它提供了很好的抽象概念/抽象层,使得可以被多种driver共用。显然,有关OpenGL的多个层面的实现是独立的,与底层硬件无关的,因此这些部分可以被抽象和复用。
举个例子,如果你对OpenGL比较熟悉的话,你会知道OpenGL是基于API提供state的(状态机是OpenGL中的重要概念),这意思着很多API调用不是立即生效的,他们只会在driver中修改某些变量的值,而不会立即把这些值push到硬件中。事实上,通常当我们通过调用glDrawArrays() or a similar API来真正渲染(render)一些东西的时候才会生效:在那个时刻driver将会根据之前API调用设置的state(状态)来配置3D pipeline,然后来执行渲染。由于这些APIs不会与硬件交互,所以这些APIs的实现可以被多种drivers来共用,接着,每个driver中glDrawArrays()的implementation可以获得状态机中存储的值,然后立即将这些值转换为硬件中的执行代码。
这样来说,Mesa为很多东西提供了抽象,甚至完成了很多个不需要与硬件交互的OpenGL APIs的实现,至少是不必立即产生交互的。
Mesa也定义了多个hooks,这些hooks是drivers中可能需要做硬件相关的工作,比如the implementation of glDrawArrays().
Looking into glDrawArrays()
让我们举一个关于这些hooks的例子,通过检测Mesa中glDrawArrays() 产生的stacktrace来进入硬件。在这个例子中,我会在程序中用Mesa Intel DRI driver并且会在function render()调用glDrawArrays() ,以下是stacktrace中相关的部分:
brw_upload_state () at brw_state_upload.c:651 brw_try_draw_prims () at brw_draw.c:483 brw_draw_prims () at brw_draw.c:578 vbo_draw_arrays () at vbo/vbo_exec_array.c:667 vbo_exec_DrawArrays () at vbo/vbo_exec_array.c:819 render () at main.cpp:363
注意我们前面提到的glDrawArrays() 在代码中实际上是vbo_exec_DrawArrays().。关于这个stack中有意思的部分是vbo_exec_DrawArrays()和vbo_draw_arrays()是不依赖硬件(hardware independent)的,从而被mesa中的很多drivers复用。如果你不像我一样用的是Intel的GPU,但同样用的是Mesa,你的backtrace也应该是相似的。这些通用的functions通常会做检查API use error,reformatting inputs等事情,从而使后续的处理或者为当前状态机(current state)获取额外的信息更方便,这些都是在hardware中实现真正的操作所需要的。
Notice that glDrawArrays() is actually vbo_exec_DrawArrays(). What is interesting about this stack is that vbo_exec_DrawArrays() and vbo_draw_arrays() are hardware independent and reused by many drivers inside Mesa. If you don’t have an Intel GPU like me, but also use a Mesa, your backtrace should be similar. These generic functions would usually do things like checks for API use errors, reformatting inputs in a way that is more appropriate for later processing or fetching additional information from the current state that will be needed to implement the actual operation in the hardware.
在某些时刻,然而我们需要去做实际的渲染,这涉及到根据我们发出的命令和在先前 API 调用中设置的相关状态,来配置hardware pipeline。在上面的stracktrace中,这从函数 brw_draw_prims()开始。这个函数调用是Intel DRI driver的一部分,是一个Hook,在这个Hook中Intel driver会做一些准备工作,用来执行配置Intel GPU进行绘制。正如你所见,后面会调用类似 brw_upload_state()的函数,其会upload一堆的状态给hardware去做完成此任务,比如根据当前program的需求配置各种shader的stage。
Registering driver hooks
在后续文章中我们将会用更多细节讨论driver是如何配置pipeline的,但此刻我们仅会探讨Intel driver是如何为glDrawArrays() 的调用注册它的hook的。如果我们观察stackstrace,知道brw_draw_prims() 是Intel driver中的hook,我们可以看到它是如何在vbo_draw_arrays()中被调用的:
static void vbo_draw_arrays(struct gl_context *ctx, GLenum mode, GLint start, GLsizei count, GLuint numInstances, GLuint baseInstance) { struct vbo_context *vbo = vbo_context(ctx); (...) vbo->draw_prims(ctx, prim, 1, NULL, GL_TRUE, start, start + count - 1, NULL, NULL); (...) }
所以hook是vbo_context中的draw_prims() 。在源代码中做一些深入搜索,我们可以看到hook是在brw_draw_init()中被setup的,如下所示:
void brw_draw_init( struct brw_context *brw ) { struct vbo_context *vbo = vbo_context(ctx); (...) /* Register our drawing function: */ vbo->draw_prims = brw_draw_prims; (...) }
让我们设置一个断点,看Mesa是什么时候调用到那里的:
brw_draw_init () at brw_draw.c:583 brwCreateContext () at brw_context.c:767 driCreateContextAttribs () at dri_util.c:435 dri2_create_context_attribs () at dri2_glx.c:318 glXCreateContextAttribsARB () at create_context.c:78 setupOpenGLContext () at main.cpp:411 init () at main.cpp:419 main () at main.cpp:477
所以当我们setup OpenGL context时,Mesa毫无意外的调用到Intel DRI driver中,此时driver会注册各种各样的hooks,包含绘制图形的hook.
我们可以做相似的事情,来观察driver时如何注册context creation的hook的。我们可以看到Intel driver(Mesa中其他vendor的driver也一样)给需要的hook assign了全局变量,如下所示
static const struct __DriverAPIRec brw_driver_api = { .InitScreen = intelInitScreen2, .DestroyScreen = intelDestroyScreen, .CreateContext = brwCreateContext, .DestroyContext = intelDestroyContext, .CreateBuffer = intelCreateBuffer, .DestroyBuffer = intelDestroyBuffer, .MakeCurrent = intelMakeCurrent, .UnbindContext = intelUnbindContext, .AllocateBuffer = intelAllocateBuffer, .ReleaseBuffer = intelReleaseBuffer }; PUBLIC const __DRIextension **__driDriverGetExtensions_i965(void) { globalDriverAPI = &brw_driver_api; return brw_driver_extensions; }
在Mesa里的DRI的实现中,这些全局变量始终被使用,根据需求来调用到不同的hardware driver。
我们可以看到有两种不同类型的hook,一种是把driver给link到DRI的实现(implementation)中所需的Hook(这是Mesa中driver的主入口),另一种是为OpenGL的硬件实现的相关任务添加的Hook,通常由driver在上下文创建(context creation)时注册。
要写一个新的DRI driver, 开发者只写这些Hook的实现就可以了,其他剩余的部分已经被Mesa实现完成,并且可以在不同的driver中复用。
Gallium3D, a framework inside a framework
如今,我们可以把Mesa DRI 驱动分成两类:传统(classic)drivers(非基于Gallium3D framework)和Gallium drivers。
Gallium3D 是Mesa的一部分,试图让3D driver的开发比之前更加简单和实用。举个例子,classic Mesa drivers是与OpenGL紧密耦合的,这意味着要实现对其他APIs(比如Direct3D)的支持,工作量跟写一个新的完整driver/implementation差不多。Gallium 3D framework通过提供一个API解决了这个问题,这个API暴露了现代GPUs的hardware function,而不是专注于OpenGL这样的某个特定API。
Gallium的其他好处包括,通过分离driver中依赖底层操作系统中的某些特定方面的部分,来实现对不同操作系统的支持。
在过去的这些年里,我们已经看到很多drivers转到了Gallium基础设施架构中,包括 (the open source driver for NVIDIA GPUs),和不同种类的radeon drivers,以及一些software drivers (swrast, llvmpipe) .
Gallium3D driver model (image via wikipedia)
尽管在过去有人试图把Intel driver port到Gallium中并且付出一些努力,但是据我所知,Intel Gallium drivers(i915g和i965g)的开发已经停滞不前。Intel正专注于driver的传统版本。这可能是因为在保证同样的feature和稳定性下,把现在的classic diver port到Gallium需要大量的时间和effort,这是由于当前的classic driver包含了太多代的Intel GPU。而且现在为了增加对新的OpenGL feature的支持,同样需要大量的工作要做,这有着更高的优先级。
Gallium and LLVM
正如我们在未来的文章中将会看到的更多细节,写一个现代GPU driver会包含许多native code generation和优化。并且,OpenGL包含OpenGL Shading Language (GLSL),其直接要求在driver中提供GLSL编译器。
毫无疑问,对Mesa开发者而言,明智的想法是重用现有的编译器基础设施,而不是构建和用他们自己的: enter LLVM.
It is no wonder then that Mesa developers thought that it would make sense to reuse existing compiler infrastructure rather than building and using their own: enter LLVM.
通过将LLVM引入mix,Mesa开发者希望为着色器带来新的更好的优化,并生成更好的本地代码(native code),这对性能至关重要。
这也可以消除Mesa和/或驱动程序中的大量代码。事实上,Mesa有自己完整的GLSL编译器实现,包括GLSL解析器、编译器和链接器,以及许多优化,包括Mesa中代码的抽象表示,以及实际硬件驱动程序中特定(specific)GPU的实际本机代码(actual native code)。
Gallium插入LLVM的方式很简单:Mesa解析GLSL并生成着色器代码的LLVM中间表示,然后将其传递给LLVM,LLVM将负责优化。在此场景中,硬件驱动程序的作用仅限于提供描述各自GPU(指令集、寄存器、约束等)的LLVM后端,以便LLVM知道如何为目标GPU工作。
Hardware and Software drivers
即使在今天,我也看到一些人相信Mesa只是OpenGL的一个软件实现(software implementation)。如果你已经阅读了这篇文章,应该很清楚这不是真的:Mesa提供了OpenGL的多个实现(驱动程序),其中大多数都是硬件加速驱动程序,但Mesa也提供了软件驱动程序。
Software drivers 有时非常有用:
- 出于开发和测试的目的,特别是当你想排除掉硬件因素时。从这个角度来看,software representation可以为不受任何特定硬件约束或约束的预期行为提供参考。例如,如果您有一个OpenGL程序不能正常运行,我们可以使用软件驱动程序运行它:如果它运行良好,那么我们就知道问题在硬件驱动程序中,否则我们可以怀疑问题在应用程序本身。
- 允许在缺少3D硬件驱动程序的系统中执行OpenGL。这显然会很慢,但在某些情况下,这可能就足够了,而且肯定比没有任何3D支持要好。
我最初打算在这篇文章中涵盖更多内容,但它已经足够长了,所以我们现在就到此为止。在下一篇文章中,我们将讨论如何check and change Mesa使用的driver,例如在software和hardware driver之间切换,然后我们将开始研究Mesa的源代码并介绍其主要模块。