宠文网

深度探索Linux操作系统

宠文网 > 科普学习 > 深度探索Linux操作系统

8.3 2D渲染

书籍名:《深度探索Linux操作系统》    作者:王柏生
    《深度探索Linux操作系统》章节:8.3 2D渲染,宠文网网友提供全文无弹窗免费在线阅读。!


这一节,我们结合X窗口系统,讨论2D程序的渲染过程。我们可以形象地将2D渲染过程比喻为绘画,其中有两个关键的地方:一个是画布,另外一个是画笔。

X服务器启动后,将加载GPU的2D驱动,2D驱动将请求内核中的DRM模块创建帧缓冲,这个帧缓冲就相当于画布。然后X服务器按照绘画需要,从画笔盒子中挑选合适的画笔进行绘画。

X的画笔保存在结构体GCOps中,其中包含了基本的绘制操作,如绘制矩形的PolyRectangle,绘制圆弧的PolyArc,绘制实心多边形的FillPolygon,等等。代码如下:



最初,这些绘制操作均由CPU负责完成,也就是我们通常所说的软件渲染。X中的fb层就是软件渲染的实现,代码如下:



但是随着GPU的不断发展,其计算能力越来越强。于是X的开发者们不断改进X的渲染部分,希望能充分利用GPU擅长的图形操作以大幅提高计算机的图形能力,而又可以解放CPU,使其专心于控制逻辑。也就是说,X的开发者们希望画笔盒子中的画笔更多地来自GPU。

