随着HTML5不断加入图形和多媒体方面的功能,例如Canvas2D、WebGL、CSS 3D和视频等,对渲染引擎使用图形库的性能提出很高的要求,本节描述WebKit为了支持硬件加速机制而引入了哪些内部结构以及chromium如何在这些设施上实现特殊的硬件加速机制,这些机制的引入提高了WebKit引擎的渲染性能
硬件加速基础
概念
硬件加速是指实用哦该GPU的硬件能力来渲染网页,GPU的主要作用是用来绘制3D图形并且有很好的性能,对于GPU绘图而言,通常不像软件渲染那样只是计算其中更新的区域,一旦有新的更新请求,如果没有分层,引擎可能会重新绘制所有的区域,因为计算更新部分对GPU来说可能耗费更多的时间,当网页分层之后,部分区域的更新可能只在一层或几层,而不需要更新整个网页,通过重新绘制网页的一个或几个层,并将它们和其他之前绘制完的层合成起来,既能使用GPU的能力,又能够减少重绘的开销。在理想情况下每个RenderLayer对象和最终显示出来的图形层次一一对应,也就是每个RendrLayer对象都有一个后端存储,这样带来的好处是当某一层有更新的时候,WebKit重绘该层的所有内存,在现实情况下由于硬件能力和资源有限,往往达不到这点。因此为了节约资源,硬件加速机制在RenderLayer建立好了之后需要满足三件事情来完成网页的渲染:
- WebKit决定将哪些RenderLayer对象组合在一起形成一个有后端存储的新层,这一新层用于之后的合成,被称为合成层,每个新层都有一个或者多个后端存储,后端存储可能是GPU的内存,对于一个RendrLayer对象,如果没有后端存储,那么就使用它父亲的合成层
- 将每个合成层包含的这些RendeLayer内存绘制在合成层的后端存储中
- 由合成器将多个合成层合成起来,形成网页的最终可视化结果,实际就是一张图片,其中合成器是一种能够将多个合成层按照这些层的前后顺序,合成层的3D变形等设置而合成一个图片结果的设施
WebKit硬件加速设施
如果一个RenderLayer对象需要后断存储,它会创建一个RenderLayerBacking对象,该对象负责RenderLayer对象所需要的各种存储,如果一个RenderLayer对象被WebKit按照一定的规则创建了后端存储,那么该RenderLayer被称为合成层。每个合成层都有一个RenderLayerBacking,RenderLayerBacking负责管理RenderLayer所需要的后端存储,因为后端存储可能需要多个存储空间,在WebKit中,存储空间使用GraphicsLayer类来表示,类的主要关系如下:
GraphicsLayer表示RendeLayer中前景层、背景层所需要的一个后端存储,每个GraphicsLayer都使用一个GraphicsLayerClient对象昂,该对象能够收到GraphicsLayer的一些状态更新信息,并且包含一个绘制GraphicsLayer对象的方法,RendeLayerBacking继承与该类,GraphicsLayer是WebKit的基础类,主要定义一些接口,在WebKit不同的移植中有不同的子类和实现,如果一个RenderLayer具有以下特征之一,那么它就被称为合成层:
- RendLayer具有CSS 3D属性或者CSS透视效果
- RenderLayer包含的RenderObject节点表示的是使用硬件加速视频解码技术HTML5的“video”元素
- RenderLayer包含的RenderObject节点表示的是使用硬件加速的Canvas 2D元素或者WebGL技术
- RenderLayer使用了CSS透视效果或者CSS变换的动画
- RenderLayer使用了硬件加速的CSS Filter技术
- RenderLayer使用了剪裁或者反射属性,并且它的后代中包括一个合成层
- RenderLayer有一个z坐标比自己小的兄弟节点,并且该节点是一个合成层
这么做有如下几个原因:1 合并一些RenderLayer层可以减少内存的使用量;2 合并之后可以尽量减少合并带来的重绘性能和处理上的困难;3 对于那些使用单独层能偶显著提高性能的RenderLayer对象,可以继续使用这些好处。下图描述了RenderLayer树、RenderLayerBacking对象和GraphicsLayer树这些硬件加速基础设施的对应关系,对于每个RendeLayerBacking对象至少需要一个GraphicsLayer对象:
RenderLayerBacking对象包含了一下各种GraphicsLayer对象层:
RenderLayerCompositor类负责管理这些合成层,它不仅计算和决定哪些RenderLayer对象是合成层,而且为合成层创建GraphicsLayer对象,每个RenderView对象包含一个RenderLayerCompositor,这些对象仅在硬件加速机制下才会创建,RenderLayerCompositor类本身也类似与一个RenderLayerBacking类,它也包含一些GraphicsLayer对象,这些对象对应整个网页所需要的后端存储。
硬件渲染过程
给出书中代码,该网页包含了很多HTML5的功能,需要硬件加速机制才能够渲染,这其中的CSS 3D变形、WebGL和Video等都是HTML5引入的新特性,必须用来GPU硬件加速才能达到比较好的效果。
<html>
<style>
div {
-webkit-transform:rotateY(10deg);
}
</style>
<body>
<p>GPU test</p>
<div>css 3d transform</div>
<canvas id="webgl" width="80" height="80"></canvas>
<video width="400" height="300" controls="controls">
<source src="test.ogg" type="video/ogg">
</video>
<script type="text/javascript">
var canvas = document.getElementById("webgl");
var gl=canvas.getContext("experimental-webgl");
gl.clearColor(0.0, 1.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
</script>
</body>
</html>
首先是看WebKit是如何确定并计算合成层的,下图描述了WebKit如何决定哪些层是合成层并为它们分配后端存储的过程,图中主要包含两个部分,都是RenderLayerCompositor类的函数,一个检查RenderLayer对象是否是合成层,如果是的话为它们创建后端存储对象RenderLayerBacking;二是更新重新更新的合成层来更改合成层树,并修改后端存储的一个设置信息,除了下图所述,当RenderLayer对象被创建时,网页还有一些其他情况也可能需要创建RenderLayerBacking对象,具体过程由RenderLayerModelObject::styleDidChanged()函数调用RenderLayer::styleChanged()函数来触发,然后WebKit调用RenderLayerCompositor::updateLayerCompositingState()函数为RenderLayerModelObject对象所在的RenderLayer层创建后端存储对象。
下图描述了WebKit为上述代码建立的合成层和合成层对应的RenderLayerBacking对象,根据前面的解释,WebKit为网页中的5个DOM节点创建RendeLayer对象,分别是HTMLDocument对象、HTMLHtmlElement对象、HTMLDivElement对象、HTMLCanvasElement对象和HTMLVideo对象,当HTMLHtmlElement对象对应的RenderLayer没有自己的RenderLayerBacking对象,因为该RendeLayer对象不满足之前所述的规则:
其次,WebKit需要遍历和绘制每一个合成层,每个合成层可能有一个或多个RenderLayer对象,这可能包含至少四种情况,第一种HTMLDocument节点,WebKit绘制该节点所在的合成层需要遍历两个RenderLayer对象所包含的子树,与其他绘制内容的调用过程类似,该合成层需要一个用于2D图形的图形上下文对象,该对象的内部实现由各个移植来决定,该层的调用过程如下所示,该过程与软件渲染类似,只是递归过程稍微不同:
在软件渲染过程中,paintLayer函数被递归调用,也就是从RenderLayer根节点开始,知道所有的RenderLayer对象都被递归遍历为止,在硬件加速机制机制中由于引用了合成层的概念,每个RenderLayer对象都被绘制到祖先链中最近的合成层。
第二种使用CSS 3D变形的合成层,第三种使用WebGL技术的canvas元素所在的合成层,它的绘制由JavaScript操作来完成,并且使用了3D的图形上下文,第四种是类似使用了硬件加速的视频元素所在的合成层,该层的内容由视频解码器来绘制,而后通过定时器或其他通知机制来告诉WebKit该层的内容已经发生改变,需要重新合成。
3D图形上下文
WebKit中的3D图像上下文主要是提供一些抽象接口,这组接口能够提供类似OpenGLES的功能,其主要目的是使用Opengl绘制3D图形,这一层抽象能够将WebKit各个移植的不同部分隐藏起来,WebCore只是使用统一的抽象接口,在WebKit中,3D图形上下文的主要用途是WebGL:
图中GraphicsContext3DPrivate是一个跟WebKit各个移植相关的类,虽然在各个移植中都是使用该名称,但是每个移植的定义不同,它主要是针对移植的不同来实现的,PlatformGraphicsContext3D类是WebCore用于创建Surface等对象的参数,其名字是一致的,但是每个移植的定义是不一样的。GraphicsContext3D中的接口有三种类型,第一类是所有移植共享实现的接口,如texImage2DResourceSafe;第二类是一些移植能够共享实现的接口,如texImage2D,它们可以直接调用OpengGL或者OpengGL ES的应用编程接口;第三类跟每个移植具体相关,如platformGraphicsContext3D。这些移植相关的类需要各个移植去实现,否则这一机制就不能工作。
Chromium硬件加速机制
GraphicsLayer的支持
GraphicsLayer对象是对一个渲染后端存储中某一层的抽象,同众多其他WebKit所定义的抽象类一样,在WebKit移植中,它需要具体的实现类来支持该类所要提供的功能,下图描述了从WebCore的同移植无关的GraphicsLayer到WebKit的Chromium移植,再到Chromium游览器所设计的Chromium合成器的LayerImpl类这个过程,其中间有好几层,原因在于抽象和合成机制的复杂性,以及性能等多方面的考虑:
- GraphicsLayerChromium:GraphicsLayer的子类,实现GraphicsLayer需要的一个功能,并且加入了Chromium所需信息
- WebLayer:WebKit的Chromium移植的抽象接口类,它被GraphicsLayerChromium等调用,主要目标是将Chromium的实际后端存储抽象出来,以便WebCore使用它们
- WebLayerImpl:WebLayer的实现类,具体作用是将合成器的层能力暴露出来,跟Layer类一一对应
- Layer:合成器的层表示类,是chromium合成器的接口类,用于表示合成器的合成层,它会形成一棵合成树
- LayerImpl:同Layer对象一一对应,是实际的实现类,包含后端存储,可能跟Layer树在不同的线程
框架
在Chromium中,所有使用GPU硬件加速的操作都是由一个GPU进程负责完成的,这其中包括使用GPU硬件来进行绘图和合成,Chromium是多进程架构,每个网页的Render进程都是将之前介绍的3D绘图和合成操作通过IPC传递给GPU进程,由它来统一调度并执行。WebKit定义了两种类型的图形上下文,它们都可以使用GPU来加速,加速机制最后都是调用OpenGL/OpengGLES库,3D和2D图形上下文在Chromium中分别对应Chromium的3D图形上下文实现和Skia画布(canvas),它们在调用(GL操作)之后会被转换成IPC消息传给GPU进程,该进行中的解释器对这些消息进行解释后调用GL函数指针表中的函数,这些函数指针是从GL库中获取的,流程如下图:
下图以Chromium的3D图形上下文为例剖析中间的流程:
- WebGraphicsContext3DCommandBufferImpl:继承自WebKit::WebGraphicsContext3D类,是WebKit的Chroimum移植中的PlatformGraphicsContext3D类,这个类主要转接自WebKit的调用到Chroimum的具体实现,同时将这些3D图形操作调用转换成GL命令,主要包括一个RenderGLContext对象
- RenderGLContext:Render进程对GLContext的一个封装,包括所有用于跟GPU进程交互的类,有一个GLES2Implementation对象、一个CommandBufferProxy对象和一个GPUChannelHost对象
- GLES2Implementation:该类模拟OpenGL ES的编程接口,但是不直接调用GLES2的实现,而是将这些调用转换成特定格式的命令存入CommandBuffer中
- CommandBufferHelper:该类是一个辅助类,包括一个CommandBuffer代理类和一个共享内存
- CommandBufferProxy:CommandBuffer的一个代理类,实现CommandBuffer的接口,用于和GPUCommandBufferStub之间通信
- GPUChannelHost:用于传递GL命令的IPC消息辅助类
GPU中:
- GPUChannel:用于接收GL命令并发送回复的辅助类
- GPUCommandBufferStub:CommandBuffer的桩,接收来自与CommandBufferProxy的消息,将消息交给CommandBufferSerivce处理
- CommandBufferService:CommandBuffer具体的实现类,但它并不具体解析和执行这些命令,而是当有新的命令到达时,调用注册的回调函数来处理
- GPUScheduler:负责调度执行CommandBuffer的命令,它会检查该CommandBuffer是否应该被执行,并适时将命令交给CommandParser类处理
- CommandParser:仅检查CommandBuffer中的命令头部,其余部分交给具体的命令解码器来解释,所以它同GL命令的理解是独立的
- GLES2DecoderImpl:针对GLES命令的命令解释器,它解析每条具体的命令并执行调度GL相应的函数
- GL Implementation Wrapper:一组GL相关的函数指针,通过设定的3D图形库来读取库中相应的函数地址
- GL Libraries:具体的函数库,在Chromium中可以设置为OpenGL、OpenGLES、Mesa GL、Mock、ANGLE等,得益于设计上的灵活性,不同的3D图形库都可以被Chromium所使用而不需要修改任何代码
GPU进程处理一些命令后会向Render进程报告自己当前的状态,Render进程通过检查状态信息和自己的期望结果来确定是否满足自己的条件,GPU进程最终绘制的结果不再像软件渲染那样通过共享内存传给Browser进程,而是直接将页面的内存绘制在游览器的标签窗口中。
命令缓冲区
命令缓冲区(Command Buffer)主要用于GPU进程(GPU服务端)和GPU的调用者进程(GPU客户端进程,如Render,Pepper,Browser等)传递GL操作命令,从接口上讲,这一设计只提供一些基本的接口来管理缓冲区,它并没有对缓冲区的具体方式和命令的类型进行限制,目前chromium只有GLES一种实现方式,现有的实现是基于共享内存的方式来完成的,因此命令是基于GLES编码成特定的格式存储在共享内存中的,共享内存采用了环形缓冲区(RingBuffer)的方式来管理,这表示共享内存可以循环使用,旧的的命令会被新的命令覆盖。一条命令被分成两个部分:命令头和命令体,命令头是命令的原数据信息,包含两个部分:一个是命令的长度,一个是命令的标识,命令体包含该命令所需要的其他信息,例如命令的立即操作数,命令的长度是可以变化的,具体长度取决于该命令,下图展示了命令结构:
命令缓冲区本身没有定义具体的命令格式,因此GLES实现可以根据自己的需要来定义,GLES实现所使用的命令可以大致分成两类:第一类是基本命令,主要用来操作桶(Bucket)、跳转、调用和返回等指令;第二类跟GLES2函数相关的命令,用来操作GLES2的函数。命令本身是保存在共享内存中的,每条命令的长度不能超过(1<<21 - 1),另外共享内存的大小也是固定的,如果命令太长,可存储的命令就很少,对于这样的数据,chromium可以对它们使用独立的共享内存来实现,典型的命令例如TexImage2D(传输大量数据到GPU内存),但是,当共享内存大小超过系统的限制,这种方式就不可取,chromium提供了一种新的机制来解决这个问题,这个机制被称为桶(Bucket),原理是通过共享内存机制来分块传输,而后把分块的数据保存在本地的桶内,从而避免申请大块的内存,前面提到的公共命令就是用来处理桶相关的数据,当数据传输完成之后,对该数据进行操作的命令就开始执行,桶机制也可以用来传输字符串类型的变长数据:接收端首先获取桶内字符串的长度,然后通过共享内存的方式来分块传输,最后合并在接收端的桶内。
Chromium合成器
架构
合成器的作用就是将多个合成层合成并输出一个最终的结果,所以它的输入是多个待合成的合成层,每个层都有一些属性(3D变形等),它的输出就是一个后端存储,例如一个GPU的纹理缓冲区。Chromium合成器是一个独立且复杂的模块,它既可以合成网页也可合成用户界面,或者多个标签页,在架构设计上合成器采用的是表示和shi’xian分离的原则,也就是合成器Layer层(同GraphicsLayer类一一对应)同具体合成器所要合成的操作分离的原则,WebKit对合成层的各种设置,最后都使用Layer树来表示,每个Layer节点包含3D变形、裁剪等属性,但是chromium将这些属性应用到后端存储这个过程并不是在Layer树中进行的,而是将这些委托给LayerImpl树来完成,两者之间通过代理来同步,代理的作用是协调和同步两者之间的这些操作,Layer树所有的信息都会拷贝到LayerImpl树中。Layer树工作在主线程,实际指的是渲染引擎工作的线程,不一定是Render进程的主线程,但LayerImpl树都是工作在“实现部分”的线程,实现部分的线程可以是主线程也可以是单独的线程,两者目前在chromium中都被使用,实现部分作为单独一个线程是在Render进程中用来合成网页的,通常也被称为合成器(Compositor)线程,后者也称为线程化合成(Threaded Compositing)。
基础设施
合成器的主要组成,大致可以分成以下几个部分:
- 事件处理部分:主要是接收WebKit或者其他的用户事件,例如网页滚动、放大缩小等事件,这些事件会请求合成器重新绘制每一个合成层,然后合成器在合成这些层的绘制结果
- 合成层的表示和实现:主要定义各种类型的合成层,包括它们的位置、滚动位置、颜色等属性
- 合成层组成两种类型的树,以及它们之间的同步等机制
- 合成调度器(Scheduler)主要调度来自用户的请求,它包括一个状态用于调度当前队列中需要执行的请求,目的是协调合成层的绘制和合成、树的同步等操作
- 合成器的输出结果,在Chromium合成器中,结果可以是一个GPU Surface或者是一个CPU的存储空间,同时也包括GL操作类可以让合成器使用GL来合成这些合成层
- 各种后端存储等资源,合成器需要能够创建各种类型的GL缓冲区、纹理等,因为每个层都需要这些资源
- 支持动画和3D变形这些功能所需要的基础设施
下图展示了合成器的线程内和线程化合成使用到的主要设施:
先看Layer树所在的线程,每一层都是一个Layer对象,Layer树由LayerTreeHost类来维护,LayerTreeHost类的作用是根据调用者的需求创建和更新Layer树,另外就是将这些变动通过代理拷贝给实际的实现者,也就是LayerTreeImpl,这可能需要跨线程,拷贝的作用就是使得合成器能够不依赖与WebKit渲染所在的线程而独立工作。代理类的作用很重要,在合成器中,代理是一个抽象类,定义了Layer树和LayerImpl树之间完成合成所需要的转接工作,它有两个子类,分别是SingleThreadProxy类和ThreadProxy类,它们分别用于线程内合成和线程化合成两种情况,以ThreadProxy为例,代理的一些接口由主线程调用,也就是由LayerTreeHost调用,用来复制信息到实现类LayerImpl,另外的就是使用调度器来调度合成的过程,再看实现部分,实现的主要逻辑有LayerTreeHostImpl来负责,如调度、复制信息到LayerImpl树等,它包含至少一个LayerImpl树对象,在线程化的绘图模式中,他可能至少包含有三个树,而LayerTreeImpl就维护一个LayerImpl树,包括为树中的层创建后端存储、为整个树创建输出结果、合成该树中各个节点的实际过程等。类Layer和LayerImpl是两种基类,它们各自有多个子类,它们和它们的子类基本上是一一对应的,下图描述了Layer类和它的子类:
每个类都有各自的应用场景,例如VideoLayer类是表示视屏播放的,SolidColorLayer类可以表示单一颜色的背景层,而TextureLayer类表示该合成层直接接收一个纹理,该纹理已经由其他部分处理,不需要合成器触发任何绘图操作。图中Layer类是所有类的基类,TiledLayer类是一个中间类,它被ContentLayer类和ImageLayer类继承,它的含义是一个层的后端存储被分割成瓦片状(Tiles),由多个小后端存储共同存储而成,有两种情况会使用瓦片化,其一是ContentLayer类,它表示合成层使用skia画布将内容绘制到结果中,对应到网页中就是常见的HTML元素,例如DOM树中的html、div等所在的层,在chromium中它们使用Skia图形库的SkiaCanvas类来绘图,其二是图形图,如果一个合成层仅仅包含一个图片,那么该图片也会使用该技术。使用瓦片化的后端存储主要有一下几点原因:1 DOM树中的html元素所在的层可能会比较大,因为网页的高度很大,如果只是使用一个后端存储的话那么需要一个很大的纹理对象,但实际GPU硬件可能只支持非常有限的纹理大小;2 在一个比较大的合成层中,可能只是其中一小部分发生变化,根据之前的介绍,需要重新绘制整个层必然产生额外的开销,使用瓦片化的后端存储就只需要重绘一些存在更新的瓦片;3 当层发生滚动的时候,一些瓦片可能不在需要,完后WebKit需要一些新的瓦片来绘制新的区域,这些大小相同的后端存储容易重复利用。在线程内合成模式下,chromium不需要调度器,仅仅在线程化的合成模式下chromium才会使用,所以调度器是在合成器线程中,因而不能访问主线程中的资源,调度器需要考虑整个合成器系统的状态,它需要考虑何时更新树、何时绘图、何时运行动画、何时上传内容到纹理对象等。合成器中的调度器和状态机如下图所示:
Scheduler类就是调度器类,任何合成的相关操作都需要设置到该调度器中,如ThreadProxy类会调用SetNeedsCommit函数来触发Commit操作,该操作的含义是将Layer树的属性等改变同步到LayerImpl树,任务的发起者只是告诉调度器希望执行该任务,通过接口设置标记,Scheulder本身不直接处理这些状态设置,而是将它传给SchedulerStateMachine类处理,该状态机设置相应的状态位,一个任务一般不会被立即执行,而是等到调度器调度到该任务的时候才会执行。
当调用Scheduler类的ProcessScheduleActions时,调度器会通过状态机获取当前需要执行的任务,状态机根据之前设置的各种信息来决定下面的任务是什么,一旦确定了任务,调度器通过SchedulerClient来执行实际的任务,ThreadProxy类就是一个SchedulerClient子类,它会桥接到Layer树、LayerImpl树或其他设施。调度器Scdeduler的基本原则是一切请求都是设置状态机中的状态,这些请求什么时候被执行由调度器来决定,调度任务的主要函数是ProcessSchedulerActions,它首先调用状态机的NextAction函数,有状态机来计算和决定下一个要执行的任务,前面描述过,在此之前,任务的发起者是这些状态,它表示之后希望执行一些任务,而不是立即要求执行,状态机计算出下一个任务,调度器获取任务的类型并执行该任务,然后再接着计算下一个任务,如此循环,直到空闲为止。下图以同步Layer树到LayerImpl树为例说明任务的调度过程以及调度器在这一过程中的作用:
① 当Layer树有变动的时候,需要调用ThreadProxy::SetNeedsCommit,这些任务是在渲染线程中的,随后它会提交一个请求到“Compositor”线程
② 当该“Compositor”线程处理到该请求的时候,会通知调度器的SetNeedsCommit函数设置状态机的状态
③ 调度器的SetNeedsCommit会调用ProcessScheduleAction函数,它检查后面需要的任务
④ 如果没有其他任务或者时间合适的话,状态机决定下面立刻执行该任务,它调用ThreadProxy的SchedulerActionCommit函数,该函数实际执行“commit”任务需要的具体流程
⑤ 在ScheduledActionCommit函数中,它会调用LayerTreeHostImpl和LayerTreeHost中的相应函数来完成同步两个树的工作,同步结束后它需要通知渲染线程,因为这个过程需要阻止该线程
合成过程
合成工作主要由四个步骤,这些步骤都是由调度器调度,它需要各个类参与来共同完成。
① 创建输出结果的目标对象“Surface”,也就是合成结果的存储空间
② 开始一个新的帧,包括计算滚动和缩放大小、动画计算、重新计算网页布局、绘制每个合成层等
③ 将Layer树中包含的这些变动同步到LayerImpl树中
④ 合成LayerImpl树中的各个层并交换前后帧缓冲区,完成一帧的绘制和显示动作
下图为四个步骤对应的时序图:
步骤一:“Compositor”线程首先创建合成器需要的输出结果的后端存储,在调度器执行该任务时,该线程会将任务交给主线程来完成,主线程会创建后端存储并把它传回给“Compositor”线程
步骤二:“Compositor”线程告诉主线程需要开始绘制新的一帧,通过线程间通信来传递任务,主线程接收到任务后开始执行图中的操作, 操作执行完了之后主线程会等待第三个步奏,当第三个步骤完成后,通知主线程的LayerHost等类,这是因为步骤三需要阻塞主线程,同步Layer树
步骤三:如图中所示
步骤四:主要是合成,经过第三步后“Compositor”线程实际上已经不在需要主线程的参与了,这时的线程有了合成这些层需要的一切资源
其他硬件加速模块
2D图形的硬件加速机制
HTML5中规范引入了2D绘图的画布功能,它的作用是提供2D绘图的JavaScript接口,所以JavaScript代码可以很容易的调用该接口来绘制任意的2D图形,2D图形绘图本身是使用2D的图形上下文,而且一般使用软件方式来绘制它们,也就是光栅化的方法,其实2D绘图也可以使用GPU也就是3D绘图来完成,这里把使用GPU来绘制2D图形的方法称为2D图形的硬件加速机制,目前2D图形的硬件加速有两种应用场景,第一种是网页基本元素的绘制,针对的层次类型为ContentLayer,它的后端是一个2D画布对象,第二种是HTML5的新元素canvas,用来绘制2D图形。
2D图形上下文
WebKit的2D图形上下文在WebKit的Chromium移植中需要使用Skia图形库来完成2D图形操作,下图描述了WebKit的Chromium移植中2D图形上下文的实现类:
对于WebKit需要使用到GraphicsContext的地方,Chromium会创建一个Skia图形库中提供的SkCanvas对象来处理WebKit的2D图形操作请求,至于这个SkCanvas对象是使用软件绘图还是GPU绘图,取决于对SkCanvas对象的设置,SkCanvas类表示的是一个画布,2D的图形操作都是在这个画布上处理,绘制结果也是保存在SkCanvas对象中,如果调用者需要创建一个基本的SkDevice对象,该对象使用光栅扫描的方法来一一计算绘制的像素结果,并把结果存入SkBitmap对象中,SkBitmap对象使用一块CPU内存,该内存中保存的是一个个像素值,典型的例如RGBA格式。如果调用者使用GPU硬件来绘图,那么在创建SkCanvas对象的时候通过传入的SkSurface_Gpu对象即可,当然创建SkSurface_Gpu对象需要很多其他的对象,最重要的是SkGpuDevice对象,它是SkDevice的一个基类,同原先软件方式不同,它是将2D图形操作转变成对GL操作,使用GrContext的3D图形上下文来绘制,并将结果存储在GrRenderTarget,该存储目标是GPU的内存缓存区,因此可以看出WebKit调用GraphicsContext对象的时候,WebKit不知道下层实际使用的软件还是GPU来绘制2D图形,这都是有SKia图形库来完成的,当需要硬件加速的时候,Chromium只需要为SkCanvas对象设置相应的对象即可。
Canvas 2D
canvas是HTML5中新加入的元素,可以利用JavaScript接口在画布上绘制任意的2D或3D图形,一个canvas元素的对象只能绘制2D或3D图形中的一种,不能同时绘制两种类型,canvas元素的getContext方法包含一个参数,该参数用来指定创建上下文对象的类型,对于2D的图形操作,通过传递参数值“2d”,游览器返回一个2D图形上下文,称为CanvasRendingContext2D,它提供用于绘制2D图形的各种应用程序编程接口,包括基本图形绘制、文字绘制、图形变换、图片绘制及合成等。下图给出了Canvas2D技术的基本使用方法:
<html>
<body>
<canvas id="canvasT" width="80" height="100">
current browser support canvas
</canvas>
<script type="text/javascript">
var canvas=document.getElementById("canvasT");
vat ctx=canvas.getContext('2d');
ctx.fillStyle='#FF0000';
ctx.filllRect(0,0,80.100);
</script>
</body>
</html>
下图描述了Canvas 2D使用GPU来绘图所涉及到的一些主要类:
Canvas2D机制使用了一个GraphicsContext对象,也就是2D图形上下文,同时还包括一个ImageBuffer对象来表示canvas绘制的结果,同时使用硬件加速机制Chromium会创建一个SkDeferredCanvas对象,该对象的特别之处在于采用了延迟机制来绘制2D图形,同时该对象需要SkGPUDevice来将2D绘图操作转换为使用3D图形上下文来绘制,其中Canvas2DLayerBridge是一个桥接类,因为2D图形是使用3D图形接口绘制的,chromium需要3D图形上下文和一些准备工作,这些都是在这个类中完成的。下面用三个阶段来描述chromium是如何使用硬件加速绘图来支持HTML5的Canvas2D功能的。
第一阶段:初始化阶段,这一阶段需要WebKit和Chromium创建下图中所涉及到的对象,其中GraphicsContext类主要被CanvasRenderingContext2D类所使用,而GraphicsContext3D类是被Canvas2DLayerBridge类使用
第二阶段:构建RenderLayer等对象,在DOM树构建完之后,WebKit检查有无变化的CSS样式,这里JavaScript代码改变了canvas元素的属性,所以WebKit会更新RenderObject树和RenderLayer树,下图描述了这一过程
第三阶段:绘图部分,下图描述了这一思想和过程,Chromium采用缓存模式来处理JavaScript代码的2D图形操作,当JavaScript通过标准的接口调用2D图形的时候,Chromium使用SkDeferredCanvas对象保存2D图形操作,当Chromium需要绘制一个新帧的时候,Skia图形库才会一次性提交并绘制这些缓存的操作
上半部分中JavaScript调用2D绘图接口是需要使用contextAcquired()函数获取2D绘图上下文,Chromium据此判断后面修改画布的内容,所以Chromium会使用Canvas2DLayerManager类来设置一个TaskObserver对象到主线程循环,这样做的好处是等到JavaScript代码调用2D绘图接口之后才会触发真正的绘图动作,而JavaScript代码调用的这些操作都是依靠SkDeferredCanvas来保存的。下半部分表示当前面JavaScript调用2D绘图接口完毕后,WebKit调用TaskObserver类的didProcessTask方法,Canvas2DLayerManager类调用CanvasLayerBridge类来判断是否需要刷新这些操作,Canvas2DLayerBridge类检查并重置前面设置的标记,如果时机合适的话,该类调用SkDeferredCanvas类的flush函数提交前面保存的所有绘图操作,这样就完成了Canvas2D的绘制工作。
当合成器调用updateLayers函数的时候,该函数会触发每个合成层绘制自己,因为canvas2D机制是由JavaScript代码来绘制2D图形,所以这个时候canvas所在的合成层实际上已经绘制完成,下图描述了合成器要求绘制Canvas2D的合成层的过程,这时候WebKit实际上不需要绘制该层,只需要改变一下3D图形上下文的状态:
WebGL
3D图形上下文
WebCore表示该上下文的抽象类是GraphicsContext3D,WebKit的Chromium移植定义了WebGraphicsContext3D接口,该接口是GraphicsContext3D的实现:
WebGraphicsContext3DCommandBufferImpl类是WebGraphicsContext3D类对应的使用命令缓冲区的实现子类,合成过程和canvas2D、WebGL都会使用该类来实现3D图形操作。
WebGL的实现
WebGL是一套基于3D图形定义的JavaScript接口,它基于canvas元素,跟canvas2D不同的是Web开发者可以使用3D图形接口来绘制3D图形,这些接口可以分为如下几个部分:
- 上下文及内容展示:在使用WebGL的编程接口之前,开发者需要获取WebGLRenderingContext和DrawingBuffer接口,对JavaScript代码来说,GL的操作都是由WebGLRenderingContext对象来负责完成的,但DrawingBuffer接口对用户来说是透明的,它用来存储渲染的内容并被合成器所合成,包括帧缓冲器对象和纹理对象
- WebGL的资源及其生命周期:纹理对象、缓冲区、帧缓冲区、渲染缓冲区、着色器等,它们有对应的JavaScript对象即WebGLObject对应,这些对象的生命周期是一致的
- 安全:WebGL规范为保证安全性,第一所有的WebGL资源必须包含初始化数据,第二来源安全性,为防止信息泄漏,当canvas元素的属性“origin-clean”为false时,readPixels将会抛出安全方面的异常,第三要求所有的着色语言必须符合OpenGL ES Shading Language 1.0,第四为防止DOS攻击,规范建议采取适当的措施对花费时间过长的渲染操作过程进行监控和限制
- WebGL接口:主要包括各种资源类的接口和上下文类的接口,这些接口用于绘制3D的操作
针对规范定义的内容,WebKit和Chromium定义了相应的类来描述它们,下图给出了主要的类
WebGLRenderingContext类同CanvasRendingContext2D类的作用类似,都是规范定义的接口,不同的是,WebGL的接口是3D图形操作,WebGLRenderingContext类同样需要一个GraphicsContext3D类和它的实现类,除此之外,还需要一个DrawingBuffer类,它类于Canvas2D中的ImageBuffer,它的作用是保存WebGL渲染目标结果,WebKit将渲染结果用来合成。
Chromium中WebGL的工作过程也可分成三个阶段:
第一阶段:对象的初始化阶段,当JavaScript引擎调用之前实例代码中的getContext函数的时候,WebKit会执行如下的创建过程,这个过程跟Canvas2D的对象创建过程类似,不同的是这一阶段不会创建cc::Layer对象
第二阶段:构建RendeLayer、WebLayer、cc::Layer等对象,同样是在DOM树构建之后检查CSS样式变化时才会触发,当RendeLayer等对象被创建之后,WebKit设置GraphicsLayer对象所对应的WebLayer对象,同canvas2D不一样的是,此时DrawingBuffer对象才会开始请求创建WebLayer(WebLayerImpl)和TextureLayer对象,之后WebKit同样将WebLayer对象设置到GraphicsLayer中
第三阶段:绘图阶段,下图是一个简单的WebGL使用clearColor接口来设置颜色的JavaScript代码被WebKit执行的过程,同canvas2D不同的是,每个GL的调用都是直接通过WebGraphicsContext3DCommandBufferImpl类将GL命令传给GPU,这一过程没有使用缓存机制,而是直接将命令传递给GPU进程
当合成器调用updateLayers函数的时候,该函数会触发WebGL所使用的合成层绘制合成层的目标结果到合成层的存储结果,WebGL所在层的内容在合成器请求更新层之前已经由WebKit完成,下图描述了合成器绘制WebGL的合成层时候的过程,DrawingBuffer所要做的就是刷新3D图形上下文中的结果数据,并返回结果
CSS 3D变形
CSS 3D变形和动画是HTML5新引入的特性,作用是能够对任意DOM子树做3D变形,它同WebGL提供的能力不一样,WebGL是在一个canvas元素内部绘制3D图形,CSS 3D变形功能可以对任意元素进行3D变形,它是一个可以被元素子女继承的属性,也就是一个元素和它的子女都会作相应的3D变形。下图介绍了WebKit和Chromium设置3D变形值的过程:
当网页中JavaScript代码修改元素的变换属性值的时候,通过上面的过程,最后样式是设置在该合成层上,当WebKit将元素绘制完成之后,在合成过程中,WebKit通过3D变形作用到该合成层上,即可完成特定的效果,WebKit只是在第一次需要绘制div元素内容,之后仅仅设置变换属性值,然后重新合成即可,当合成器调度绘制该合成层的时候,WebKit根本不会发生想象中的重新布局和重绘动作,虽然用户看起来网页在变动,但是这只是合成的动作,这些动画等都是WebKit和Chromium设计的机制和硬件加速的效果。