8.4.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所示。

8.4.3 交换前缓冲和后缓冲 - 图1

图 8-10 图像扫描示意图

更新一帧图像远不需要1/60秒,从更新完最后一行的最右侧一个点,到开始扫描下一帧之间的间隙被称为垂直空闲(vertical blank),简称为"vblank"。显然,如果在vblank这段时间更新前缓冲,就不会导致上述撕裂和闪烁现象的出现了。

(3)交换的方法

交换后缓冲和前缓冲通常有两种方法:第一是复制,在绘制完成后,X服务器将后缓冲中的数据复制到前缓冲,如图8-11所示。

8.4.3 交换前缓冲和后缓冲 - 图2

图 8-11 复制模式

但是这种方法效率相对较低,所以开发者们设计了页翻转模式(page flip)。页翻转模式不进行数据复制,而是将显示控制器指向后缓冲。后缓冲与前缓冲的角色进行互换,后缓冲摇身一变成为前缓冲,显示控制器将扫描后缓冲的数据到屏幕,而原来的前缓冲则变成了后缓冲,应用程序在前缓冲上进行绘制,如图8-12所示。

8.4.3 交换前缓冲和后缓冲 - 图3

图 8-12 页翻转模式

页翻转模式虽然效率高,但也不是所有的情况都适用。典型的,当一个应用处于全屏模式时,可以采用页翻转模式互换前缓冲和后缓冲。但是这对于使用复合管理器的图形系统来说,其实已经大大的提升效率了,因为复合管理器控制着整个屏幕的显示,所以复合管理器可以使用页翻转模式交换前缓冲和后缓冲。

1.应用发送交换请求

对于一个OpenGL程序来说,在绘制完成后,需要调用GLX扩展中的函数glXSwapBuffers向X服务器发出交换请求:

8.4.3 交换前缓冲和后缓冲 - 图4

glXSwapBuffers首先调用glFlush启动Pipeline进行渲染。然后调用DRI2扩展的指针swapBuffers指向的函数向X服务器发出交换请求。DRI2扩展中指针swapBuffers指向的函数是DRI2SwapBuffers:

8.4.3 交换前缓冲和后缓冲 - 图5

8.4.3 交换前缓冲和后缓冲 - 图6

函数DRI2SwapBuffers创建了一个类型为X_DRI2SwapBuffers的X请求,然后调用函数_XReply将这个请求发送给X服务器。

2.X服务器处理交换请求

X服务器中处理来自OpenGL应用的请求在DRI/GLX的扩展模块中,对应的函数是DRI2SwapBuffers:

8.4.3 交换前缓冲和后缓冲 - 图7

函数DRI2SwapBuffers首先获取请求更新的窗口的前缓冲和后缓冲。X服务器在前面创建帧缓冲时已经将各个缓冲记录到了各个窗口中,所以这里取出即可。其中,pDestBuffer指向前缓冲,pSrcBuffer指向后缓冲。取得前缓冲和后缓冲后,具体的交换动作显然需要2D驱动来完成。DRI2SwapBuffers调用2D驱动中的函数ScheduleSwap交换后缓冲和前缓冲。

在Intel GPU的2D驱动中,函数指针ScheduleSwap指向函数I830DRI2ScheduleSwap:

8.4.3 交换前缓冲和后缓冲 - 图8

8.4.3 交换前缓冲和后缓冲 - 图9

前面谈到X服务器应该在vblank时更新前缓冲,实现中也确实如此。I830DRI2ScheduleSwap没有直接进行交换,而是调用库libdrm中的函数drmWaitVBlank,这个函数告诉显示控制器,在vblank时,向内核发送vblank事件,如第17行代码所示。

函数I830DRI2ScheduleSwap需要做的另外一件事就是判断前缓冲和后缓冲的交换方式。默认的交换方式是复制,如第7行代码所示。第9~12行代码调用函数can_exchange来判断是否可以使用更高效的页翻转方式。

Intel GPU的2D驱动在初始化时注册vblank事件的回调函数是intel_vblank_handler:

8.4.3 交换前缓冲和后缓冲 - 图10

