深度探索Linux操作系统 —— Linux图形原理探讨2:https://developer.aliyun.com/article/1598096
2、渲染Pipleline
与 2D 渲染相比,3D 渲染要复杂得多。就如同有些复杂的绘画过程,要分成几个阶段一样,OpenGL 标准也将 3D 的渲染过程划分为一些阶段,并将由这些阶段组成的这一过程形象地称为 Pipleline 。
应用程序建立基本的模型包括在对象坐标中的顶点数据、顶点的各种属性(比如颜色),以及如何连接这些顶点(如是连接成直线还是连接为三角形),等等,统一存储在顶点缓冲中,然后作为 Pipeline 的输入,这些输入就像原材料一样,经过 Pipeline 这台机器的加工,最终生成像素阵列,输出到后缓冲的 BO 中。
OpenGL 的标准规定了一个参考的 Pipeline,但是各家 GPU 的实现与这个参考还是有很多差别的,有的 GPU 将相应的阶段合并,有的 GPU 将个别阶段又拆分了,有的可能增加了一些阶段,有的又砍了一些阶段。但是,大体上整个过程如图 8-7 所示。
(1)顶点处理
OpenGL 使用顶点的集合来定义或逼近对象,应用程序建模实际上就是组织这些顶点,当然也包括顶点的属性。Pipeline 的第一个阶段就是顶点处理(vertex operations),顶点处理单元将几何对象的顶点从对象坐标系变换到视点坐标系,也就是将三维空间的坐标投影到二维坐标,并为每个顶点赋颜色值,并进行光照处理等。
(2)图元装配
显然,很多操作处理是不能以顶点单独进行处理的,比如裁减、光栅化等,需要将顶点组装成几何图形。Pipeline 将处理过的顶点连接成为一些最基本的图元,包括点、线和三角形等。这个过程成为图元装配( primitive assembly )。
任何一个曲面都是多个平面无限逼近的,而最基本的是三点表示一个平面。所以,理论上,GPU 将曲面都划分为若干个三角形,也就是使用三角形进行装配。但是也不排除现代 GPU 的设计者们使用其他的更有效的图元,比如梯形,进行装配。
(3)光栅化
我们前文提到,图形是使用像素阵列来表示的。所以,图元最终要转化为像素阵列,这个过程称为光栅化(rasterization),我们可以把光栅理解为像素的阵列。经过光栅化之后,图元被分解为一些片断(fragment),每个片段对应一个像素,其中有位置值(像素位置)、颜色、纹理坐标和深度等属性。
(4)片段处理
在 Pipeline 更新帧缓冲之前,Pipeline 执行最后一系列的针对每个片段的操作。对于每一个片断,首先进行相关的测试,比如深度测试、模板测试。以深度测试为例,只有当片段的深度值小于深度缓存中与片段相对应的像素的深度值时,颜色缓冲、深度缓冲中的与片段相对应的像素的值才会被这个片段中对应的信息更新。
Pipeline 可全部由软件实现(CPU),也可全部由硬件实现(GPU),或者二者混合,这完全取决于 GPU 的能力。对于 GPU 没有 3D 计算能力的,则 Pipeline 完全由软件实现。比如,Mesa 中的 _tnl_default_pipeline,即是一个纯软件的 Pipeline,Pipeline 中的每一个阶段均由 CPU 负责渲染:
对于 3D 计算能力比较强的 GPU,如 ATI 的 GPU,Pipeline 完全由 GPU 实现。
而有些 GPU 能力不那么强大,那么 CPU 就要参与图形渲染了,因此,Pipeline 一部分由 CPU 实现,一部分由 GPU 实现。
以 Intel GPU 为例,Pipeline 的渲染过程大致如图 8-8 所示。
1)首先,应用程序通过 glVertex 等 OpenGL API 将数据写入用户空间的顶点缓冲。
2)当程序显示调用 glFlush,或者,当顶点缓冲满时,其将自动激活 glFlush,glFlush 将启动 Pipeline。以 intel_pipeline 为例,Pipeline 的前几个阶段是 CPU 负责的,因此,所有的输入来自用户空间的顶点缓冲,计算结果也输出到用户空间的顶点缓冲;在最后的 _intel_render_stage 阶段,按照 intel GPU 的要求,从公共的顶点缓冲中读取数据,使用 intel GPU 的 3D 驱动中提供的函数,重新组织一个符合 intel GPU 规范的顶点缓冲。
3)glFlush 调用 3D 驱动中的函数 intel_glFlush 。intel_glFlush 首先将顶点缓冲和批量缓冲复制到内核空间对应的 BO,实际上就是相当于复制到了 GPU 的显存空间,这样 GPU 就可以访问了。然后,内核的 DRM 模块将按照 Intel GPU 的要求建立一个环形缓冲区(ring buffer)。
4)准备好环形缓冲区后,内核中的 DRM 模块将环形缓冲区的信息,如缓冲区的头和尾的地址分别写入 GPU 的寄存器 Head Offset 和 Tail Offset 等。当 DRM 向寄存器 Tail Offset 写入数据时,将触发 GPU 读取并执行环形缓冲区中的命令,启动 GPU 中的 Pipeline 进行渲染。最后,GPU 的 Pipepline 将生成的像素阵列输入到帧缓冲。
建立数学模型
1.启动 Pipeline
在建模后,应用将顶点数据存入了顶点缓冲,加工需要的原材料已经准备好了,接下来就需要开动 Pipeline 这台加工机器了。那么,这个机器什么时候运转起来呢?通常是在程序中显示调用函数 glFlush 时。当然,一旦顶点缓冲已经充满了,也会自动调用 glFlush。读者可能有个疑问:我们编写程序时,有时并没有显示调用 glFlush 啊?没错,那是通常情况下,我们使用的都是启用了双缓冲的 OpenGL ,即前缓冲和后缓冲。对于启用双缓冲的 OpenGL 程序,OpenGL 规定,当程序在后缓冲渲染完成后,请求交换到前后缓冲时,使用 OpenGL 的 API glXSwapBuffers,而实际上,函数 glXSwapBuffers 已经替我们调用了 glFlush。
2.Pipeline 中的软件计算阶段
所谓的软件计算阶段,是指计算过程是由 CPU 来负责的。CPU 从上下文中获取上个阶段的状态信息,进行计算,然后将计算结果保存到上下文中,作为下一个阶段的输入。上下文的数据抽象为结构体 TNLcontext,其中非常重要的一个成员是结构体 vertex_buffer 。
3.Pipeline 中 GPU 相关的阶段
很难要求所有厂家的 GPU 都按照一个标准设计,所以在启动 GPU 中的硬件阶段之前,需要将 OpenGL 标准规定的标准格式的顶点缓冲中的数据按照具体的 GPU 的要求组织一下,然后再传递给 GPU 。
4.复制顶点数据和批量数据到内核空间
在 Pipeline 的软件阶段,所有阶段的计算结果都保存在用户空间,为了启动 Pipeline 的硬件阶段,显然需要将这些数据复制到内核空间的 BO,这样 GPU 才可以访问。_mesa_flush 最后将调用 3D 驱动中的函数 _intel_batchbuffer_flush 进行复制。
5.启动 GPU 中的 Pipeline
将数据复制到内核空间的BO后,接下来就需要通知GPU来读取这些数据,并执行GPU中的Pipeline。以Intel GPU为例,其规定需要将批量数据组织到一个环形缓冲区中,然后GPU从环形缓冲区中读取并执行命令,如图8-9所示。
环形缓冲区也只是从内存中分配的一块用于显存的普通存储区,所以,当内核中的DRM模块组织好其中的数据后,GPU并不会自动到环形缓冲区中读取数据,而是需要通知GPU来读取。
那么内核如何通知GPU呢?熟悉驱动开发的读者应该比较容易猜到,方法之一就是直接写GPU的寄存器。Intel GPU为环形缓冲区设计了专门的寄存器,典型的包括Head Offset、Tail Offset等。其中寄存器Head Offset中记录环形缓存区中有效数据的起始位置,寄存器Tail Offset中记录的则是环形缓存区中有效数据的结束位置。
一旦内核中的DRM模块向寄存器Tail Offset中写入数据,GPU就将对比寄存器Head Offset和Tail Offset中的值。如果这两个寄存器中的值不相等,那么就说明环形缓冲区中已经存在有效的命令了,GPU中的命令解析单元(Command Parser)通过DMA的方式直接从环形缓冲区中读取命令,并根据命令的类型,定向给不同的处理引擎。如果是3D命令,则转发给GPU中的3D引擎;如果是2D命令,则转发给GPU中的BLT引擎;如果是控制显示的,则转发给Display引擎;等等。
3、交换前缓冲和后缓冲
应用程序绘制完成后,需要将后缓冲交换(swap)到前缓冲,其中有三个问题需要考虑。
(1)谁来负责交换
如果应用自己负责将后缓冲更新到前缓冲,那么当有多个应用同时更新前缓冲时如何协调?显然将交换动作交给更擅长窗口管理的X服务器统一协调更为合理。
如果X服务器开启了复合扩展,更需要知道应用已经更新前缓冲了,因为X服务器需要通知复合管理器重新合成前缓冲。
综上,应该由X服务器来负责交换前后缓冲。
对于GPU支持交换的情况,X服务器通过2D驱动请求GPU进行交换。否则X服务器只能将前缓冲和后缓冲的BO映射到用户空间,使用CPU逐位复制。
(2)交换的时机
与2D应用不同,3D程序通常涉及复杂的动画和图像,如果显示控制器正在扫描前缓冲的同时,X服务器更新了前缓冲,那么可能会导致屏幕出现撕裂(tearing)现象。所谓的撕裂就是指本应该分为两桢显示在屏幕上的图像同时显示在屏幕上,上半部分是一帧的上半部分,而下半部分是另外一帧的下半部分,情况严重的将导致屏幕出现闪烁(flicker)。
以一个刷新率为60Hz的显示器为例,显示控制器每隔1/60秒从前缓冲读取数据传给显示器。每开始新的一帧扫描时,显示控制器都从前缓冲的最左上角的点,即第一行的第一个点开始,逐行进行扫描,直到扫描到图像右下角的点,即最后一行的最后一个点。经过这样一个过程之后,就完成了一帧图像的扫描。然后显示控制器回溯(retrace)到第一行的第一个点的位置,等待下一帧扫描开始,如图8-10所示。
更新一帧图像远不需要1/60秒,从更新完最后一行的最右侧一个点,到开始扫描下一帧之间的间隙被称为垂直空闲(vertical blank),简称为"vblank"。显然,如果在vblank这段时间更新前缓冲,就不会导致上述撕裂和闪烁现象的出现了。
(3)交换的方法
交换后缓冲和前缓冲通常有两种方法:第一是复制,在绘制完成后,X服务器将后缓冲中的数据复制到前缓冲,如图8-11所示。
但是这种方法效率相对较低,所以开发者们设计了页翻转模式(page flip)。页翻转模式不进行数据复制,而是将显示控制器指向后缓冲。后缓冲与前缓冲的角色进行互换,后缓冲摇身一变成为前缓冲,显示控制器将扫描后缓冲的数据到屏幕,而原来的前缓冲则变成了后缓冲,应用程序在前缓冲上进行绘制,如图8-12所示。
页翻转模式虽然效率高,但也不是所有的情况都适用。典型的,当一个应用处于全屏模式时,可以采用页翻转模式互换前缓冲和后缓冲。但是这对于使用复合管理器的图形系统来说,其实已经大大的提升效率了,因为复合管理器控制着整个屏幕的显示,所以复合管理器可以使用页翻转模式交换前缓冲和后缓冲。
- 应用发送交换请求
- X服务器处理交换请求
五、Wayland
将所有图形全部交由 X 服务器绘制的这种设计,在以2D应用为主的时代,一切还相安无事。但是随着基于3D的应用越来越多,效率问题逐渐凸显出来。与2D程序不同,3D程序的数据量要大得多,所以应用与X服务器之间需要传递大量的数据。设想一下几个人过独木桥和万人争过独木桥的场景,显然,X曾经引以为傲的设计——通过网络通信的客户/服务器架构,成为性能的瓶颈。
为了解决这个问题,X的开发者们设计了DRI机制,即应用程序不再将绘制图形的请求发送给X服务器,而是由应用程序自行绘制。这种设计与X最初的设计原则虽然有些格格不入,但是从某种程度上确实缓解了3D应用的效率问题。
但是,好景不长,人们逐渐不再满足于看上去比较“呆板”的图形用户界面,人们追求具有更华丽的3D特效的图形用户界面,比如窗口弹出和关闭时的放大/缩小动画、窗口之间的透明等。于是开发者们为 X 设计了复合( Composite ) 扩展,并仿效窗口管理器设计了一个所谓的复合管理器(Composite Manager)来实现这些效果。
我们以2D绘制过程为例来简要地看一下什么是复合扩展以及复合管理器,如图8-13所示。
开启复合扩展后,最大的一个区别是所有的窗口都不再共享一个前缓冲,而是有了各自的离屏区域。X服务器在各个窗口的离屏区域上进行绘制。在绘制好后,X服务器向另外一个特殊的应用复合管理器(Composite Manager)发出Damage通知。然后由复合管理器请求X服务器对这些离屏的窗口的缓冲区进行合成,最后请求X服务器显示到前缓冲。
在这个复合过程中,就是制造那些绚丽效果的地方。比如在合成的过程中,我们使用如图8-14的方法,就可以使窗口看起来是以放大效果出现的。
使用复合管理后,绚丽的效果有了,但是仔细观察图8-13会发现,X被人诟病的基于网络通信的客户/服务器模式的问题又变得严重了。除了X服务器和应用之间的通信外,为了进行合成,X服务器和复合管理器之间又多了一层通信关系。
事实上,在DRI的演进过程中,X不断被拆分和瘦身,开发者从X中移除了大量与渲染有关的功能到内核和各种程序库中。慢慢的,人们发现,X所做的事情已经大为减少,替代X已经不是一项不可能的任务。于是一部分开发者开始尝试为Linux开发替代X的窗口系统,Kristian Høgsberg提出了Wayland。事实上,这一个过程迟早要发生的,即使不是Wayland,也会涌现出如Yayland、Zayland等。
Wayland并不是一个全新的事物,它是站在X这个巨人的肩膀上,在X的不断演进中进化而来的。虽然从名字上看,Wayland与X没有丝毫相干,但是实际上两者的联系可谓千丝万缕。Wayland的开发者Kristian Høgsberg曾经是X的DRI
的主要开发者之一。套用一句奔驰的广告语,“经典是对经典的继承,经典是对经典的背叛”,Wayland去掉了X的客户/服务器架构,但是继承了X为提高绘制效率不懈努力的成果:DRI。除了逻辑上设计上不同外,Wayland基本的渲染原理与我们前面讨论的2D和3D的渲染原理完全相同。基本上,基于Wayland的图形架构如图8-15所示。
Wayland本身是一个协议,其具体的实现包括一个合成器(Compositor)以及一套协议实现库。当然,图形库为了与合成器进行通信,在图形库中需要加入Wayland协议的相关模块,也就是图8-15中的Wayland backend部分,当然这些都可以基于Wayland提供的库,而不必从头再将wayland协议实现一遍。
在Wayland下,所有的图形绘制完全由应用自己负责。其绘制过程与我们前面讨论的2D和3D的绘制过程完全相同,只不过2D的绘制部分也搬到图形库中了,绘制动作与合成器没有丝毫关系。而在绘制后,应用将前缓冲和后缓冲进行对调,并向合成器发送Damage通知,当然颜色缓冲不一定是前后两个,在具体实现中,有的图形系统可能使用3个、4个甚至更多。在收到Damage通知后,合成器将应用的前缓冲合成到自己的后缓冲中。而合成器的这个合成过程,与普通应用的绘制过程并无本质区别,也是通过图形库完成。
在合成完成后,合成器对调后缓冲与前缓冲,并设置显示控制器指向新的前缓冲,即原来的后缓冲。此前的前缓冲作为新的后缓冲,并作为合成器下一次合成的现场;而原来的后缓冲则变成现在的前缓冲,用于显示控制器的扫描输出。