Updates from 九月, 2013 Toggle Comment Threads | 键盘快捷键

  • Roger 9:37 am on September 2, 2013 固定链接 | 回复
    Tags: , , , , , 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 10:40 am on October 24, 2011 固定链接 | 回复
    Tags: Hybiard, ,   

    为什么学习JavaScript 

    最近花了不少时间去学习JavaScript,以一般的理解,JS主要用于浏览器前端编程,也就是所谓的Web App。我自己目前是以本地客户端应用开发为主,也就是所谓的Native App,所以JS跟目前的工作交集不大,不过个人认为JS的学习和研究仍然是一件非常有意义的事情,下面是列举的一些理由。

     

    JavaScript的编程范式跟主流语言有很大的不同,跟Python这样的脚本语言也有比较大的差异,它从其它语言里面吸收了很多不同的概念并糅合在一起。学习JavaScript,可以帮助编程经验比较局限在C++/Java这样的主流语言的我拓宽思维的边界,吸收更多新的理念,及打破以往对编程和编程语言理解的一些局限性。

     

    JavaScript虽然是从浏览器前端编程中发展起来,目前也还是它最大的应用领域。但JS本身仍然是一门完整的面向对象的编程语言,只要能够为它提供一个合适的运行时环境,它就可以很好的应用于在其它的领域。而它作为编程语言的一些优点如下:

    1. JavaScript虚拟机的速度越来越快,以V8虚拟机为代表的即时编译技术的成熟使得构建更复杂的应用成为可能;
    2. JavaScript跟其它语言如C++/Java/.Net的binding越来越容易,使它很适合作为一门粘合剂语言,构建Hybird App;
    3. JavaScript对函数式编程和闭包的支持使得它很适合应用于异步编程,像跟网络相关的应用和以事件驱动为基础的GUI应用;
    4. JavaScript操作复杂的数据结构如树结构很容易,这使得它很适合用于GUI编程和网页编程(后者本来就是它最初的发展目的);

    JavaScript在浏览器前端编程的领域内继续迅猛发展,HTML5标准为浏览器的JS运行时环境增加了大量新的API,提供了更完整的功能,为使用JS编写更复杂和本地化的Web App提供了强有力的支持,而其它以浏览器运行时为核心的中间件如PhoneCap更是增强了通过JS直接访问和控制移动设备的能力,这些都进一步模糊了Web App和Native App的界限。

     

    其它的JS运行时环境的发展也使得JS在除了浏览器前端领域外的应用变得越来越重要,NodeJS构建了JS服务器编程的运行时,QML/JavaScipt构建了JS用于Qt的QML GUI编程的运行时,Windows 8的Windows Runtime构建了JS跟.Net混合编程的运行时等等。所以未来,JS在3个主要的编程领域,本地客户端,浏览器前端,服务器后端都会得到更广泛的应用,特别是本地客户端编程,目前主流的GUI框架和OS的发展都有把JS搭配HTML或者其它的UI脚本语言作为本地GUI应用构建的主要语言的趋势,通过JS跟C++/Java/.Net的Binding,Hybird Native App会变得越来越普遍。

     
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
取消