收到vblank事件后,函数I830DRI2FrameEventHandler首先判断等待vblank的交换请求希望使用的是页翻转模式还是复制模式。如果是页翻转模式,为了安全起见,再次使用函数can_exchange检查是否可以进行页翻转,确认没有问题后,则调用函数I830DRI2ScheduleFlip执行翻转。否则,则调用函数I830DRI2CopyRegion将后缓冲的内容复制到前缓冲。

(1)页翻转模式

进行页翻转的函数I830DRI2ScheduleFlip的相关代码如下:

8.4.3 交换前缓冲和后缓冲 - 图11

8.4.3 交换前缓冲和后缓冲 - 图12

I830DRI2ScheduleFlip调用2D驱动中的函数intel_do_pageflip进行翻转。当然翻转后需要更新状态,包括更新当Screen Pixmap对应的BO,这就是函数I830DRI2ScheduleFlip调用I830DRI2ExchangeBuffers的目的。2D驱动中函数intel_do_pageflip的代码如下:

8.4.3 交换前缓冲和后缓冲 - 图13

函数intel_do_pageflip并没有使用库libdrm提供的接口,如drmModeSetCrtc设置显示控制器扫描的缓冲,而是使用了接口drmModePageFlip。相比于有点莽撞的drmModeSetCrtc,函数drmModePageFlip能确保是在发生vblank时设置显示控制器扫描的缓冲。drmModePageFlip将翻转的动作排队到下一个vblank事件发生时的处理队列中,在下个vblank发生时,设置显示控制器扫描的缓冲。

(2)复制模式

处理复制模式的函数I830DRI2CopyRegion的代码如下:

8.4.3 交换前缓冲和后缓冲 - 图14

看到ops,读者一定非常熟悉了,没错,这就是我们前面讨论2D渲染时提及的画笔。在UXA(uxa_ops)中,CopyArea对应的函数是intel_uxa_copy:

8.4.3 交换前缓冲和后缓冲 - 图15

8.4.3 交换前缓冲和后缓冲 - 图16

看到函数intel_uxa_copy的内容是否似曾相识?没错,指令XY_SRC_COPY_BLT与8.3.2节讨论的指令XY_COLOR_BLT非常相似,最大的不同是多了复制的源的信息。Intel GPU的指令XY_SRC_COPY_BLT的格式如表8-2所示。

8.4.3 交换前缓冲和后缓冲 - 图17

下面我们结合表8-2来分析函数intel_uxa_copy为GPU组织批量缓冲的过程。

1)第9行代码填充的是第0个双字,即BLT引擎的寄存器BR00。这个寄存器中最重要的就是指令的操作码(Opcode),即第22~28位。对于指令XY_SRC_COPY_BLT,其操作码是0x53。观察宏XY_SRC_COPY_BLT_CMD的定义:

8.4.3 交换前缓冲和后缓冲 - 图18

其中从第22位开始的0x53正是指令XY_SRC_COPY_BLT的指令码。另外,第29~30位设置为2,告诉GPU这个指令是一个2D指令,需要GPU定向给BLT引擎。

2)第11行代码填充的是第1个双字,对应BLT引擎的寄存器BR13,其中"intel->BR[13]"在8.3.2节我们已经讨论过,表示色深。另外,dst_pitch表示目标区域的跨度,所谓的跨度就是以字节为单位的图形的宽度。

3)第12行代码填充了第2个双字,对应BLT引擎的寄存器BR22,这个寄存器中保存的是目标区域的左上角的坐标。

4)第13行代码填充了第3个双字,对应BLT引擎的寄存器BR23,这个寄存器中保存的是目标区域的右下角的坐标。

5)第14行代码填充了第4个双字,对应BLT引擎的寄存器BR09,这个寄存器中保存的是存储目标区域像素阵列的BO,当然使用的是BO在GPU虚拟地址空间的地址,即BO的offset。

6)第15行代码填充了第5个双字,对应BLT引擎的寄存器BR26,这个寄存器中保存的是源区域的左上角的坐标。

7)第16行代码填充了第6个双字,对应BLT引擎的寄存器BR11,这个寄存器中保存的是源区域的图形的跨度。

8)第17行代码填充了第7个双字,对应BLT引擎的寄存器BR12,这个寄存器中保存的是存储源区域的像素阵列的BO的地址。