当然,任何事物都不是一蹴而就的,GPU的渲染能力也是螺旋演进的,对于GPU尚未实现的或者相比来说CPU更适合的渲染操作还是需要CPU来完成,因此,X的渲染架构也随着GPU的演进不断地改进。在XFree86  3.3的时候,X的开发者设计了XAA(XFree86  Acceleration  Architecture)架构;在X.Org  Server  6.9版本,开发者用改进的EXA取代了XAA;当DRM中使用了GEM后,Intel的GPU驱动开发者们重新实现了EXA,并命名为UXA(Unified  Acceleration  Architecture);随着Intel推出Sandy  Bridge及ivy  Birdge芯片组,Intel又开发了SNA(SandyBridge's  New  Acceleration)。

后续,我们以成熟且稳定的UXA为例进行讨论。在UXA架构下,X的画笔盒子如下:



我们看到uxa_ops包含在Intel的GPU驱动中,当然,这是非常合理的,因为只有GPU自己最清楚哪些渲染自己可以胜任,哪些还需要CPU来负责。在uxa_ops中,有一部分画笔来自GPU,另外一部分来自CPU。

对于每一个绘制操作,UXA首先检查GPU是否支持这个绘制操作,或者说在某些条件下,对于这个绘制操作,GPU渲染的比CPU更快。如果GPU支持这个绘制操作,UXA首先将绘制的命令翻译为GPU可以识别的指令,并将这个指令、绘制所需的相关数据,以及保存像素阵列的BO在显存地址空间中的地址,一同保存在用户空间的批量缓冲(Batch  Buffer),然后通过DRM将用户空间的批量缓冲复制到内核为批量缓冲创建的BO,之后通知GPU从BO中读取指令和数据进行绘制。实际上,DRM按照Intel  GPU的要求在批量缓冲和GPU之间还组织了一个环形缓冲区(Ring  buffer),但是我们暂时忽略它,这对于理解2D渲染过程没有任何影响,后面在讨论3D渲染过程时,我们会简单的讨论这个环形缓冲区。

如果GPU不支持这个绘制操作,那么UXA将代表帧缓冲的BO映射到X服务器的用户空间,X服务器借助fb层中的实现,使用CPU进行绘制。

也就是说,UXA在fb和GPU加速的上面封装了一层,其根据具体绘制动作选择使用来自GPU的画笔或来自CPU的画笔。

综上,X的2D渲染过程如图8-5所示。

图 8-5 X的2D渲染过程

不知读者是否注意到,无论是fbGCOps,还是uxa_ops,其中均有个别的绘制函数以"mi"开头。这些以"mi"开头的函数包含在X的mi层中。mi是Machine  Independent的缩写,顾名思义,是与机器无关的实现。笔者没有找到X中关于这个层的非常明确的解释,但是根据mi中的代码来看,其中的绘致函数根据不同的绘制条件,被拆分为调用其他GCOps中的绘制函数。

基本上,拆分的原因无外乎GPU支持的绘制原语有限,所以有些绘制操作需要分解为GPU可以支持的动作。或者出于绘制效率的考虑,将某些绘制操作拆分为效率更好的绘制原语。因此,X将这些与具体绘制实现无关的代码剥离到一个单独的模块mi中。从这个角度或许能解释X为什么将这个层命名为Machine  Independent。



8.3.1 创建前缓冲


在X环境下,在不开启复合(Composite)扩展的情况下,所有程序共享一个前缓冲。对于2D程序,所有的绘制动作生成的图像的像素阵列最终都输出到这个前缓冲上,窗口只不过是前缓冲中的一块区域而已。

但是一旦开启了复合扩展,那么每个窗口都将被分配一个离屏(offscreen)的缓冲,类似于OpenGL环境中的后缓冲。应用将生成的像素阵列输出到这个离屏的缓冲中,在绘制完成后,X服务器将向复合管理器(Composite  Manager)发送Damage事件,复合管理器收到这个事件后,将离屏缓冲区的内容合成到前缓冲。为了避免复合扩展干扰我们探讨图形渲染的本质,在讨论2D、包括后面的3D渲染时,我们都不考虑复合扩展开启的情况。

在X中,Window和Pixmap是两个绘制发生的地方,Window代表屏幕上的窗口,Pixmap则代表离屏的一个存储区域。所以自然而然的,X使用数据结构Pixmap来表示前缓冲。因为这个前缓冲对应整个屏幕,而且不属于某一个应用,因此开发者也将代表前缓冲的这个Pixmap称为Screen  Pixmap。后续为了行文方便,我们有时也使用Screen  Pixmap这个词来代表前缓冲的这个Pixmap对象。显然,这个Screen  Pixmap也是显示器(Screen)的资源,所以X将其保存到了代表显示器的结构体_Screen中:



其中指针devPrivate指向这个前缓冲,后面看到如GetScreenPixmap的函数,就是从_Screen中获取前缓冲。

Pixmap并不只是简单地抽象为像素阵列,还要包含一些解释像素数组所需要的信息,比如图形的高度、宽度、格式等,其定义如下:



结构体_Pixmap中的指针devPrivates指向保存前缓冲的像素阵列的BO。但是Intel  GPU的2D驱动为了记录更多信息,在BO基础上封装了一层,封装后的数据结构为intel_pixmap。所以,最终Pixmap中的devPrivate指向的并不是一个裸BO,而是在BO上包围了一层的一个intel_pixmap对象。结构体intel_pixmap的定义如下:



其中指针bo指向的就是保存前缓冲的像素阵列的BO。

1.创建前缓冲的BO

前缓冲的BO是在X服务器启动过程中,2D驱动初始化输出设备时,调用函数intel_allocate_framebuffer创建的,具体代码如下:



函数drm_intel_bo_alloc_tiled是库libdrm提供的API,在库libdrm中对应的函数是drm_intel_gem_bo_alloc_internal:



由代码可知,函数drm_intel_gem_bo_alloc_internal就是向内核中的DRM模块申请创建前缓冲的BO。

2.将前缓冲保存到屏幕对象

在创建了前缓冲的BO后,X服务器为前缓冲创建了Pixmap对象,即Screen  Pixmap。2D驱动则创建了封装前缓冲的BO的intel_pixmap对象。并且,X也将各个对象关联了起来。

X服务器创建Pixmap对象的代码如下:



函数miCreateScreenResources首先创建了一个Pixmap对象,这个对象就是Screen  Pixmap。然后将屏幕对象中的指针devPrivate指向Screen  Pixmap。

2D驱动中创建intel_pixmap对象的代码如下:



在上面的代码中,函数GetScreenPixmap的目的就是获取Screen  Pixmap。其中函数intel_set_pixmap_bo的代码如下:



函数intel_set_pixmap_bo首先创建了一个intel_pixmap对象,这个intel_pixmap对象中的指针bo指向的函数的第2个实参bo正是保存前缓冲像素阵列的BO。然后调用函数intel_set_pixmap_private将intel_pixmap与该函数的第1个实参,即Screen  Pixmap关联起来。

3.窗口与前缓冲的绑定

前缓冲已经建立起来了,但是,显然需要将窗口与前缓冲关联起来,否则在窗口上的绘制并不能体现到屏幕上。我们在编写具有图形界面的程序时,在绘制之前首先需要创建绘制所在的窗口。恰恰就是在创建窗口时,窗口与前缓冲绑定了。我们来看一下X中创建窗口的函数fbCreateWindow:



fbCreateWindow调用函数fbGetScreenPixmap获取Screen  Pixmap,并将窗口对象与Screen  Pixmap绑定。

显然,所谓的创建窗口事实上就是将窗口与前缓冲关联起来,以后凡是发生在窗口上的绘制,都将直接绘制到前缓冲中。



8.3.2 GPU渲染

GPU渲染,也就是我们通常所说的硬件加速,从软件的层面所做的工作就是将数学模型按照GPU的规定,翻译为GPU可以识别的指令和数据,传递给GPU,生成像素阵列等图像密集型计算则由GPU负责完成。可见,当使用GPU进行渲染时,在软件层面,实质上就是组织命令和数据而已。

Intel  GPU的2D驱动是如何将这些命令和数据传递给GPU的呢?读者一定想到了BO。在Intel  GPU的2D驱动中,定义了使用了一种所谓的批量缓冲来保存这些命令和数据,这里所谓的批量就是将驱动准备命令和数据放到这个缓冲,然后批量地让GPU来读取,这就是批量缓冲的由来。批量缓冲的相关定义在结构体intel_screen_private中:



在结构体intel_screen_private中,数组batch_ptr是X(准确地说是2D驱动)在用户空间使用的组织命令和数据的地方,指针batch_bo则指向内核空间保存批量缓冲数据的BO。2D驱动将相关的命令和数据组织在数组batch_ptr中,然后将数组batch_ptr中的数据复制到batch_bo指向的内核空间中的BO中,供GPU来读取。

2D驱动的代码中为了方便,也定义了几个操作批量缓冲的宏,典型的有如下的OUT_BATCH和OUT_RELOC_PIXMAP_FENCED:



宏OUT_BATCH将命令或者数据写入数组batch_ptr,宏OUT_RELOC_PIXMAP_FENCED将BO的在GPU地址空间中的虚拟地址写入数组batch_ptr。

接下来,我们以具体的绘制方法miPolyRectangle为例,讨论2D驱动如何使用GPU进行绘制,也就是我们通常所说的硬件加速,具体代码如下:



根据不同的绘制条件,函数miPolyRectangle将绘制矩形的动作进行了拆分,拆分的目的是选择最合适的绘制方式进行绘制。这个拆分方法不依赖于任何具体硬件,因此,X将这个拆分过程放到mi层中。

如果矩形的线性是实心填充的,且线段交汇处是尖角(JoinMiter)风格的,并且宽度不为0,那么使用方法PolyFillRect绘制,见代码第4~9行。否则,使用方法Polylines绘制,如代码第10~14行所示。

以PolyFillRect为例,其在UXA(即uxa_ops)中对应的具体函数是uxa_poly_fill_rect:



函数uxa_poly_fill_rect首先检查各种绘制条件以确认是否适合使用GPU进行绘制,如代码第4~7行所示。如果适合使用GPU进行绘制,则陆续调用函数prepare_solid和solid为GPU准备指令和数据,下面我们会重点讨论这两个函数。

否则,正如第5行代码所示,跳转到标签fallback处,即代码第9行,使用函数uxa_check_poly_fill_rect进行绘制,这个函数实际是使用CPU进行绘制的,我们将在8.3.3节进行讨论。

UXA中的函数指针solid指向intel_uxa_solid:r  />


根据前面讨论的批量缓冲以及为操作批量缓冲封装的几个宏,读者一定已经看出来了,上面的代码是在组织批量缓冲在用户空间的数组batch_ptr。那么函数intel_uxa_solid向batch_ptr中填入各项的意义是什么呢?这个显然要参考Intel  GPU的指令格式。根据第8行代码可见,2D驱动发送给GPU的指令是XY_COLOR_BLT,该指令的功能是对目标区域以指定颜色填充。Intel  GPU的指令XY_COLOR_BLT的格式如表8-1所示。

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

1)由表8-1可见,指令XY_COLOR_BLT包含6个双字(DWord),第10行代码填充的是第0个双字,其中"BR00"是什么意思呢?事实上,GPU内部分为多个微核,处理不同的命令,处理2D指令的微核称为BLT引擎(Engine)。对于每个2D指令,每个双字实际上分别被送往BLT引擎的各个寄存器中。因此,这里的BR就是"BLT  Register"的简写,如指令XY_COLOR_BLT中的第1个双字被送往BLT引擎的第0个寄存器,第2个双字被送往BLT引擎的第13个寄存器,等等。

