chrome 硬件渲染(GPU Accelerated Compositing in Chrome)

来源:互联网 发布:职业介绍所 知乎 编辑:程序博客网 时间:2024/06/12 01:29

原文链接

http://www.chromium.org/developers/design-documents/gpu-accelerated-compositing-in-chrome

chrome 中集成了webkit,这篇文章对webkit 硬件渲染过程有详细的介绍,很好。

简介
这篇文档讲解chrome硬件加速合成的实现细节和背景。

介绍
通常来讲,网页浏览器完全依赖CPU来渲染网页内容。
随着高性能的GPU日益成为设备(即使是最小的设备)的一部分,以及富媒体(video,3D图形游戏)在网络体验中扮演着越来越重要的角色,
人们的注意力开始转向寻找一些方法来更充分利用底层硬件的能力,以提供更好的体验以及更少的电量消耗。
很明显,如果使GPU直接参与网页内容的合成会带来非常显著地提速。这样做最大的好处是可以省去不必要的大量数据拷贝过程,尤其是将内存中的video数据拷贝到系统内存的过程。
这种加速有两个最明显的用武之地,一个是使用硬件解码的Video元素,一个是WebGL canvas, 这两个元素都把它们的结果数据放在一个GPU无法快速访问的内存区域。
将包含网页内容的各个layer的合成交给GPU来做还有另外一些好处。
就涉及大量像素处理的绘制和合成操作而言,GPU要远比CPU有效率(不管是速度还是绘制能力),这是因为针对这些类型的工作负载,GPU有做专门的设计。
将这些操作交给GPU完成,可以实现GPU和CPU并行运行,从而创造出一个高效率的图形处理管线。
第一部分:webkit 渲染基础及软件渲染路径
webkit 渲染引擎的源码非常的繁杂,而且注释和说明性文档实在是太少了。
为了了解GPU加速在Chrome中是怎么工作的,必须先了解webkit 实现页面渲染的基础组件。
我们先简要回顾一下GPU加入以前的工作流程,再分析GPU加入后的工作流程,以此来理解GPU是怎样起作用的。
Nodes and DOM tree
网页内容在webkit 内部以Node为节点的树形结构存储,称为DOM tree。
网页中的每一个HTML 元素,包括元素之间的text都和一个Node相关联。
DOM tree的最顶层Node 永远是Document Node.
Frome Nodes to RenderObjects
Dom tree中每一个可视化的Node 节点都对应着一个RenderObject.RenderObject 也存储在一棵对应的树结构中,称为Render tree.
RenderObject 知道如何在一个display surface上显示(绘制) Node 节点的内容。
它通过调用GraphicsContext提供的绘制接口来完成显示(绘制)过程。
GraphicsContext最终负责将像素写入一块bitmap,这块bitmap会被显示在屏幕上。
在Chrome中,GraphicsContext 封装了Skia, 2D图形库,对GraphicsContext的大多数调用都转变成对SkCanvas或SkPlatformCanvas的接口调用。
在软件渲染的情况下,整个网页只有一个GraphicsContext,所有的RenderObjects都绘制在同一个GraphicsContext上.
From  RenderObjects to RenderLayers
每一个RenderObject 都关联着一个RenderLayer.这种关联是通过祖先RenderObject 节点直接或间接地建立的。
分享同一坐标系的RenderObject(比如被同一CSS transform属性影响的元素)必然位于同一RenderLayer.
正是由于RenderLayer的存在,网页上的元素才可以按照正确的顺序合成,从而恰当的显示有交叠的内容,和半透明元素等效果。
像在RenderBoxModelObject::requiresLayer() 及RenderBoxModelObject的子类中重实现的requiresLayer()中定义的那样,有很多种条件可以
触发一个特定的RenderObject创建一个RenderLayer.
通常来讲,满足下列条件之一时,RenderObject就会创建RenderLayer:
1.网页的root节点;
2.有明确的CSS position属性(relative,absolute,transform)
3.元素是透明的
4.overflow, alpha mask,或者reflection
5.有css filter(滤镜) 属性
6.有2D加速Context或者3D(webGL)context的 canvas 元素对应的RenderObject.
7.video元素对应的RenderObject
需要注意的是RenderObject和RenderLayer之间并不是一一对应的。 
RenderObject 或者与它所创建的RenderLayer相关联(如果它创建了的话),或者与它的第一个拥有RenderLayer的祖先RenderObject创建的RenderLayer相关联。
RenderLayer 也会形成一个树型层次结构。这个树结构的根节点是与网页的根元素相对应的RenderLayer.每一个RenderLayer 节点的后代都是
包含在父亲RenderLayer内的可视化的RenderLayer.
每一个RenderLayer的子节点都被存储在两个按升序排列的有序表中。
negZOrderList 有序表中存储的子节点是z-index值为负的子RenderLayer,所以这些RenderLayer在当前RenderLayer的下面;
posZOrderList有序表中存储的子节点是z-index值为正的子RenderLayer,所以这些RenderLayer在当前RenderLayer的上面;
Putting it Together: many trees
简言之,在概念上存在着三颗平行的树结构,分别负责渲染过程中的不同目的:
DOM Tree 是我们的基础模型;
RenderObject Tree是由DOM Tree的可视化节点一对一映射而来的,RenderObject知道如何渲染它所对应的Dom Node.
RenderLayer Tree 由RenderObject 映射的RenderLayers组成。这种映射关系是多对一的,因为每一个RenderObject或者与它所拥有(创建)的RenderLayer相关联,
或者与它的第一个拥有RenderLayer的祖先RenderObject的RenderLayer相关联。
RenderLayer Tree负责维护RenderLayers 之间的Z-ordering 顺序。

