Chrome中的硬件加速合成


概述

  本文主要讲述了硬件加速合成在chrome中的实现细节以及使用背景。

 

引言

   以前的浏览器都使用CPU来渲染整个网页的内容。 随着硬件加速的发展,一些小型设备也具备硬件加速的能力,同时视频和3d图像等富媒体在网页中扮演的角色越来越重要,人们正在考虑通过充分利用底层硬件技术来提升渲染性能和节省CPU。毫无疑问,通过GPU可以大大提高网页的渲染速度。使用硬件加速最大的好处是可以减少大内存块的拷贝(因为这种拷贝非常慢),特别是在显存和系统内存之间的拷贝。使用硬件加速优化以后,最大的受益者是使用硬件解码的<video>标签和WebGL canvas,因为两者都会产生CPU不能快速访问的内存区。使用GPU来代理网页的合成也带来了其它好处。通常情况下,GPU在绘制或者合成大量像素数据时的效率比CPU高很多,因为GPU本来就是为绘制和合成大量像素数据而专门设计的硬件。利用GPU来创建一条高效的图形管线,使得CPU和GPU可以并行工作。

 

第一部分 webkit渲染基础以及软件渲染

 webkit渲染引擎的源代码是非常多和复杂(而且基本没有说明文档)的。为了理解GPU加速在chrome中的作用,必须首先理解webkit网页的构建流程。为了理解GPU带给chrome带来的改变,我们首先要了解一下不包含GPU的时候,chrome是怎样工作的。

 

节点(node)和DOM树

  在webkit中网页的内容被存储在一棵DOM树中。每一个html元素和元素之间的文本都对应一个节点。我们习惯把DOM树的根节点叫做Document。

 

从nodes(DOM节点)到RenaderObjects

  DOM树中的每一个可视节点都对应一个RenaderObject。RenaderObject被存储在一棵与DOM树对等的渲染树中。Renderobjec负责把Dom节点的内容绘制在界面上。Renderobjec向GraphicsContext发送绘画请求。GraphicsContext最终会把像素数据写到一张能显示在屏幕上的位图中。在chrome中GraphicsContext封装了Skia(一个2d画图库),大部分GraphicsContext的调用都转化为SkCanvas和SkPlatformCanvas的调用。

         在软件渲染中,整个网页只有一个GraphicsContext,所有的Renderobject共享一个GraphicsContext。

 

从RenaderObjects到RenderLayers

         每个RenderObject要么直接与自己的RenderLayer相关联,要么间接的和祖先节点的RenderLayer相关联,该祖先节点与该节点最接近且拥有自己的Rendelayer。

         通常情况下共享一个坐标空间的RenderObject属于同一个ReaderLayer。正式由于RenderLayer的存在,界面上的元素只要按照正确的顺序被合成就能够显示重叠,半透明等效果。只有某几种特殊的RenderObject才会创建一个独占的ReaderLayer。创建RenderLayer的源代码放在方法RenderBoxModelObject::requiresLayer()中,该方法会被子类重写。满足下列条件之一的ReanderObject都会创建一个独占的RenderLayer。

a)      RenderObject树的根节点

b)      拥有显示CSS位置特性的节点(相对位置,绝对位置和坐标转换)

c)      有透明效果的对象

d)      节点有溢出,alpha mask和反射等效果

e)      CSS过滤器

f)       Canvas 2D和3D (WebGL)

g)       Video节点对应的RenderObject对象

 

注意,RenderLayer节点和RenderObject节点不是一一对应关系,而是一对多的关系。一些特殊的RenderObject会拥有自己的RenderLayer,另外的RenderObject会被关联到祖先节点的RenderLayer,该祖先节点跟它最接近。

 

         RenderLayer也生成了一棵树。RenderLayer树的根节点对于整个网页的根节点。RenderLayer树中的每个子节点都显示在父节点中。每个RenderLayer的子节点被按照升序保存在negZOrderList和posZOrderList两个有序列表中。negZOrderList保存了z轴坐标为负数的子节点,这些节点显示在下面。posZOrderList与negZOrderList相反,z轴坐标为正,这些节点显示在上面。

 

把三棵树放一起

总结一下,在概念上形成了三颗对等的树,他们都在渲染过程中起到了不同的作用。


a)  DOM树,是最本的数据模型

b)  RenderObject树,它的节点与DOM树的可见节点一一对应。RenderObject负责绘制它对应的节点

c)  RenderLayer树,是通过RenderObject映射到RenderLayer生成的。RenderObject和RenderLayer是多对一的关系,RenderObject要么和自己的RenderLayer相关联要么与祖先的RenderLayer相关联,该祖先与它最接近。

 