因此,对于GPU指令来说,需要指明自己需要哪个微核来处理,这就是第一个双字中第29~31位的作用,0x2表示2D指令、0x3表示3D指令等。寄存器BR00中最重要的就是指令的操作码,即第22~28位,对于指令XY_COLOR_BLT,其操作码是0x50。其余位主要用于控制,如第11位用于控制是否打开Tiling,等等。因此,寄存器BR00也被称为"BLT  Opcode&Control  Register"。

根据宏XY_COLOR_BLT_CMD的定义:



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

2)第12行代码填充的是第1个双字,对应BLT引擎的寄存器BR13。其中"intel->BR[13]"是为了方便构建指令在程序中定义的一个变量,保存寄存器BR13的值。这就是函数uxa_poly_fill_rect在调用solid之前,调用函数prepare_solid的目的。在UXA(uxa_ops)中,prepare_solid对应的函数是intel_uxa_prepare_solid:



函数intel_uxa_prepare_solid根据图像实际使用的色深,设置相应的位。intel_uxa_prepare_solid除了计算了寄存器BR13中的色深外,也计算了寄存器BR16的值,BR16中的值是GPU进行填充时使用的颜色。

除了设置色深外,第12行代码也设置了图像的跨度(pitch),跨度是以字节为单位表示的图像的一行的长度。

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

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

