Tagged: Canvas Toggle Comment Threads | 键盘快捷键

  • Roger 7:08 pm on April 16, 2014 固定链接 | 回复
    Tags: , Canvas, , , webgl,   

    How Rendering Work (in WebKit and Blink) 

    How Rendering Work (in WebKit and Blink)

    自从开始从事浏览器内核开发工作以来,已经写过不少跟渲染相关的文章。但是一直想写一篇像 How Browsers Work 类似,能够系统,完整地阐述浏览器的渲染引擎是如何工作的,它是如何对网页渲染性能进行优化的文章,却一直因为畏惧所需要花费的时间和精力,迟迟无法动笔。不管如何,现在终于鼓起勇气来写了,希望自己能够完成吧…

    文章包括的主要内容如下 —

    • 渲染基础 - DOM & RenderObject & RenderLayer
    • WebView,绘制与混合,多线程渲染
    • 硬件加速
    • 分块渲染
    • 图层混合加速
    • 网页游戏渲染 - Canvas & WebGL

    首先明确文中关于渲染的定义,浏览器内核引擎通常又被称为网页渲染引擎,但是这里的渲染实际上是一个泛指,广义的渲染,它包括了浏览器内核所有的主要工作 - 加载,解析,排版,绘制等等。而在本文里面的渲染,指的是跟绘制相关的部分,也就是浏览器是如何将排版后的结果最终显示在屏幕上的这一过程。如果读者希望先对浏览器内核引擎,特别是 WebKit 有一个大概的了解,How Browsers WorkHow WebKit WorkWebKit for Developers 可以提供不错的入门指引。

    其次,本文主要描述 WebKit 引擎的实现,不过因为 Blink 实际上从 WebKit 分支出来的时间并不长,两者在渲染整体架构上还是基本一致的,所以文中不会明确区分这两者。

    最后,希望这篇文章能够给从事浏览器内核开发,特别是渲染引擎开发的开发者一个能够快速入门的指引,并给前端开发者优化网页渲染性能提供足够的知识和帮助。

    因为文章后续还有可能会持续修订和补充,如果要查看最新的内容,请访问个人博客的原文:http://blog.csdn.net/rogeryi/article/details/23686609,如果有什么疏漏和错误,也欢迎读者来信指正(roger2yi@gmail.com)。

    渲染基础 - DOM & RenderObject & RenderLayer

    DOM,RenderObject,RenderLayer

    图片来自 GPU Accelerated Compositing in Chrome

    当浏览器通过网络或者本地文件系统加载一个 HTML 文件,并对它进行解析完毕后,内核就会生成它最重要的数据结构 - DOM 树。DOM 树上每一个节点都对应着网页里面的每一个元素,并且网页也可以通过 JavaScript 操作这棵 DOM 树,动态改变它的结构。但是 DOM 树本身并不能直接用于排版和渲染,内核还会生成另外一棵树 - Render 树,Render 树上的每一个节点 - RenderObject,跟 DOM 树上的节点几乎是一一对应的,当一个可见的 DOM 节点被添加到 DOM 树上时,内核就会为它生成对应的 RenderOject 添加到 Render 树上。

    Render Tree

    图片来自 How WebKit Work

    Render 树是浏览器排版引擎的主要作业对象,排版引擎根据 DOM 树和 CSS 样式表的样式定义,按照预定的排版规则确定了 Render 树最后的结构,包括其中每一个 RenderObject 的大小和位置,而一棵经过排版的 Render 树,则是浏览器渲染引擎的主要输入,读者可以认为,Render 树是衔接浏览器排版引擎和渲染引擎之间的桥梁,它是排版引擎的输出,渲染引擎的输入。

    Layer Tree

    图片来自 How WebKit Work

    不过浏览器渲染引擎并不是直接使用 Render 树进行绘制,为了方便处理 Positioning(定位),Clipping(裁剪),Overflow-scroll(页內滚动),CSS Transform/Opacity/Animation/Filter,Mask or Reflection,Z-indexing(Z排序)等,浏览器需要生成另外一棵树 – Layer 树。渲染引擎会为一些特定的 RenderObject 生成对应的 RenderLayer,而这些特定的 RenderObject 跟对应的 RenderLayer 就是直属的关系,相应的,它们的子节点如果没有对应的 RenderLayer,就从属于父节点的 RenderLayer。最终,每一个 RenderObject 都会直接或者间接地从属于一个 RenderLayer。

    RenderObject 生成 RenderLayer 的条件,来自 GPU Accelerated Compositing in Chrome

    • It’s the root object for the page
    • It has explicit CSS position properties (relative, absolute or a transform)
    • It is transparent
    • Has overflow, an alpha mask or reflection
    • Has a CSS filter
    • Corresponds to canvas element that has a 3D (WebGL) context or an accelerated 2D context
    • Corresponds to a video element

    浏览器渲染引擎遍历 Layer 树,访问每一个 RenderLayer,再遍历从属于这个 RenderLayer 的 RenderObject,将每一个 RenderObject 绘制出来。读者可以认为,Layer 树决定了网页绘制的层次顺序,而从属于 RenderLayer 的 RenderObject 决定了这个 Layer 的内容,所有的 RenderLayer 和 RenderObject 一起就决定了网页在屏幕上最终呈现出来的内容。

    软件渲染模式下,浏览器绘制 RenderLayer 和 RenderObject 的顺序,来自 GPU Accelerated Compositing in Chrome

     

    In the software path, the page is rendered by sequentially painting all the RenderLayers, from back to front. The RenderLayer hierarchy is traversed recursively starting from the root and the bulk of the work is done in RenderLayer::paintLayer() which performs the following basic steps (the list of steps is simplified here for clarity):

    1. Determines whether the layer intersects the damage rect for an early out.
    2. Recursively paints the layers below this one by calling paintLayer() for the layers in the negZOrderList.
    3. Asks RenderObjects associated with this RenderLayer to paint themselves.
    4. This is done by recursing down the RenderObject tree starting with the RenderObject which created the layer. Traversal stops whenever a RenderObject associated with a different RenderLayer is found.
    5. Recursively paints the layers above this one by calling paintLayer() for the layers in the posZOrderList.

    In this mode RenderObjects paint themselves into the destination bitmap by issuing draw calls into a single shared GraphicsContext (implemented in Chrome via Skia).

    WebView,绘制与混合,多线程渲染

    WebView

    图片来自 [UC 浏览器 9.7 Android版],中间是一个 WebView,上方是标题栏和工具栏

    浏览器本身并不能直接改变屏幕的像素输出,它需要通过系统本身的 GUI Toolkit。所以,一般来说浏览器会将一个要显示的网页包装成一个 UI 组件,通常叫做 WebView,然后通过将 WebView 放置于应用的 UI 界面上,从而将网页显示在屏幕上。

    一些 GUI Toolkit,比如 Android,默认的情况下 UI 组件没有自己独立的位图缓存,构成 UI 界面的所有 UI 组件都直接绘制在当前的窗口缓存上,所以 WebView 每次绘制,就相当于将它在可见区域内的 RenderLayer/RenderObject 逐个绘制到窗口缓存上。上述的渲染方式有一个很严重的问题,用户拖动网页或者触发一个惯性滚动时,网页滑动的渲染性能会十分糟糕。这是因为即使网页只移动一个像素,整个 WebView 都需要重新绘制,而要绘制一个 WebView 大小的区域的 RenderLayer/RenderObject,耗时通常都比较长,对于一些复杂的桌面版网页,在移动设备上绘制一次的耗时有可能需要上百毫秒,而要达到60帧/每秒的流畅度,每一帧绘制的时间就不能超过16.7毫秒,所以在这种渲染模式下,要获得流畅的网页滑屏效果,显然是不可能的,而网页滑屏的流畅程度,又是用户对浏览器渲染性能的最直观和最重要的感受。

    要提升网页滑屏的性能,一个简单的做法就是让 WebView 本身持有一块独立的缓存,而 WebView 的绘制就分成了两步 1) 根据需要更新内部缓存,将网页内容绘制到内部缓存里面 2) 将内部缓存拷贝到窗口缓存上。第一步我们通常称为绘制(Paint)或者光栅化(Rasterization),它将一些绘图指令转换成真正的像素颜色值,而第二步我们一般称为混合(Composite),它负责缓存的拷贝,同时还可能包括位移(Translation),缩放(Scale),旋转(Rotation),Alpha 混合等操作。咋一看,渲染变得比原来更复杂,还多了一步操作,但实际上,混合的耗时通常远远小于网页内容绘制的耗时,后者即使在移动设备上一般也就在几个毫秒以内,而大部分时候,在第一步里面,我们只需要绘制一块很小的区域而不需要绘制一个完整 WebView 大小的区域,这样就有效地减少了绘制这一步的开销。以网页滚动为例子,每次滚动实际上只需要绘制新进入 WebView 可见区域的部分,如果向上滚动了10个像素,我们需要绘制的区域大小就是10 x Width of WebView,比起原来需要绘制整个 WebView 大小区域的网页内容当然要快的多了。

    进一步来说,浏览器还可以使用多线程的渲染架构,将网页内容绘制到缓存的操作放到另外一个独立的线程(绘制线程),而原来线程对 WebView 的绘制就只剩下缓存的拷贝(混合线程),绘制线程跟混合线程之间可以使用同步,部分同步,完全异步等作业模式,让浏览器可以在性能与效果之间根据需要进行选择,比如说异步模式下,当浏览器需要将 WebView 缓存拷贝到窗口缓存,但是需要更新的部分还没有来得及绘制时,浏览器可以在还未及时更新的部分绘制一个背景色或者空白,这样虽然渲染效果有所下降,但是保证了每一帧窗口更新的间隔都在理想的范围内。并且浏览器还可以为 WebView 创建一个更大的缓存,超过 WebView本身的大小,让我们可以缓存更多的网页内容,可以预先绘制不可见的区域,这样就可以有效减少异步模式下出现空白的状况,在性能和效果之间取得更好的平衡。

    硬件加速

    上述的渲染模式,无论是绘制还是混合,都是由 CPU 完成的,而没有使用到 GPU。绘制任务比较复杂,较难使用 GPU 来完成,并且对于各种复杂的图形/文本的绘制来说,使用 GPU 效率有时反而更低(并且系统资源的开销也较大),但是混合就不一样了,GPU 最擅长的就是并行处理多个像素的计算,所以 GPU 相对于 CPU,执行混合的速度要快的多,特别是存在缩放,旋转,Alpha 混合的时候,而且混合相对来说也比较简单,改成使用 GPU 来完成并不困难。

    并且在多线程渲染模式下,因为绘制和混合分别处于不同的线程,绘制使用 CPU,混合使用 GPU,这样可以通过 CPU/GPU 之间的并发运行有效地提升浏览器整体的渲染性能。更何况,窗口的更新是由混合线程来负责的,混合的效率越高,窗口更新的间隔就越短,用户感受到 UI 界面变化的流畅度就越高,只要窗口更新的间隔能够始终保持在16.7毫秒以内,UI 界面就能够一直保持60帧/每秒的极致流畅度(因为一般来说,显示屏幕的刷新频率是60hz,所以60帧/秒已经是极限帧率,超过这个数值意义不大,而且 OS 的图形子系统本身就会强制限制 UI 界面的更新跟屏幕的刷新保持同步)。

    所以对于现代浏览器来说,所谓硬件加速,就是使用 GPU 来进行混合,绘制仍然使用 CPU 来完成。

    分块渲染

    Tile Rendering

    图片来自 [UC 浏览器 9.7 Android版],使用256×256大小的分块

    网页的缓存通常都不是一大块,而是划分成一格一格的小块,通常为256×256或者512×512大小,这种渲染方式称为分块渲染(Tile Rendering)。使用分块渲染的主要原因是因为 –

    1. 所谓 GPU 混合,通常是使用 Open GL/ES 贴图来实现的,而这时的缓存其实就是纹理(GL Texture),而很多 GPU 对纹理的大小有限制,比如长/宽必须是2的幂次方,最大不能超过2048或者4096等,所以无法支持任意大小的缓存;
    2. 使用小块缓存,方便浏览器使用一个统一的缓存池来管理分配的缓存,这个缓存池一般会分配成百上千个缓存块供所有的 WebView 共用。所有打开的网页,需要缓存时都可以以缓存块为单位向缓存池申请,而当网页关闭或者不可见时,这些不需要的缓存块就可以被回收供其它网页使用;

    总之固定大小的小块缓存,通过一个统一缓存池来管理的方式,比起每个 WebView 自己持有一大块缓存有很多优势。特别是更适合多线程 CPU/GPU 并发的渲染模型,所以基本上支持硬件加速的浏览器都会使用分块渲染的方式。

    图层混合加速

    Layer Accelerated Compositing

    图片来自 [UC 浏览器 9.7 Android版],可见区域内有4个 Layer 有自己的缓存 – 最底层的 Base Layer,上方的 Fixed 标题栏,中间的热点新闻栏,右下方的 Fixed 跳转按钮

    图层混合加速(Accelerated Compositing)的渲染架构是 Apple 引入 WebKit 的,并在 Safari 上率先实现,而 Chrome/Android/Qt/GTK+ 等都陆续完成了自己的实现。如果熟悉 iOS 或者 Mac OS GUI 编程的读者对其应该不会感到陌生,它跟 iOS CoreAnimation 的 Layer Rendering 渲染架构基本类似,主要都是为了解决当 Layer 的内容频繁发生变化,或者当 Layer 触发一个2D/3D变换(2D/3D Transform )或者渐隐渐入动画,它的位移,缩放,旋转,透明度等属性不断发生变化时,在原有的渲染架构下,渲染性能低下的问题。

    非混合加速的渲染架构,所有的 RenderLayer 都没有自己独立的缓存,它们都被绘制到同一个缓存里面(按照它们的先后顺序),所以只要这个 Layer 的内容发生变化,或者它的一些 CSS 样式属性比如 Transform/Opacity 发生变化,变化区域的缓存就需要重新生成,此时不但需要绘制变化的 Layer,跟变化区域(Damage Region)相交的其它 Layer 都需要被绘制,而前面已经说过,网页的绘制是十分耗时的。如果 Layer 偶尔发生变化,那还不要紧,但如果是一个 JavaScript 或者 CSS 动画在不断地驱使 Layer 发生变化,这个动画要达到60帧/每秒的流畅效果就基本不可能了。

    而在混合加速的渲染架构下,一些 RenderLayer 会拥有自己独立的缓存,它们被称为混合图层(Compositing Layer),WebKit 会为这些 RenderLayer 创建对应的 GraphicsLayer,不同的浏览器需要提供自己的 GrphicsLayer 实现用于管理缓存的分配,释放,更新等等。拥有 GrphicsLayer 的 RenderLayer 会被绘制到自己的缓存里面,而没有 GrphicsLayer 的 RenderLayer 它们会向上追溯有 GrphicsLayer 的父/祖先 RenderLayer,直到 Root RenderLayer 为止,然后绘制在有 GrphicsLayer 的父/祖先 RenderLayer 的缓存上,而 Root RenderLayer 总是会创建一个 GrphicsLayer 并拥有自己独立的缓存。最终,GraphicsLayer 又构成了一棵与 RenderLayer 并行的树,而 RenderLayer 与 GraphicsLayer 的关系有些类似于 RenderObject 与 RenderLayer 之间的关系。

    混合加速渲染架构下的网页混合,也变得比以前复杂,不再是简单的将一个缓存拷贝到窗口缓存上,而是需要完成源自不同 Layer 的多个缓存的拷贝,再加上可能的2D/3D变换,再加上缓存之间的Alpha混合等操作,当然,对于支持硬件加速,使用 GPU 来完成混合的浏览器来说,速度还是很快的。

    RenderLayer 生成 GraphicsLayer 的条件,来自 GPU Accelerated Compositing in Chrome

    1. Layer has 3D or perspective transform CSS properties
    2. Layer is used by < video> element using accelerated video decoding
    3. Layer is used by a < canvas> element with a 3D context or accelerated 2D context
    4. Layer is used for a composited plugin
    5. Layer uses a CSS animation for its opacity or uses an animated webkit transform
    6. Layer uses accelerated CSS filters
    7. Layer with a composited descendant has information that needs to be in the composited layer tree, such as a clip or reflection
    8. Layer has a sibling with a lower z-index which has a compositing layer (in other words the layer is rendered on top of a composited layer)

    混合加速的渲染架构下,Layer 的内容变化,只需要更新所属的 GraphicsLayer 的缓存即可,而缓存的更新,也只需要绘制直接或者间接属于这个 GraphicsLayer 的 RenderLayer 而不是所有的 RenderLayer。特别是一些特定的 CSS 样式属性的变化,实际上并不引起内容的变化,只需要改变一些 GraphicsLayer 的混合参数,然后重新混合即可,而混合相对绘制而言是很快的,这些特定的 CSS 样式属性我们一般称之为是被加速的,不同的浏览器支持的状况不太一样,但基本上CSS Transform & Opacity 在所有支持混合加速的浏览器上都是被加速的。被加速的CSS 样式属性的动画,就比较容易达到60帧/每秒的流畅效果了。

    Falling Leaves

    图片来自 Understanding Hardware Acceleration on Mobile Browsers,展现了经典的 CSS 动画 Demo – Falling Leaves 的图层混合的示意图,它所使用的 Transform 和 Opacity 动画在所有支持混合加速的浏览器上都是被加速的

    不过并不是拥有独立缓存的 RenderLayer 越多越好,太多拥有独立缓存的 Layer 会带来一些严重的副作用 – 首先它大大增加了内存的开销,这点在移动设备上的影响更大,甚至导致浏览器在一些内存较少的移动设备上无法很好地支持图层混合加速;其次,它加大了混合的时间开销,导致混合性能的下降,而混合性能跟网页滚动/缩放操作的流畅度又息息相关,最终导致网页滚动/缩放的流畅度下降,让用户觉得操作不够流畅。

    在 Chrome://flags 里面开启“合成渲染层边框”就可以看到哪些 Layer 是一个 Compositing Layer,也就是拥有自己的独立缓存。前端开发者可以用此帮助自己控制 Compositing Layer 的创建。总的的说, Compositing Layer 可以提升绘制性能,但是会降低混合性能,网页只有合理地使用 Compositing Layer,才能在绘制和混合之间取得一个良好的平衡,实现整体渲染性能的提升。

     

    Chrome Flags

    图片来自 Chrome,展现了经典的 CSS 动画 Demo – Falling Leaves 的合成渲染层边框

    网页游戏渲染 - Canvas & WebGL

    2D Canvas

    图片来自 [UC 浏览器 9.7 Android版],基于2D Canvas 的游戏不江湖,在主流配置手机上可以达到60帧/每秒的流畅度

    以前网页游戏一般都是使用 Flash 来实现,但是随着 Flash 从移动设备被淘汰,越来越多的网页游戏会改用 Canvas 和 WebGL 来开发,浏览器关于 Canvas 的基本绘制流程可以参考我以前的文章Introduce My Work。虽然一般网页元素都是使用 CPU 来绘制,但是对于加速的2D Canvas 和 WebGL 来说,它们的绘制是直接使用 GPU 的,所以它们一般会拥有一个GL FBO(FrameBufferObject)作为自己的缓存,Canvas/WebGL 的内容被绘制到这个 FBO 上面,而这个 FBO 所关联的纹理再在混合操作里面被拷贝到窗口缓存上。简单的来说,对于加速的2D Canvas 和 WebGL,它们的绘制和混合都是使用 GPU。

    WebGL

    图片来自 [UC 浏览器 9.7 Android版],一个演示 WebGL 的网页 Demo

    关于如何优化 Canvas 游戏的性能,请参考我以前的文章 – High Performance Canvas Game for Android(高性能Android Canvas游戏开发)

    参考索引

    How Browsers Work: Behind the scenes of modern web browsers
    How WebKit Work
    WebKit for Developers
    GPU Accelerated Compositing in Chrome
    Understanding Hardware Acceleration on Mobile Browsers
    Web Page Rendering and Accelerated Compositing
    我的2013 – 年终总结 + 浏览器渲染发展的一些思考
    Introduce My Work
    High Performance Canvas Game for Android(高性能Android Canvas游戏开发)
    OpenGL Frame Buffer Object (FBO)

     
  • Roger 9:37 am on September 2, 2013 固定链接 | 回复
    Tags: , Canvas, , , , uc   

    High Performance Canvas Game for Android 

    UC在今年5月份发布了支持硬件加速的实验室版,开始实验性地支持对2D Canvas进行硬件加速。在9月份发布的9.3版本中开始分开两个版本 – 普通版和加速版,针对中高端手机的加速版开始正式支持硬件加速2D Canvas渲染。并且在未来将要发布的9.4加速版还会带来全新的Canvas渲染架构,进一步提升性能和减少对系统资源的占用。
    这篇文章的主要目的是为移动Canvas游戏的开发者如何针对Android UC浏览器9.4加速版进行渲染性能优化提供指导,不过文中的大部分内容也适用于Android平台其它支持硬件加速2D Canvas的浏览器,比如Chrome for Android。另外,这篇文章的内容主要是针对渲染性能优化,而不是JavaScript性能优化,不过因为Android UC浏览器使用的是V8引擎,所以您应该很容易找到很多如何针对V8优化JavaScript性能的文章。

    Rule #0 为移动平台进行优化

    为移动平台进行优化是十分重要的,因为移动平台的性能大概只有桌面平台的1/10左右(*1),它通常意味着:
    1. 更慢的CPU速度,这意味着不经过优化的JavaScript代码,性能会十分糟糕;
    2. 更小的内存,没有虚拟内存的支持,这意味着加载太多的资源容易导致内存不足,JSVM更容易引发GC,并且GC造成的停顿时间也越长;
    3. 更慢的GPU速度,没有独立的显存,内存带宽相比PC要慢的多,这意味着即使使用GPU对Canvas进行加速,您仍然需要小心网页DOM树的复杂度,游戏所使用的分辨率(Canvas的大小),图片资源的分辨率,游戏场景的复杂度和尽量避免Overdraw等等;
    注释:
    1. 如果您需要对移动平台浏览器的性能有一个全面的了解,建议阅读文章“Why mobile web apps are slow”(原文译文)和”5 Myths About Mobile Web Performance“(原文译文)。

    Rule #1 为Android而不是iOS而优化

    牢记这一点非常重要,Mobile Safari的Canvas渲染机制跟Android平台有很大的不同,特别地针对Mobile Safari进行优化的Canvas游戏在Android平台的渲染性能会十分的糟糕。
    Mobile Safari使用了iOS/MacOS平台特有的IOSurface作为Canvas的Buffer,当通过Canvas API往IOSurface绘制内容的时候是没有GPU加速的,iOS仍然使用CPU进行绘制,但是将一个IOSurface绘制到另外一个IOSurface上的时候,iOS会使用GPU的2D位拷贝加速单元进行加速(*1)。这种机制其实也是iOS UI界面Layer Rendering渲染架构的基础。所以为iOS优化的Canvas游戏会倾向于使用大量的Off-Screen Canvas(*2),不管是静态的图片也好,还是需要动态产生的内容也好,统统都缓存到一个Off-Screen Canvas上,最终游戏场景的绘制就是一个把一堆Off-Screen Canvas绘制到一个On-Screen Canvas的过程,这样就可以充分利用iOS绘制IOSurface到IOSurface使用了GPU加速的特性来提升渲染性能。
    但是这种大量使用Off-Screen Canvas的做法在Android平台的浏览器上会非常糟糕。Android平台并没有IOSurface的同等物(一块同时支持CPU读写和GPU读写的缓冲区),所以它只能使用GL API对Canvas进行加速,一个加速的Canvas,它的Buffer是一个GL Texture(被attach到一个FBO上),这意味着:
    1. 无论是绘制到Canvas本身,还是Canvas绘制到Canvas都是GPU加速的,普通的位图要绘制到Canvas上,需要先被加载到一个Texture中;
    2. Texture Buffer只能通过GPU进行读写,如果要使用CPU访问,必须先通过glReadPixels把内容从显存拷贝到一块普通内存,而这个操作会非常慢并且会造成GL渲染流水线的阻塞;
    3. 如果游戏频繁创建和销毁一些比较小的Canvas,会很容易造成显存的碎片化,让显存的耗尽速度加快,并且创建太多的Canvas也容易把GPU资源都消耗光,导致后续的分配失败和渲染错误;
    4. 当每个Game Loop都对多个Canvas进行同时更新时,会导致GL Context不断地切换不同的Render Target(FBO),而这对GL的渲染性能有很大的影响;
    后续的内容会进一步说明如何针对使用GL加速的Canvas渲染架构进行优化。
    注释:
    1. 一般GPU都会带有多个独立的加速单元,包括3D加速单元,支持GL和D3D这样的3D API;2D位拷贝加速单元,对将一块缓冲区绘制到另外一块缓冲区进行加速;2D矢量绘制加速单元,支持像OpenVG这样的2D API,但是Android平台只支持通过GL API使用GPU加速,并没有公开的2D位拷贝加速API,虽然2.x的时候厂商可以提供一个copybit模块对位拷贝进行加速,供SurfaceFlinger使用,但这个模块不是通用的,并且不对外公开,另外在4.x的时候也已经移除了。
    2. Off-Screen Canvas在文中是指display:none,没有attach到DOM树上的Canvas,相对于On-Screen Canvas而言。

    Rule #2 优化网页的DOM树结构

     

    Canvas只是网页的一部分,它最终要显示出来还需要浏览器对网页本身的绘制,如果网页的DOM树结构越复杂,浏览器花在网页绘制上的时间也就越长,网页绘制占用的CPU/GPU资源也就越多,而留给Canvas绘制的CPU/GPU资源也就越少,这意味着Canvas绘制本身需要的时间也越长。并且网页绘制的耗时越长,Canvas最终更新到屏幕上的延迟也就越长,总之,这是一个此消彼涨的过程。所以,优化网页的DOM树结构,让其尽可能简单,浏览器就可以把更多的系统资源花费在Canvas的绘制上,从而提升Canvas的渲染性能。

     

    最理想的DOM结构就是只包含一个<body>,加上一个<div>作为容器和加上一个<canvas>本身。如果Canvas上面需要显示其它的网页内容,最好只是用于一些临时使用的对话框之类的东西,而不是一直固定显示。

     

    Rule #3 优化网页元素的css背景设置

     

    跟#2的道理一样,背景设置越简单或者根本不设置背景,浏览器花费在网页本身绘制的开销也就越小,一般来说<canvas>元素本身都不应该设置css背景,它的背景应该通过Canvas API来绘制,避免浏览器在绘制<canvas>元素时还要先绘制背景,然后再绘制Canvas的内容。另外<body>和其它元素都应该首先考虑使用background-color而不是background-image,因为background-image的绘制耗时比一个纯色填充要大的多,而且背景图片本身还需要消耗额外的显存资源(生成Texture)。

     

     Rule #4 使用合适大小的Canvas

     

    考虑移动设备的性能限制,Canvas不适宜太大,否则需要消耗更多的GPU资源和内存带宽,480p或者600p是一个比较合适的选择(横屏游戏可以选择800p或者960p),一般不应该超过720p。并且游戏图片资源的分辨率应该跟Canvas的分辨率保持一致,也就是正常情况下图片绘制到Canvas上应该不需要缩放。

     

    我们需要避免创建了一个较大的Canvas,但是仍然使用较低分辨率的图片资源,图片绘制到Canvas上还需要经过缩放的情况。这样做毫无意义,因为游戏本身的分辨率是由图片资源的分辨率来决定的,上述的情形既不能提升游戏的精美程度,也白白浪费了系统资源。

     

    如果自己预先指定了Canvas的大小,又希望Canvas在网页中全屏显示,可以通过<meta viewport>标签设置viewport的大小(*1,*2),直接告诉浏览器网页虚拟viewport的宽度应该是多少,并且让viewport的宽度等于Canvas的宽度,而浏览器会自动按照viewport宽度和屏幕宽屏的比值对网页进行整体放大。

     

    注释:
    1. <meta viewport>的设置可以参考这个例子:http://www.craftymind.com/factory/guimark3/bitmap/GM3_JS_Bitmap.html
    2. Android系统浏览器在网页不指定viewport宽度时,它会认为这是一个WWW页面,并且使用980的默认viewport宽度,UC浏览器也遵循了同样的做法。这意味着您不设置viewport宽度,并且直接使用window.clientWidth作为Canvas的宽度时,就会创建出一个980p的Canvas,通常这是毫无意义的;

     Rule #5 避免使用多个On-Screen Canvas

    如#1所述,多个Canvas同时更新会降低GL渲染的效率,并且如#2所述,多个On-Screen Canvas会导致网页本身的绘制时间增加,所以应该避免使用。

    Rule #6 合理地使用Off-Screen Canvas

    在GL加速的Canvas渲染架构下,合理地使用Off-Screen Canvas可以在某些特定的场景提升渲染性能,但是不合理的使用反而会导致性能下降。
    1. 将图片绘制到一个Off-Screen Canvas上,然后把这个Canvas当作原图片使用,这种用法,如#1所述,在iOS上是有用的,但是在Android上不必要的,甚至会导致额外的资源浪费(虽然渲染性能还是一样)。浏览器会自动将需要绘制到Canvas的图片加载成Texture并缓存起来,避免反复加载,只要这个缓存池大小没有超过限制,图片的绘制就只需要付出一次贴图的开销,这对GPU来说是很小的;
    2. 避免使用过大或者过小的Off-Screen Canvas —— 首先过大的Canvas会超过系统的Max Texture Size而无法进行加速(*1,*2),而太小的Canvas(*3,*4),因为对它加速不但不会加快渲染速度,反而会导致如#1所述的一些问题 —— 加快GPU资源的耗尽,频繁切换Render Target的额外开销等,所以也是不加速的;
    3. 避免频繁动态创建和销毁Canvas对象,这样很容易引发GC,而且浏览器为了避免大量的Canvas Buffer把GPU资源耗尽,还会在接近临界值时进行强制GC(*5),而强制GC造成的停顿比一般GC还要长,通常会达到500ms~1000ms。所以一般来说应该事先生成所有需要的Canvas然后一直使用,或者建立一个缓存池来回收和重用;
    4. Canvas初始大小设置后就不应该再改变,否则浏览器需要为它分配新的Buffer;
    5. 需要动态生成的内容,可以在一个Off-Screen Canvas上预先生成,然后直接将这个Canvas绘制到On-Screen Canvas上,但是这个生成应该是一次性的(或者偶尔),而不是每个Game Loop都需要更新,否则就会造成#1所述的问题 —— 频繁切换Render Target的额外开销;
    6. 如果场景中的部分内容很少发生变化,但是位置,缩放比例,旋转角度,透明度等属性需要频繁变化,可以把一个Off-Screen Canvas当作Layer使用,缓存这部分内容,然后在绘制这部分内容时就直接绘制这个Off-Screen Canvas;
    总结一下,Off-Screen Canvas的使用应该尽量遵循以下原则:
    1. 数量适中(越少越好);
    2. 大小适中(面积128×128以上,长宽2048以内,并且为2的幂次方对GPU来说是最友好的);
    3. 一次创建,大小固定,持续使用;
    4. 读多写少(可以在每个Game Loop都绘制到On-Screen Canvas上,但是自身更新/变化的次数应该很少,避免每个Game Loop都更新);
    注释:
    1. 一般手机的Max Texture Size是2048,高端的机器可能会到4096或者8192,Canvas长宽任意一边超过这个大小都无法使用Texture做为自己的Buffer;
    2. 非加速的Canvas仍然使用普通的Bitmap作为自己的Buffer,这意味着它的绘制仍然使用CPU,并且它绘制到另外一个Canvas还需要先加载成Texture,而加速的Canvas本身就是一个Texture,所以它绘制到另外一个Canvas上只需要一次贴图的开销;
    3. WebKit默认的设置是128×128大小以内的Canvas不加速,UC和Chrome都使用了默认的设置;
    4. 把一个比较大,加速的Canvas绘制到一个比较小,不加速的Canvas会非常非常慢,这是因为浏览器需要从显存拷贝内容到普通内存,拷贝的速度很慢并且会造成GL渲染流水线的阻塞;
    5. 一个小技巧是,如果一个Canvas不再使用,可以将它的长宽设置为0,这样在JSVM的垃圾收集器还没有回收该Canvas对象时,浏览器就可以先释放它的Buffer,这样可以避免浏览器因为Buffer占用太多而不得不强制GC,不过总的来说最好还是自己建立缓存池;

    Rule #7 避免频繁调用getImageData,putImageData和toDataURL

    因为它们都会需要从显存拷贝内容到普通内存,或者相反,拷贝的速度很慢并且会造成GL渲染流水线的阻塞。所以不要在每个Game Loop都调用这几个API。

    Rule #8 如果需要最大帧率,优先使用requestAnimationFrame而不是Timer

    如果您的游戏只需要20或者30帧,那么就只能使用Timer。但是如果希望达到设备本身的最大帧率,则应该使用rAF,因为rAF可以让浏览器把网页绘制,Canvas绘制跟屏幕刷新保持同步,减少Canvas更新的延迟,并且在网页不可见的时候还可以自动停止rAF回调,避免无谓的浪费电池。

    Rule #9 图片资源大小应该对GPU友好

    1. 避免使用太多小图片,而是应该把它们拼接成一张大图;
    2. 拼接的图片长宽应该是2的幂次方,并且小于或者等于2048,512×512,1024×1024都是不错的选择;
    3. 拼接的图片应该尽量避免留下大量空白区域,造成无谓的浪费;
     
    • shuyong 8:43 下午 on 四月 13, 2014 固定链接 | 回复

      好文章。以后写的一些文档会参考这里的观点。

      不过这个文章有一个小遗憾。在“Rule #1 为Android而不是iOS而优化”中有一句:
      “Android平台并没有IOSurface的同等物(一块同时支持CPU读写和GPU读写的缓冲区)”

      这里虽然没有错,因为ANDROID平台确实没有提供现成的off-screen nativewindow / nativewindowbuffer。但是应用程序其实是可以自己实现相应的off-screen window / buffer的。这需要思维上有点突破,把自己当成是system engineer,而不只是application engineer。多看一些其它项目,理解interface / implement,就会有新的天地,新的想法了。

    • Roger 10:21 上午 on 四月 14, 2014 固定链接 | 回复

      实际上Android 的 GraphicBuffer 有类似的作用,也可以被应用所使用,但是它有很多限制,不如IOSurface那么灵活,并且不同SoC上的兼容性问题很严重,所以在实际使用中只能用于特定的场景。

      另外不明白这里跟system engineer有何关系,浏览器毕竟不是OS本身,它需要运行在各种不同的硬件/系统版本上,浏览器自身是没办法绕过硬件/系统本身的限制的,除非像Chrome OS/Firefox OS那样。

      • shuyong 11:40 下午 on 四月 14, 2014 固定链接 | 回复

        所谓system engineer,就是有一点可能性,就会尝试实现。Chrome / Firefox也是逐步总结出抽象的移植层,在各个平台上,保证上层都有相同的特性,又要保证可移植性。如果在某个平台没有底层支持,那就实现一个。最终就是要保证上层都有相同的特性。而上层的特性足够多了,足够稳定了,自然而然就想进化到Chrome OS/Firefox OS了。

        我不了解IOSurface,所以对比不了灵活性。但是我做过测试,Android 的 GraphicBuffer应该有足够的稳定性和兼容性,不论是直接调用系统原生的,还是我自己重新实现的。我从Android 2.3到4.3,硬件平台有高通,NVIDIA,FREESCALE,TI,SUMSANG。国产的芯片的系统好像也测过几个版本。GraphicBuffer的接口已经足够稳定,特性也有足够的一致性,否则ANDROID就很难移植和运行了。

        如果自己实现一个off-screen ANativeWindow,通过重载dequeueBuffer / queueBuffer,应该很容易实现CPU / GPU之间访问内存的ZERO-COPY算法的。

        • Roger 9:45 上午 on 四月 15, 2014 固定链接

          如果你了解Chrome,就知道Chrome在Canvas上的实现更保守(或者也可以说是通用)和低效… 所以你既然没有真正了解Chrome的实现,就别在这里乱讲了…

          Android本身对GraphicBuffer的使用比较简单,NativeWindow是Double/Triple Buffering的机制,不存在需要CPU/GPU “同时” 读写同一块Buffer的状况,而Canvas是必须Single Buffer的。如果你测试过Nvidia的芯片,就应该知道Tegra是不支持CPU/GPU “同时” 读写同一块Buffer的,如果一个GraphicBuffer已经通过EGLImage binding到一个Texture上,再试图去unlock它,在Tegra的芯片上就会陷入死锁,高通的Adreno 2xx也有类似问题,Adreno 3xx虽然支持,但是Unlock/Lock一次的开销在3~7ms之间…

  • Roger 3:35 pm on February 21, 2013 固定链接 | 回复
    Tags: , Canvas   

    GUIMark3 – 针对移动设备的HTML5 Canvas性能测试套件介绍 

    GUIMark3是针对移动设备的HTML5 Canvas性能测试套件,它设计了几种不同的场景,针对不同方面进行测试。所有的Demo都同样由一个60帧/秒的Timer来驱动绘制,所以理论上的最大帧率是60(实际上在不同的浏览器实现上可能会略有不同,在60上下浮动1~2帧),一般来说,超过50帧以上,考虑到误差的原因,个位数的参考意义已经不是很大,达到30帧以上的流畅度对于使用者来说已经基本可以接受了。测试里面Canvas都是固定大小的,在不同的设备上会根据屏幕的大小进行拉伸,但是Canvas本身的大小保持不变。

    GM3 Bitmap (Canvas大小:480×640)

    位图测试,主要测试Canvas贴图的性能,位图的绘制只是很单纯的1:1贴图,包括绘制地面背景,飞机,子弹和云朵,没有任何旋转和缩放,只有少量的半透明混合。应该说大部分针对移动设备的Canvas游戏最多的操作就是贴图,所以这个测试的结果对它们来说参考价值较大。GM3 Bitmap测试还有一个变种GM3 Bitmap Cache,它将地面,飞机,子弹,云朵等图片先分别绘制在不同的离屏Canvas上面,然后绘制这些Canvas而不是直接绘制图片,实际上这种方式对浏览器来说一般反而会更慢,消耗更多的内存。
     gm3_bitmap

    GM3 Compute (Canvas大小:480×480)

    这个测试实际上更多是在测试JS引擎本身的性能,它先通过一个物理模拟引擎计算出一束点,然后在这些点的位置上画上一条短线,因为更多的时间是耗费在物理模拟计算上面,对Canvas性能测试反而参考价值不大,除非你的Canvas游戏类似Cut the Rope或者Angry Bird一样,也需要大量的物理模拟计算。
     gm3_compute

    GM3 Vector (Canvas大小:480×640)

    这是一个针对Canvas矢量绘图的测试,需要对很多环形的路径进行渐变填充。一般来说矢量绘图较少会被使用,因为它比位图贴图要慢很多,不过你的Canvas游戏需要大量使用矢量绘图的话,这个测试的结果就比较有参考价值。
    gm3_vector

    更多的信息请参考作者的文章:http://www.craftymind.com/guimark3/
     
  • Roger 10:32 am on January 18, 2013 固定链接 | 回复
    Tags: Accelerated 2D Canvas, Accelerated Compositing, , Canvas,   

    Introduce My Work 

    图像

    我在LinkedIn上面的个人简介是:

    Roger at UC Mobile Ltd. (www.uc.cn), focus on graphics stack (rendering architecture) research of WebKit based Browser in Android platform, include the graphics stack of WebKit itself along with the display subsystem of Android platform, and design how to combine the best of both them to achieve butter graphics performance of page rendering.

    简单的说就是专注于Android平台基于WebKit的浏览器绘图堆栈的研究,可是,到底什么是所谓的”Android平台基于WebKit的浏览器绘图堆栈“,下面我用一个例子来说明。

    我们来考究一个最简单的HTML5 Canvas在Android平台基于WebKit的浏览器上的实现:

    1. 如果网页包含Canvas标签,或者由JavaScript动态生成,都会导致WebKit内核生成一个HTMLCanvasElement Node对象;
    2. 当JS从Canvas元素获得一个2D Context对象时(var ctx = canvas.getContext(“2d”)),实际上这个Context JS对象会跟一个C++的CanvasRenderingContext2D对象绑定,JS在这个Context上的全部调用都会被虚拟机转发给对应的CanvasRenderingContext2D对象;
    3. WebKit内部的2D绘图是基于GraphicsContext的,所以CanvasRenderingContext2D对象需要一个GraphicsContext来执行真正的2D绘图操作,它会向HTMLCanvasElement对象发出请求,通过它的drawingContext()方法返回一个GraphicsContext;
    4. 而HTMLCanvasElement则把这个请求转发给内部的ImageBuffer对象,所以真正用于绘图的GraphicsContext是由ImageBuffer提供的;
    5. ImageBuffer顾名思义,是用来管理一块位图缓存,默认情况下,它会分配一块内存,把这块内存封装成一个位图(在Android平台是SkBitmap),然后通过这个位图创建一个平台相关的绘图上下文PlatformGraphicsContext(在Android平台是SkCanvas和PlatformGraphicsContextSkia),最后把这个PlatformGraphicsContext封装成一个平台无关的GraphicsContext返回;
    6. 从上面可知,当JS通过Canvas元素获得的Context对象绘图时,最终是绘制到HTMLCanvasElement对象内部的ImageBuffer对象管理的一块内存里面;
    上面仅仅说明了JS调用本身的绘制路径,要最终在屏幕上看到结果,我们还需要把ImageBuffer里面的内容绘制到窗口上:

    1. 当JS执行Context对象的每一个绘图操作时,对应的HTMLCanvasElement对象都会对外广播一个内容发生变化的通知(canvasChanged);
    2. 这个通知会被转换成网页内容的某个区域的重绘请求;
    3. 这个网页内容的重绘请求会被WebKit内核的适配层拦截,在Android平台上,它最终会被转换成用于显示这个网页的View对象的某个区域的重绘请求(View.invalidate),重绘区域需要经过从网页的内容坐标系到View的显示坐标系的坐标转换;
    4. 当Android当前Activity的View Hierachy的某个View需要重绘时,Android会在稍后的某个一个时间点发起一个View Hierachy的遍历操作(ViewRootImpl.doTraversal);
    5. 在这个View Hierachy的遍历操作中,Android会生成一个Canvas对象,然后逐个调用View的onDraw方法,让View把自身的内容通过这个Canvas绘制到当前的窗口上,这个Canvas对象实际上从当前窗口对应的Surface对象中获得,而Surface对象需要从当前窗口的Graphics Buffer队列中取出一块空闲的Buffer(这个Buffer由系统的窗口混合器SurfaceFlinger分配),然后把这块Buffer封装成一个SkBitmap,再根据SkBitmap生成SkCanvas,再根据SkCanvas生成Java Canvas…
    6. 当用于显示网页的View对象的onDraw方法被Android系统调用到时,它需要重新把C++的SkCanvas对象从Java的Canvas对象中取出来,然后把这个SkCanvas包装成WebKit所需的GraphicsContext对象,传给WebKit让它去绘制网页,当WebKit绘制到对应这个Canvas元素的Render Object(RenderHTMLCanvas)时,RenderHTMLCanvas实际上会把这个绘制请求转发给它对应的HTMLCanvasElement对象(就是前面说的那一个),而HTMLCanvasElement则把自己的ImageBuffer对象通过传入的GraphicsContext最终绘制到当前窗口上;
    7. 当Android遍历完整个View Hierachy,所有View都通过传入的Canvas绘制到了当前的窗口上,但这时我们还未能在屏幕上看到更新的内容,因为实际上我们是绘制到了当前窗口的back buffer,Android还需要把绘制完的back buffer解锁,放回窗口的Graphics Buffer队列,通知SurfaceFlinger窗口的内容发生了变化(Surface.unlockAndPost);
    8. 当SurfaceFlinger收到窗口更新的通知时,它会切换窗口的前后台缓存(Page Flip),然后逐个把当前可见的窗口的front buffer拷贝到系统的framebuffer里面去,这样我们最终才在屏幕上看到Canvas绘制的结果(Android 4.0开始,如果硬件支持,可以省略掉将窗口front buffer拷贝到系统framebuffer这一步,由display hardware自己负责);
    上面的例子足够复杂吗,实际上已经省略了很多不太重要的部分,也简化了一些比较复杂的概念比如Android的Native Window和Window Compositing机制,并且这仅仅是一个最最最简单的实现!!!

    如果我们需要考虑支持:
    1. 支持像系统浏览器一样的多线程渲染架构或者像Chrome一样的多进程渲染架构;
    2. 支持Android的GPU加速绘图(从Android3.0 开始支持);
    3. 支持WebKit的图层混合加速(Accelerated Compositing);
    4. 支持硬件加速的2D Canvas实现(Accelerated 2D Canvas);
    上面的每一个选项都会让本来的流程再加倍的复杂,而这些就是我的主要工作内容 ^_^
     
    • unbug 10:38 上午 on 一月 18, 2013 固定链接 | 回复

      强文,营养丰富,收藏备读!

    • 一丝 10:55 上午 on 一月 18, 2013 固定链接 | 回复

      虽不明,但觉厉(PS,楼上的你好讨厌捏,老是抢在我前面。)

  • Roger 1:09 pm on January 19, 2012 固定链接 | 回复
    Tags: , , Canvas, GLSurfaceView, Hardware Accelaration, SurfaceView   

    Beyond Android Views – Window, Surface, Special Views, and More 

    主要内容:

    • 介绍隐藏在用户界面后面的系统底层模块 – Window, Surface,Canvas,和它们之间的关系与交互
    • 介绍一些特殊View的运作原理 – SurfaceView,GLSurfaceView,TextureView
    • 介绍硬件加速绘图与非硬件加速绘图的差异

    关于硬件加速的部分可以参考另外一篇文章:Android 硬件加速的2D绘图研究 

    PPT链接:beyond_android_views-window_surface_special_views_and_more_revised

    SlideShare:

     
c
Compose new post
j
Next post/Next comment
k
Previous post/Previous comment
r
回复
e
编辑
o
Show/Hide comments
t
返回顶部
l
Go to login
h
Show/Hide help
shift + esc
取消