软件渲染

         Webki的渲染基本是从RenderLayer树的根节点开始遍历的。Webkit的代码中包括两种渲染方式,即软件渲染和硬件加速合成渲染。软件渲染是传统的渲染方式。

         在软件渲染中,网页的渲染是通过从后往前绘制RenderLayer的。从RenderLayer树的根节点开始遍历,大量的绘制工作都是在方法RenderLayer::paintLayer()中完成的,该方法执行了下面的步骤(为了描述清晰,这些步骤都是精简的)

a.      决定是否需要删除一些重叠的区域;

b.      递归绘制显示在下面的RenderLayer,分别调用negZOrderList集合中各个Rendelayer对象的方法paintLayer();

c.      绘制在该RenderLayer上的RenderObject

d.       在递归遍过程中只要发现RendeObject有自己独占的RenderLayer,递归将被终止。不管什么时候只要发现与RenderObject关联的RenderLayer与当前的RenderLayer不相同则终止遍历

e.      递归绘制显示在上面的RenderLayer,分别调用negZOrderList集合中各个Rendelayer的方法paintLayer();

在这种模式下,RenderObject通过向不断的向一个共享GraphicsContext(在chrome中是通过Skia实现)中发送绘制请求来将自己绘制在一张共享位图中。

 

注意GraphicsContext本身是没有层的概念,但是为了绘制半透明效果必须做一些特殊的处理:半透明RenderObject在绘制之前必须先调用方法GraphicsContext::beginTransparencyLayer()。在Skia的实现中,在调用beginTransparencyLayer()之后的所有绘画会被渲染进一张单独的位图中, 可以通过调用endTransparencyLayer()来结束绘制,当半透明的RenderObject绘制完成以后这张半透明位图会和原来的位图进行合成。

 

从webkit到屏幕


Software Rendering Architecture

 

         当所有的RenderLayer被绘制到一张共享位图以后,这张图片还需要在屏幕中显示。在chrome中,位图被存储在共享内存中,并且通过ipc控制消息传递给Browser进程。最终browser进程通过调用操作系统的窗口api,将位图显示在对应的窗口上。(例如在windows下位图被绘制在HWND上)

 

第二部分,硬件基础

         上面提到过webkit可以通过软件渲染和硬件加速合成来网页的内容。现在,你已经理解了软件渲染,接下来我们将解释一下硬件加速合成与软件渲染的不同之处。

         正如其名,硬件加速是利用GPU加速来合成网页的内容。当编译代码时设置了标记ACCELERATED_COMPOSITING,webkit就会带有硬件加速合成的能力。

         只要网页中有某个RendeLayer需要硬件加速或者标志--forced-compositing-mode被打开,那么chrome中就会使用硬件加速。在Android和ChomeOs中该标记位是默认打开的。Chrome在其它平台上也是如此。Mac和IOS上的Safari都实现了硬件加速,因此Safari浏览器中有大量的核心动画API可供外部调用。

 

合成器的引入

         在硬件加速路径中,有些RenderLayer会有自己的后端界面(拥有后端界面的Renderlayer一般称它为合成层(compositing layer)),RenderLayer会把自己绘制进后端界面而不是绘制在整个网页共享的一张位图中。最后会将所有的后端界面合成到一张最终的位图。虽然我们开始于RendeLayer树,结束于一张位图,但是将渲染分成两个阶段使得我们可以在每一个合成层上做一些额外的工作。

         例如,合成器可以在合成层被合成到最终位图之前做一些CSS转换。由于绘制和合成是分开的,如果其中一个层失效,只要重新绘制有影响的层,而不影响其它层。

         相反,在软件渲染路径中,任何层失效都需要重绘所有的层(至少需要绘制与它有重叠关系的层)。

 

再引入一棵树:从RenderLayers树到 GraphicsLayers树

         上面提到过在软件渲染方式中整个网页只有一个GraphicsContext。在硬件加速合成中,每一个合成层都需要一个GraphicsContext以至于每个层都能够将自己绘制到一个单独的位图中。上面我们已经提到过几种对等的树,后面的树一般都比前面的树要稀疏。DOM树,RenderObject树和RenderLayer树。在引入合成层以后,我们需要增加另外一棵概念树:GrahpicisLayer树。每个RenderLayer要么有自己的GrahpicisLayer(合成层),要么跟它的祖先共享一个GrahpicisLayer。这种关系跟RenderObjec和RenderLayer的关系很像。每一个GrahpicisLayer都有一GrahpicisContext,用于保存相关RenderLayer的绘制内容。

         从理论上来说为了避免不必要的重绘,最好为每个RenderLayer提供一个后端界面,但是实际上这是非常浪费内存的。在当前的chrome实现中,下列条件都会引起RendeLayer创建自己的GrahpicisLayer(合成层)。