图 render trees.png



the software rendering path
从根本上说,webkit 通过从根节点开始遍历RenderLayer树结构来渲染页面。
Webkit 代码库中包含两种完全不同的渲染路径。软件渲染路径和硬件加速渲染路径。
软件渲染路径是传统的渲染方式。
在软件渲染方式下,按照从后向前顺序绘制各层RenderLayer来渲染页面
从根节点开始递归遍历RenderLayer层次结构。遍历过程中要完成大量工作。
这些工作是在RenderLayer::paintLayer()中完成的,包括以下一些基本步骤(为了更明晰,简要列在这里):
a.判断当前RenderLayer是否与damage rect 有交集;
b.调用paintLayer()递归绘制有序表negZOrderList中的各层RenderLayer;
c.请求与当前RenderLayer相关联的那些RenderObject 绘制它们自己;
d.这个绘制过程是通过遍历以创建当前RenderLayer的RenderObject为开始节点的Render Tree进行的。当遇到一个RenderObject所关联的RenderLayer与当前RenderLayer不同时,遍历过程便结束了。
e.调用paintLayer()递归绘制有序表posZOrderList中的各层RenderLayer;
在软件渲染方式下,RenderObjects 调用一个共享的GraphicsContext(Chrome中是Skia)提供的绘制接口将自己绘制到一块目标bitmap上。
注意,GraphicsContext 本身并没有Layers概念,但是为了正确绘制半透明的RenderLayer.
这里有一个警告:半透明的层在绘制与它自身相关联的RenderObject之前会先调用 GraphicsContext::beginTransparencyLayer()。
在Skia的实现中,beginTransparencyLayer()这个调用会使接下来的所有的绘制命令都绘在一块单独的Bitmap上。
当前RenderLayer绘制完成后,这块单独的Bitmap会和之前的那块Bitmap合成。当与当前的半透明的RenderLayer相关联的RenderObject都绘制完时候,需要相应的调用GraphicsContext::endTransparencyLayer()。
From WebKit to the Screen
图:software rendering architecture


