本文章翻译自:The Linux Graphics Stack
前言
本篇文章主要针对开源部分的软件驱动,例如radeon、nouveau和intel等,可能不太适用于AMD和NVIDIA一类的专有驱动程序。如果有说得不对的地方,欢迎批评指正。
这里会介绍一下完整的栈,主要分为两条路径:基于OpenGL的3D渲染和基于cairo的2D渲染。
基于OpenGL的3D渲染
- 程序使用OpenGL来绘制图形;
- Mesa库实现OpenGL API。它使用特定显卡的驱动程序将API转换为特定硬件的形式。如果驱动内部使用gallium,则有一个共享组件将OpenGL API转变为常见的中间表示TGSI。API将通过gallium,所有特定设备的驱动程序都从TGSI转换为硬件命令;
- libdrm使用特定显卡的ioctls与Linux内核交互;
- Linux内核在特殊权限下能够在显卡上分配内存;
- 在Mesa的层面上,Mesa使用DRI2和Xorg进行通信,以确保图形缓冲区的翻转和窗口位置等都能同步。(图形缓冲区的翻转是指在双缓冲模式下,将后台缓冲区的内容显示到前台缓冲区,从而实现流畅的动画效果;窗口位置是指图形界面中各个窗口的位置和大小)
基于cairo的2D渲染
- 程序使用cairo来绘制图形;
- 使用梯度来绘制圆形。cairo将这些圆形分解成多边形,并使用XRender扩展将这些多边形和梯度发送给X server。如果X server不支持XRender扩展,cairo就使用libpixman在本地绘制,并使用其它方法将渲染好的像素图发送给X server。
- X server响应XRender的请求。Xorg能够使用多种特殊的驱动进行绘制工作。
A. 在软件方面,当图形驱动程序不胜任任务时,Xorg将使用pixman来进行实际的绘制,这个绘制类似cairo的做法。
B. 在硬件加速方面,Xorg驱动会通过libdrm与内核交互,也用同样的方式将纹理和命令发送至显卡。
关于Xorg如何将内容显示在屏幕上,Xorg本身会使用KMS和特定于显卡的驱动程序来设置一个帧缓冲区,用于绘制图形。
X Window System,X11,Xlib,Xorg
X11不仅仅与图形相关,它还拥有一套事件传递系统(窗口附加的属性概念),许多非图形相关的东西建立在此之上,例如剪贴板、拖拽行为等。下面将列出整个X Window System、X11和其它相关内容。
X11
X Window System使用的一种协议。
Xlib
X Window System客户端(client)的实现,以及许多其它实用程序来管理X Window System上的窗口。由工具包使用以支持X,例如GTK+和QT。现如今在应用程序中很少见。
XCB
XCB有时被认为是Xlib的替代方案,实现了大量X11协议。它的API级别比Xlib低得多,因此Xlib实际上是在当今XCB上构建的。
Xorg
X Window System服务器端的实现。
这里进行一个简要说明,在下文中,
- X server:代表广义的X server,包括Xorg、苹果的X server实现、Kdrive等;
- X11或X Window System:代表协议或系统的整体设计;
- Xorg:代表Xorg本身,它是最常用的X server,这里并不指代任何其它X server。
X11是一个可扩展的协议,这意味着当我们想要加入新的特性时,不需要重新设计一套协议或者打破现有的旧客户端。例如,xeyes
和oclock
由于形状扩展(shape extension)从而可以得到精美的形状,为非矩形窗户提供了支持。但是这一功能并不能任意使用,前提是必须将对扩展extension的支持添加到服务器和客户端。核心协议本身提供了查询功能,客户端可以询问服务器支持哪些扩展,从而知道它可以或不能使用的功能。
X11还被设计为“网络透明”,它意味着我们不能总是依赖X服务器和X客户机在同一台机器上,因此两者之间的任何交互都必须通过网络进行。在这种情况下,一个现代的桌面环境是不能开箱即用的,因为很多进程间通信是通过X11以外的系统进行的,比如DBus。在网络上,它相当“健谈”,导致了大量的流量。当服务器和客户端在同一台机器上时,它们不会通过网络,而是通过一个UNIX套接字进行通信,这样内核就不必拷贝数据。
cairo
cairo是一个用于绘制矢量图形的库,可以直接被像Firefox这样的应用程序使用,也可以通过像GTK+这样的库使用。GTK+ 3的绘图模型完全基于cairo。如果你曾经使用过HTML5的<canvas>
,cairo实现了几乎相同的API。虽然<canvas>
最初是由苹果公司开发的,但矢量绘图模型是众所周知的,因为它是PostScript矢量绘图模型,它在其他矢量图形技术和标准中得到了支持,如PDF,Flash,SVG,Direct2D,Quartz 2D,OpenVG等等。
cairo支持通过Xlib后端绘制X11表面。
cairo已用于GTK+等工具包之中,在GTK+ 2中添加了一些功能,以便在 GTK+ 2.8中可选地使用 cairo。GTK+ 3中的绘图模型需要cairo。
XRender Extension
XRender是X11的特殊扩展,添加了抗锯齿绘图原语(X11现有的图形是锯齿状的)、渐变、矩阵变换等的支持。最初的意图是驱动程序可以有专门的加速代码路径来进行特定的绘图。不幸的是,由于一些不直观的原因,软件光栅化似乎和硬件加速一样快。XRender处理的是对齐的梯形——带有可选的左右斜边的矩形。Carl Worth和Keith Packard提出了一种“快速”的软件光栅化方法来绘制梯形。梯形也非常容易分解成两个三角形,以便用于快速硬件渲染之中。airo包含了一个很棒的show-traps工具,它可以让你对它所做的梯形镶嵌有一些了解。
这是我画的一个简单的红色圆圈。它被分解成两组梯形——一组用于描边,一组用于填充。由于show-traps工具默认给你的图形并不是很有启发性,我修改了这个工具,让每个梯形都有一个独特的颜色。这是黑色描边的梯形集合。
pixman
X server和cairo有时都需要做像素级操作。之前,cairo和Xorg均针对基础光栅化算法、对某些缓冲区的像素级访问(ARGB32,RGB24,RGB565)、渐变、矩阵等拥有独立实现。现在,X server和cairo共享一个低级库——pixman。pixman能够处理前面提到的所有任务,但它既不是公共的API,也不是一个图形绘制API,实际上它根本不是API,它只是一个处理各部分之间代码冗余的解决方案。
OpenGL,Mesa,gallium
现在来到了有趣的部分:现代硬件加速。这里假设读者已经知道什么是OpenGL,它不仅仅是一个库,永远不会只有一组libGL.so的源码,每一个厂商都应该提供自己的libGL.so。NVIDIA 提供了自己的OpenGL实现,并基于Windows和OS X的实现发布了自己的 libGL.so。
如果你正在运行开源驱动,你的libGL.so实现可能来自于Mesa。Mesa有很多功能,但是它提供的最著名的功能之一是它的OpenGL实现,这是OpenGL API的开源实现。Mesa本身具有多个后端,它为这些后端提供支持。它有三个基于CPU的实现:swrast(过时且老旧,不要使用它)、softtube(缓慢)、llvmpipe(可能很快)。Mesa还具有特定于硬件的驱动程序。英特尔支持Mesa,并且已经为他们的芯片组建立了许多驱动程序,这些驱动程序随Mesa一起发布。在Mesa中也支持radeon和nouveau驱动程序,但是它们是建立在一个不同的架构上的:gallium。
gallium并不是什么神奇的东西。它是一组组件,可以使驱动程序的实现变得更容易。它的主要思想是状态跟踪器,这些状态跟踪器实现了某种形式的API(OpenGL,GLSL,Direct3D),将这些状态转换为中间表示(称为Tungsten Graphics Shader Infrastructure,或TGSI),然后后端将这个中间表示转换为硬件本身能够执行的操作。
令人难过的是,英特尔驱动并不使用gallium,我的同事告诉我这是因为英特尔驱动开发商不喜欢Mesa和他们的驱动之间有一个中间层。
另一些首字母缩写的快速介绍
为了避免首字母缩写词使读者产生困惑,这里列出了下文中会涉及到的部分,供读者简单参考。
GLES
OpenGL有几个针对不同形式因素的配置文件。GLES是其中之一,代表“GL Embedded System”或“GL Embedded Subset”。这是针对嵌入式市场的最新方法。IPhone支持GLES 2.0。
GLX
OpenGL本身是没有平台和窗口系统的概念。因此,需要一些绑定来转换OpenGL和X11之间的差异。例如,在 X11窗口中放置一个OpenGL场景,GLX就是这种胶水。
WGL
请参阅上文,但用“Windows”替换“X11”,这里的“Windows”是Microsoft操作系统。
EGL
EGL和GLES经常容易混淆。EGL是一个与平台无关的新API,由Khronos Group(开发和标准化OpenGL的同一个团队)开发,它提供了在平台上建立和运行OpenGL场景的工具。与OpenGL一样,这也是供应商实现的;它是WGL/GLX等绑定的替代品,而不仅仅是建立在二者之上的库(如:GLUT)。
fglrx
fglrx是AMD专有Xorg OpenGL驱动程序的旧名称,现在被称为“Catalyst”。它的全称是“FireGL and Radeon for X”。由于它是一个专有的驱动程序,它有自己的libGL.so的实现。我不知道它是否基于Mesa。我只是提一下,因为它有时会和一些通用的技术,如AIGLX或GLX混淆,因为它们都包含了“GL”和“X”这些字母。
DIX,DDX
Xorg的X graphics部分主要有两部分组成:DIX(“Driver Independent X”,独立于驱动的X子系统)和DDX(“Driver Dependent X”,依赖驱动的X子系统)。当我们谈论Xorg驱动时,从技术上来看,更准确的术语是DDX驱动。
Xorg驱动,DRM,DRI
前文中提到过,Xorg能够基于特定硬件对渲染进行加速,同时也提到了,这一功能是实现不是将X11绘制命令转换成OpenGL调用。如果驱动程序是在Mesa上实现的,那么如何在不使Xorg依赖于Mesa的情况下工作呢?
答案是创建一个新的基础让Mesa和Xorg共享。Mesa实现了OpenGL的部分,Xorg实现了X11的绘图部分,它们都将API转换为特定于显卡的命令。这些命令然后通过一个叫做“直接渲染管理器”(Direct Rendering Manager,简称DRM)的东西上传到内核。libdrm使用一组与内核通信的通用的、私有的ioctl来在显卡上分配东西,并将它需要的命令、纹理等塞进去。这个ioctl接口有两种形式:Intel的GEM和Tungsten Graphics的TTM。它们之间没有明显的区别,它们做的事情是一样的,只是不同的竞争实现。从历史上看,GEM曾被设计并自豪地宣布是TTM的一个更简单的替代方案,但随着时间的推移,它悄悄地变得和TTM一样复杂了。哎~
这意味着当你运行像glxgears这样的程序时,它会加载Mesa,Mesa再加载libdrm,libdrm使用GEM/TTM与内核驱动直接通信。是的,glxgears直接与内核驱动通信以显示一些旋转齿轮,并严肃地提醒你该实用程序的基准测试内容。
如果你输入ls /usr/lib64/libdrm_*
命令,你会发现有一些针对不同硬件的驱动程序。对于那些GEM/TTM不足以满足的情况,Mesa和 X server的驱动程序会有一套私有的ioctls来和内核通信,这些ioctls都被封装在这里。libdrm本身并不实际加载这些驱动程序。
X server需要知道这里发生了什么,这样它才能做一些同步之类的事情。你的glxgears、内核和X server之间的这种同步被称为 DRI,或者更准确地说,DRI 2。“DRI”代表“Direct Rendering Infrastructure”。“DRI”既指将Mesa和Xorg粘合在一起的项目(引入了DRM和我在这篇文章中谈到的一些东西),也指DRI协议和库。DRI 1 并不是很好,所以我们用DRI 2将其替换了。
KMS
顺便说一下,如果你想在一个新的X server进行开发,或者你想在没有X server的情况下在虚拟终端上显示图形,你应该怎么做呢?你必须配置实际的硬件,让它能够显示图形。在libdrm和内核中,有一个专门的子系统可以做到这一点,叫做KMS,即“Kernel Mode Setting”(内核模式设置)。同样,你可以通过一组ioctl设置一个图形模式、映射一个帧缓冲区,等等,从而在TTY上显示。以前有(现在仍然有)一些硬件特定的ioctl,所以创建了一个共享的库,叫做libkms,来提供一个共享的API。但最终,内核层有了一个新的API,字面上叫做“dumb ioctl”。建议使用新的dumb ioctl,而不是libkms。
如果你想在没有X server的情况下设置图形,这是完全可能的,只是比较低级。Plymouth是一个集成到现代发行版中的启动画面程序,它就是一个很好的例子,它可以在不依赖X server的情况下设置图形。
The “Expose” model,Redirection,TFP,Compositing,AIGLX
前面我使用了“compositing window manager”(合成窗口管理器)这个术语,但并没有真正描述什么是合成,或者窗口管理器做了什么。早在80年代,当X Window System在UNIX系统上设计时,很多其他的公司,如惠普、Digital Equipment公司、Sun Microsystems、SGI 等,也在开发基于X Window System的产品。X11故意没有规定任何关于控制窗口的基本策略,而是将这个责任委托给一个叫做“窗口管理器”的单独的进程。
举个例子,当时流行的环境,CDE,有一个叫做“鼠标跟随焦点”的系统,它在用户将鼠标移动到窗口本身时会聚焦窗口。这与Windows和Mac OS X默认使用的更流行的模型“点击聚焦”是不同的。
随着窗口管理器变得越来越复杂,开始出现了描述不同环境之间互操作性的文档。它也不会像“点击聚焦”那样强制规定任何策略。
另外,由于80年代许多系统没有太多的内存,它们无法存储所有窗口内容的整个像素内容。Windows和X11以相同的方式解决这个问题:每个X11窗口应该是有损的。也就是说,一个程序会被通知一个窗口已经被“暴露”。
想象一组窗口如上图所示,然后用户拖拽移动GIMP窗口,变成下图的样子:
深灰色的区域已经被暴露。一个ExposeEvent
将被发送给拥有该窗口的程序,它将重新绘制内容。这就是为什么在Windows或Linux的旧版本中,当你拖动一个窗口覆盖它们时,挂起的程序会变成空白的原因。如果在Windows中,桌面本身只是另一个没有任何特殊权限的程序,也可以像其它程序一样挂起,你就会有一个地狱般的错误报告。
因此,现在机器有了这么多的内存,我们可以通过一种叫做redirection(重定向)的机制让X11窗口无损。当我们重定向一个窗口时,X server将为每个窗口创建后备像素图,而不是直接绘制到后缓冲区。这意味着窗口将被完全隐藏,其它东西可以趁此机会显示其内存缓冲区中的像素。
合成扩展允许合成窗口管理器或“合成器”设置一个叫做合成覆盖窗口(COW)的东西。合成器拥有COW,并可以向它绘制。当你运行Compiz或GNOME Shell时,这些程序使用OpenGL来显示重定向的窗口。X server可以通过一个GL扩展,即TFP(Texture from Pixmap),把窗口内容给它们。它让一个OpenGL程序把一个X11像素图当作一个OpenGL纹理。
合成窗口管理器不是一定要使用TFP或OpenGL,只是这是最简单的方法。如果愿意,合成窗口管理器可以正常将窗户像素绘制到COW上。有人告诉我,Kwin4直接使用QT来复合窗口。
合成窗口管理器使用TFP从X server获取像素图,然后将其渲染到OpenGL场景的恰当位置,给人一种你认为你点击的东西是实际的X11窗口的错觉。这样描述可能听起来很傻,但是如果你玩玩GNOME Shell,调整窗口的大小或位置(在looking glass中输入global.get_window_actors().forEach(function(w) { w.scale_x = w.scale_y = 0.5; }
),你很快就会看到错觉在你面前消失,因为你会意识到当你点击时,你是直接穿过视频播放器,到达下面的实际窗口(在上面的代码片段中将0.5改为1.0可以恢复原来的行为)。
知道了这些,让我们再了解一个首字母缩写词:AIGLX,它表示“Accelerated Indirect GLX”。由于X11是一个网络协议,这意味着OpenGL应该在网络上工作。当OpenGL在网络上使用时,这被称为“间接上下文”,与在同一台机器上的“直接上下文”相对。在“间接上下文”中使用的网络协议相当不完整和不稳定。
要理解AIGLX背后的设计决策,你必须理解它试图解决的问题:让像Compiz这样的合成窗口管理器变得快速。虽然NVIDIA的专有驱动程序通过一个自定义接口实现了内核级别的内存管理,但是在这一点上,开源软件栈还没有实现这一点。从X server中将窗口纹理拉到图形硬件中,意味着每次窗口更新时,它都必须被复制一次,非常慢。因此,AIGLX像是一个临时的黑客,用软件实现OpenGL,防止复制到硬件。由于像Compiz这样的合成器使用的场景并不复杂,它的效果还不错。
尽管有很多炒作和Phoronix文章,但AIGLX实际上已经不再使用了,因为我们现在有了整个DRI堆栈,可以在不复制的情况下实现TFP。
正如你可以想象的,复制(或更准确地说,采样)窗口纹理内容,以便它可以作为一个OpenGL纹理绘制,需要复制数据。因此,大多数窗口管理器都有一个功能,可以关闭全屏的窗口的重定向。把这个行为描述为undirection听起来有点傻,因为这是窗口的初始状态。但是,尽管这是窗口的初始状态,但在我们现代的Linux桌面上,这几乎不是常态。这里的逻辑是,如果一个窗口会覆盖COW,而且不需要合成特性,它就可以安全地undirection取消重定向。这个功能是为了给像游戏这样的程序提供高性能,它们需要以60帧每秒的高性能运行。
Wayland
X窗口系统现在还存在的一个很大的原因,只是因为替换它需要很多的努力。随着Xorg从它最初的样子被剥离,以及大量的现代桌面环境所需要的功能只由扩展提供,那么我该怎么说呢,X已经过时了。
我们现在进入Wayland的介绍。Wayland重用了我们建立起来的很多现有基础。关于它最有争议的一点是,它缺乏任何形式的网络透明或绘图协议。X的网络透明特性在现代已经失去了意义,因为很多Linux的特性都托管在像DBus这样的地方,而不是X。看到像拖、放和剪贴板支持这样的东西在很大程度上只是为了网络支持而在X Window System中进行的黑客行为,真是可惜。
Wayland可以使用上面介绍的几乎整个栈,来在你的显示器上启动和运行一个帧缓冲区。Wayland仍然有一个协议,但它是基于UNIX套接字和本地资源的。最大的变化是,它没有像/usr/bin/Xorg那样运行的/usr/bin/wayland二进制文件。相反,Wayland遵循现代桌面的建议,将所有这些都移动到窗口管理器进程中。这些窗口管理器,在Wayland的术语中更准确地称为“compositor”(合成器),实际上是负责使用像evdev这样的系统从内核中拉取事件,使用KMS和DRM设置一个帧缓冲区,并使用它想用的任何绘图堆栈(包括OpenGL)在屏幕上显示窗口。虽然这听起来像是很多代码,但由于这些子系统已经移动到别处,做所有这些事情的代码可能只有2000-3000源代码行的数量级。考虑到mutter的一部分,只是为了实现一个合理的窗口焦点和堆叠策略,并与X server同步,就有大约4000-5000源代码行,也许你会更理解一点我对Wayland的迷恋。
Wayland有一个库,它的客户端和合成器的实现都应该使用,但它只是指定的Wayland协议的一个参考实现。任何人都可以用Python或Ruby写一个Wayland合成器,或者完全用纯Python实现协议,而不需要libwayland的帮助。
Wayland客户端与合成器通信,并请求一个缓冲区,合成器可以用OpenGL、cairo或其他方式绘制的缓冲区交给它们。合成器可以随心所欲地处理那个缓冲区——正常显示它;抛弃掉;或者让它在一个立方体上旋转。
合成器也负责输入和事件处理。如果你尝试了上面用GNOME Shell设置窗口缩放的方法,你可能一开始会感到困惑,然后发现你的鼠标对应的是未变换的窗口。这是因为我们并没有真正影响X11窗口本身,只是改变了它的显示方式。X server跟踪窗口的位置,而合成窗口管理器必须按照X server认为的位置来显示它们,否则就会造成混乱。
由于Wayland合成器负责从evdev读取事件并给窗口发送事件,它能对窗口的位置有更好的了解,并且可以在内部进行变换,这意味着我们不仅可以暂时地让窗口在一个立方体上旋转,还可以与立方体上的窗口进行交互。
总结
我经常听到Xorg的实现非常单体化。虽然这是非常正确的,但这并不是因为Xorg开发者的无能,而是因为我们必须支持的一些历史包袱,比如硬件加速的XRender协议,或者更早的非抗锯齿绘图命令,比如XPolyFill。虽然很明显X将在不久的将来被Wayland取代,但我想明确地说,这些都是在Xorg和桌面开发者的认可和帮助下发生的。他们不是固执,也不是无能,为了处理和实现一个30多年的协议和历史,他们做得非常出色,尤其是在新的架构方面。
……后面还有几段是原作者的个人感慨及想法,就不继续翻译了。本人刚刚接触Linux图形栈不久,如果有翻译理解不对的地方欢迎批评指正!