a.      进行3D或者透视变换的CSS属性

b.      使用硬件加速视频解码的<video>元素

c.      具有3D(WebGL)上下文或者硬件加速的2D上下文的<canvas>元素

d.      组合型插件(即Flash)

e.      具有有CSS透明度动画或者使用动画式Webkit变换的元素

f.       具有硬件加速的CSS滤镜的元素

g.      子元素中存在具有组合层的元素的元素(换句话说,就是存在具有自己的层的子元素的元素)

h.      同级元素中有Z索引比其小的元素,而且该Z索引比较小的元素具有组合层(换句话说就是在组合层之上进行渲染的元素)

这就意味着只要网页某个RenderLayer需要合成层则整个个网页只能通过合成器来渲染。其它网页是否需要合成器来渲染取决于标志位--forced-compositing-mode。

 

源码

         与合成器相关的源代码在WebCore中。大部分代码在所有平台上都是一样的,但是有一部分只在chrome中使用。幸亏,webkit的代码结构很好以至实现合成器并不需要修改webkit的核心代码,所有与chrome合成器相关的代码都放在路径platform/graphics/chromium下。这样的做法与GraphicsContextSkia很相似, GraphicsContextSkia是基于Skia实现的GraphicsContext。

 

GPU有什么用?

         GPU起了什么作用呢。加速合成器加入以后,减少了很多耗时的内存传递,最终的渲染区域直接由GPU来处理。这与当前的模式(在render进程中把数据绘制到一块共享内存中然后通过ipc告诉browser进程显示在相应的窗口系统中)存在很大的区别。

         在硬件加速体系架构中,是通过调用平台相关的3D API,来将加速层(带有后端界面的层)和网页的其它内容进行合成的。产生这些调用的代码最终被封装在合成器(Compositor)库中,这个库运行在Render进程中。合成器(Compositor)库最终还是使用GPU将网页上的各个矩形区合成到一张最终位图。

 

加入GPU进程以后的体系架构

         在我们继续学习合成器生成的GPU命令之前,我们有必要了解一下Render进程如何发送GPU命令。在chrome的多进程模型中,有一个专门的进程负责处理GPU命令。这样做主要是出于安全的考虑。

         Renderer进程(webkit和合成器的代码都是运行在该进程中)被限制在沙箱中所以不能直接调用操作系统提供3D API(我们在windows下使用direct3d,在其他操作系统则使用OpenGL)。

出于安全的原因我们使用了一个单独的进程来做渲染。 我们把这个进程叫做GPU进程。GPU进程就是为了在渲染沙箱中访问系统的3D API。它的工作原理是按照客户-服务器模型设计的:

         客户端(代码运行在Renderer进程中),它将命令序列化以后放入与服务端的共享内存中,而不是直接调用系统api。

         服务端(GPU进程运行在一个能调用系统3DAPI的沙箱中),它从共享内存中获取命令,然后执行对应的图像调用,最后显示在窗口系统上。


The GPU Process

         GPU进程收到的命令与GL ES2.0 API(例如有这样的命令glClear, glDrawArrays等)非常的相近。因为大部分的GL调用都没有返回值,所以客户端服务端模型可以使用异步的方式工作,异步调用对性能的影响是非常小的。在客户端和服务端中需要用到的任何同步调用(客户端通知服务器还有任务需要做)都用IPC的同步机制处理。值得一提的是,需要为命令缓冲区提供额外的共享内存。通过共享内存可以在客户端与服务端传递纹理位图,顶点数组等。

         对于客户端应用程序可以通过两种方式与服务端进行通信,一种是直接往命令缓冲区里面写数据,另一种是使用一套已经封装好的客户端库GL ES 2.0 API。因为方便,所以合成器和WebGL都使用了GL ES 2.0 API客户端库。对于服务端而言,从命令缓冲区收到命令以后需要被转换成平台相关的调用,在Mac, Linux/ChromeOS,和android上使用OpenGL,在windows下使用Direct3D。

         现在,chrome中每个browser进程只有一个GPU进程,供renderer进程和plugin进程调用。在GPU进程中,一个线程需要处理很多个命令缓冲区,每一个缓冲区对应一个渲染的上下文。

         GPU进程架构带来以下好处,包括:

a.      安全性:大部分的渲染逻辑仍然在沙箱进程Renderer中进行

b.      健壮性:GPU进程Crash不会导致Browser进程的异常退出

c.      一致性:因为渲染使用了标准的 OpenGL ES 2.0,所以不需要过多的当心chrome的跨平台问题。

d.      并发性: Renderer进程可以快速的发送命令道命令缓冲区,然后由GPU负责处理他们,Render进程继续接着做与CPU相关的渲染活动。幸亏有了管道,使得我们能够在多核机器上好好利用这两个进程以及GPU和CPU可以同时工作。

 