当所有的RenderLayer都绘制到一块共享的Bitmap上后,就需要将这块Bitmap显示到屏幕上。
在Chrome中,这块Bitmap位于一块共享内存中,对这块Bitmap的控制通过IPC方式交给Browser进程。
Browser进程负责调用系统的窗口API将这块Bitmap绘制到合适的窗口中。
第二部分: 硬件基础
回忆Webkit 代码库中包含两种渲染路径,一种是软件渲染路径,一种是硬件加速渲染路径。
如果你已经了解了软件渲染方式,我们可以开始分析硬件加速渲染方式与软件渲染方式的区别。
正如名字所提示的,硬件加速方式就是利用GPU在合成一些RenderLayer的内容时的加速功能。
硬件加速的相关代码由编译时的宏开关ACCELERATED_COMPOSITING 控制。
当至少有一个RenderLayer要求使用硬件加速时,或者当标志 --forced-compositing-mode打开时, 
Chrome就会使用硬件加速渲染路径。这个标志在android和ChromeOS的Chrome中默认都是打开的。
苹果和大多数iOS上的Safari都采用硬件加速的渲染方式,并且大量运用了苹果的CoreAnimation API.
Introducing the compositor
在硬件加速渲染方式下,一些RenderLayer(不是所有)拥有自己的backing surface(拥有单独的backing surface的renderlayer叫做compositing layers).
拥有Backing surface的RenderLayer在绘制时将它们自身绘制到自己的backing surface上,而不是网页公共的那块bitmap上。
随后的合成过程会将所有的backing surface合成到目标Bitmap上。
我们仍然是从RenderLayer Tree开始,以一块单独的Bitmap结束,但是这种分成两个阶段的方式,允许合成器在per-compositing-layer的基础上做一些额外的工作。
例如,在合成每个compositing layer之前,合成器负责对这个compositing layer所对应的bitmap做一些必要的变换(由css 的 transform 属性值指定)。
而且,由于这些Layer的绘制和合成分开了,这些Layer中的一个如果失效,只会引起这一个Layer中内容的重绘,然后再将这个layer重新合成即可。
相反地,在软件渲染中,如果任何一个Layer的内容失效会引起在它上面和下面的所有Layer的重绘。这些都是CPU的不必要的负担。
More Trees: From RenderLayers to GraphicsLayers
在软件渲染方式下,整个网页只有一个GraphicsContext。
在硬件加速合成的方式下,每个compositing layer 都需要一个单独的GraphicsContext,以便每个Compositing layer 都可以绘制在自己单独的
bitmap上。
前面已经讲过,有一组平行的概念上的树结构,每一个都比前一个更稀疏,而且每一个都对前一个的一个子树负责,这组树结依次为:DOM Tree;RenderObject Tree,和RenderLayer Tree.
在介绍硬件合成时,我们要再加一个概念上的树: GraphicsLayer tree。每一个RenderTree或者拥有自己的GraphicsLayer(如果这个RenderLayer是compositing Layer的话),或者是使用它的第一个拥有GraphicsLayer的祖先节点的GraphicsLayer.
RenderLayer与GraphicsLayer的关系类似于RenderObject与RenderLayer之间的关系。每个GraphicsLayer都拥有一个GraphicsContext,与这个GraphicsLayer相对应的每个RenderLayer都绘制到这个GraphicsContext.上。
理论上讲,每一个RenderLayer都可以将自己绘制到一个单独的backing surface上以避免不必要的重绘。
但是在实际中,这种做法会导致内存的大量浪费。在当前的Webkit 实现中,只有满足以下条件之一,RenderLayer才会拥有它自己的composingLayers.(见RenderLayerCompositor::requiresCompositingLayer()):
layer 有3D或者CSS transform 属性值;
layer是硬解码的video 元素使用的;
layer是拥有3D context或2D加速context的Canvas标签使用的;
layer是一个合成的插件使用的;
layer使用了动画表示它的透明度,或者Layer使用了动画形式的webkit 变换;
layer 使用了加速的CSS 滤镜;
拥有compositing layer后代的layer
渲染在Compositing layer之上的layer
这意味着拥有需要合成的RenderLayer的网页总是通过合成器来渲染。其他网页可能需要也可能不需要合成器来渲染,这取决于标志 --forced-compositing- mode 的状态。
the code 
与合成器相关的代码在WebCore中。由USE(ACCELERATED_COMPOSITING)控制。一部分代码是所有平台共享的,一部分是Chrome平台专用的。
webkit的代码结构允许把chrome平台相关的合成器的实现放在平台相关的源文件中(platform/graphics/chromium),不需要修改webkit的核心代码。
同样的,依据Skia图形库实现的GraphicsContext我们也放在了平台相关的源文件中。
GPU 在哪?
GPU是怎样发挥作用的?为了节省耗时的内存传送,加入了加速的合成器。浏览器的Tab区域的最终渲染都由GPU直接控制。
这种模式与当前的render进程将一块含有网页内容的Bitmap通过IPC和共享内存传给浏览器进程去显示的模式是很不相同的。
在含有硬件加速的架构中,硬件加速Layer(比如,需要合成的 Layer,即那些拥有backing surface的Layer)和网页其余内容的合成过程是通过调用平台相关的3D API在GPU上完成的。
实现这些API的最终代码被封装成一个库,这个库运行在渲染进程中,叫做合成器。
合成器库利用GPU合成网页上的矩形区域(例如所有的compositing layers)到一块单独的bitmap中。这块bitmap中的内容就是最终要显示的网页内容。
架构插曲: GPU 进程
在我们进一步分析合成器生成的GPU 命令之前,有必要先了解一下Render进程是怎样向GPU发送命令的。
在chrome平台的多进程模型中,我们有一个专门的进程负责这项工作:GPU进程。GPU进程的存在主要是出于安全方面的考虑。
受限于沙箱性质,渲染进程(webkit 与合成器都运行在此进程)不能直接调用OS提供的3D API(windows平台是Direct3D,其余平台是OpenGL).
出于这个原因,我们需要一个单独的进程做渲染工作。这个进程就是GPU.GPU进程是专门用来访问系统的3D API.它以client-server模式工作:
客户端(运行渲染进程的代码),并不直接调用系统的3D API,而是将这些命令序列化后放在一个环形Buffer(命令Buffer)中,这块环形Buffer位于client进程与server进程之间的一块共享内存中。
服务端(GPU进程,运行在一个可以直接访问平台的3D API的受限较少的砂箱中)将序列化的命令从共享内存中取出,解析,然后调用合适的
图形命令,结果数据直接输出给窗口。