5)第15~16行代码填充的第4个双字,对应BLT引擎的寄存器BR09,这个寄存器中保存的是目标区域在GPU的显存空间中的地址。这里的pixmap就是Screen  Pixmap,所以宏OUT_RELOC_PIXMAP_FENCED就是将保存前缓冲的像素阵列的BO在显存空间中的地址填充到这个双字中。读者可以参见前面关于宏OUT_RELOC_PIXMAP_FENCED的介绍。

6)第17行代码填充了第5个双字,对应BLT引擎的寄存器BR16,这个寄存器中保存的是填充使用的颜色。

在完成指令XY_COLOR_BLT的构建后,函数intel_batch_submit将用户空间的batch_ptr中的数据复制内核空间的,并通知GPU,开始执行批量缓冲中的指令,代码如下:



其中函数dri_bo_subdata,我们已经在8.2.2节讨论过,其负责将数据从用户空间复制到内核空间。所以这里就是2D驱动将组织在用户空间中的数组batch_ptr中的数据复制到批量缓冲在内核中对应的BO。

函数drm_intel_bo_mrb_exec通知GPU开始执行批量缓冲中的指令。方式是通过写GPU的一个寄存器,具体过程请参考8.4.2节。



8.3.3 CPU渲染

根据上节讨论的函数uxa_poly_fill_rect,我们看到,GPU并不是接收全部的绘制实心矩形的操作。对于不满足GPU条件的实心矩形,则将求助于CPU绘制,对应的函数是uxa_check_poly_fill_rect:



BO是由DRM模块在内核空间分配的,因此运行在用户空间的X(2D驱动)要想访问这个内存,必须首先要将其映射到用户空间,这是由函数uxa_prepare_access来完成的。然后,X使用CPU在映射到用户空间的BO上进行绘制。看到以fb开头的函数fbPolyFillRect,读者一定猜到了,这就是X的fb层的函数,而fb层正是软件渲染的实现。

(1)映射BO到用户空间

函数uxa_check_poly_fill_rect调用uxa_prepare_access将BO映射到用户空间:



在UXA(uxa_ops)中,指针prepare_access指向的函数是intel_uxa_prepare_access:



函数intel_uxa_prepare_access通过libdrm库中的函数drm_intel_gem_bo_map_gtt申请内核中的DRM模块将保存前缓冲的像素阵列的BO映射到用户空间:



看到熟悉的函数mmap,读者应该一切都明白了。从CPU的角度看,BO与普通内存并无区别,所以,映射BO与映射普通内存完全相同。其中bufmgr_gem->fd指向的就是代表BO的共享内存。

(2)使用CPU在映射到用户空间的BO上进行绘制

我们再来简单看看软件渲染函数fbPolyFillRect的实现:



根据上面的代码可见,X的软件渲染层(即fb这一层),或者借助库pixman中的API,或者自己直接操作像素数组,完成图形的绘制。其原理非常简单,就是直接设置像素数组中的颜色值或索引。

经过对2D渲染的探讨,我们看到,所谓的软件渲染和硬件加速,本质上都是生成图像的像素阵列,只不过一个是由CPU来计算的,另外一个是由GPU来计算的。当然,对于硬件加速,CPU要充当一个翻译,将数学模型按照GPU的要求翻译为其可以识别的指令和数据。