通过上面的分析,现在我们可以回过来分析一下Renderer进程中的合成器怎样生成命令和资源的。

 

第三部分,硬件路径中的渲染合成器

         合成器是通过调用GL ES2.0的接口实现的,该库代理了对于GPU进程的图形调用(前文已经解释过了)。

         当使用合成器渲染网页的时候,所有的像素数据都通过GPU进程直接绘制到窗口中。合成器中保存了一棵GraohicsLayer树,这棵树是通过遍历RenderLayer树得到的,当网页发生变化的时候这棵树也会随着变化。除了WebGl和视频层之外,其它层都会首先将自己绘制进一张位图中(跟软件渲染差不多):每一个RenderLayet会让相关的RenderObject绘制自己到一张保存在共享内存的位图中。这张位图会被传递给GPU进程(上面已经提到过的资源传输机制)然后GPU进程将该位图当作一张纹理上传到GPU。合成器记录着所有GraphicsLayer是否被修改过,因此可以按需上传修改过的纹理。

         一旦所有的纹理被上传到GPU,渲染网页内容将是非常简单的。只要按照深度优先遍历GraphicsLayer树然后让将之前上传的纹理绘制成矩形。这个矩形会被相应的纹理填满。

         注意,做深度优先搜索主要是为了保证层在z轴上的准确显示,在光栅化(从几何数据变为像素数据的过程)RenderObject之前一定确保它所属的RenderLater已经被绘制。



Compositing with the GPU process

 

源代码

         在chrome中实现合成器的代码在WebCore的 platform/graphics/chromium目录下。合成逻辑大部分在文件LayerRendererChromium.cpp中,在文件LayerChromium.cpp中则实现了不同的合成层{内容|图片|视频}。

 

第四部分 优化!平滑渲染

现在我们已经大概知道使用合成器绘制一个网页的大致流程:

a.      网页被拆分成多个层

b.      每个层被光栅化到纹理中

c.      纹理被上传到GPU

d.      合成器让GPU把所有上传的图片放在一起形成一张屏幕能显示的图片

 

接下来我们将了解一秒60次的动画,滚动条和其它界面交互是怎样平滑过渡的。为了理解它,我们需要引入几个用于优化渲染技术的概念。        

 

失效(因为用户的操作或者程序的修改,而需要重新绘制的区域)

         到目前为止,我们已经分析了怎样绘制一个完整的网页。大概知道第一次加载网页的时候,webkit怎样绘制网页。一般情况下用户只与网页的某个部分交互。引入矩形损坏。

         当网页的可视部分发生变化(js修改了CSS样式,运行中的CSS动画或者滚动条都有可能引起显示内容发生变化)的时候webkit是知道哪一部分需要被更新。在网页中会生成一个需要被重绘的失效矩形。当重绘时,我们遍历RenderLayer树,当发现RenderLayer与失效矩形重叠时,我们只重绘与失效矩形重叠的区域,如果RendeLayer与失效矩形不重叠则直接忽略。这样做的话,当网页发生任何变化时,我们没必要重绘整个网页,如此的话能够显著提高性能。

 

分块

         上面我们提说过在软件渲染中,网页上的所有内容都被绘制进同一张位图(保存在内存中),所以我们可以轻易地修改这张位图的任一区域。在硬件加速合成中是很困难的,首先将RenderLayer绘制到一张位图中,然后再将该位图作为一个纹理上传到GPU。RenderLayer有可能非常大:他的大小主要取决于整个网页的大小。我们不能让纹理太大,否则可能会超过GPU所允许的纹理最大值,即使不超过也会占用大量的显存。绘制和上传一张巨大的纹理会花费相当长的时间。

         解决纹理过大这个问题的一种解决方案是只绘制和上传可见部分。也就是纹理绝不可能不显示窗口还大,这样可以规避纹理过大的纹理。但是这又引起新的问题,每次显示窗口发生变化我们都必须重新绘制和上传纹理。这将导致速度非常慢。

         通过分块可以完美解决这个问题。每一个层会被切割为几个256*256的小块。我们只绘制和上传GPU需要的块。我们把这些块保存在起来:只需要上传可见的和被用户操作的块。而且,我们可以通过不上传被遮住的块来优化渲染。一旦所需的块上传完成,我们就可以将这些块合成到一张图片中。

         纹理分块带来的好处是:纹理流。当有一个很大的纹理需要被上传时,为了节省内存带宽我们只需要上传所需的几个小块。对于一个很长的纹理GPU不用一直等着:因为上传的内容被拆分成了许多块。

 

案例分析:软件渲染和硬件加速合成中滚动条的实现

        

 

第五部分 不只是在合成的时候使用GPU

 原文 

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值