图 GPU Process


GPU 进程接收到的命令的样式类似于GL ES 2.0 API(比如一个命令对应于glClear,一个命令对应于glDrawArrays).由于大部分GL 命令都没有返回值,
所以client 和server之间可以近乎异步的工作。这使性能负载可以保持在非常低的程度。
客户端和服务端的所有同步,比如客户端需要通知服务端有额外的工作需要做,都由IPC机制来控制。
还有一点需要注意的是,共享内存除了存储命令外,还用来在客户和服务器之间传送更大的源数据,比如bitmap for textures,vertext

这里略掉一部分没有翻译
第三部分:硬件渲染模式下,利用合成器渲染
合成器的实现建立在GL ES 2.0的客户端库之上,这个库将图形接口调用代理给GPU 进程(用上文解释的方式)。
当一个网页使用合成器渲染时,它的所有像素都是通过GPU 进程直接绘制到窗口上。
合成器维护着GraphicsLayers的一个层次结构,这个层次结构是通过遍历RenderLayer Tree以及随着页面变化的更新建立的。
除了WebGL和Video Layers,每一个GraphicsLayer的内容都是先绘制到一块系统内存的Bitmap上的(与软件渲染方式中的过程相同):
每一个RenderLayer都请求与它相关联的RenderObject 绘制自身到与当前RenderLayer相关联的GraphicsLayer的GraphicsContext上,即绘制在了
这个GraphicContext相关联位于系统共享内存中的一块Bitmap上。这块Bitmap随后会传给GPU 进程(利用GPU进程一节中介绍的资源传送机制),
接下来,GPU 进程就把这块Bitmap作为一个textue上传给GPU.
合成器跟踪从最近一次被绘制后有变化的GraphicsLayer,只更新与变化的GraphicsLayer相对于的texture.
当所有的texture都上传给GPU之后,渲染页面内容就变成了深度优先遍历GraphicsLayer层次结构并发送GL 命令为之前上传以texture上传给GPU的GraphicsLayer 绘制texture quad.
A texture quad 是屏幕上的用给定的Texture填充的四边形(在我们的例子里,就是GraphicsLayer的内容)。
需要注意的是,深度优先遍历需要确保GraphicsLayer的正确的z-ordering顺序。与每一个GraphicsLayer相关联的RenderLayer的Z-ordering序是在较早的时候RenderObject被光栅化到texture中时予以保证的。
图: compositing with the gpu process

the code 
chrome 平台的合成器代码的实现在webcore下的目录 platform/graphics/chromium中。
合成器的逻辑大部分在LayerRendererChromium.cpp文件中。
各种composited layer的实现分别在文件 {Content|Video|Image} LayerChromium.cpp中。
第四部分:优化!平滑渲染
现在我们已经大致知道怎样用合成器渲染一个页面:网页内容被分布在各个Layerzhong ,Layer被光栅化到textures中,textures被上传给GPU.
合成器告诉GPU将所有的textures合成为最终的屏幕图像。
接下来理解怎么在一秒钟内做60次这些事情,以确保动画,滚动,以及其他的页面交互能平滑进行。
为了解释这些,我们需要介绍与渲染技术的优化相关的一些概念。
Damage:
目前为止,我们只介绍了如何渲染整个页面。这只是Webkit在页面加载完后第一次渲染页面时做的事情。
但是,更典型的情况是,用户与页面交互时只有部分页面内容发生了变化,这时就需要进入damage rect.
当网页内容的可视化部分发生变化时(比如 js 改变了css style,css 动画在运行,或者用户滚动了viewport),webkit 会跟踪网页中需要更新
的部分。跟踪的结果是用一个damage rectangle 记录需要重绘的网页区域。
当绘制时,我们遍历RenderLayer只绘制与damage rectangle 有交集的Layer,跳过与damage rectangle完全没有重叠的Layers.
这就避免了网页中任何一部分变化时都需要重绘整个网页,明显会优化性能。