设为首页收藏本站

LUPA开源社区

 找回密码
 注册
文章 帖子 博客
LUPA开源社区 首页 业界资讯 技术文摘 查看内容

Android字体渲染器—使用OpenGL ES进行高效文字渲染

2014-6-16 14:54| 发布者: joejoe0332| 查看: 9367| 评论: 0|原作者: chris|来自: 伯乐在线

摘要:   任何有多年客户端开发经验的开发者都应该知道复杂的文字渲染是怎么工作的。至少在2010年以前,我刚开始写libhwui的时候(这是一个基于Android2.0的2D绘画库),我就意识到处理文字有时会比其他方面更复杂,特别 ...

点阵化和缓存

字体渲染器的每一个绘制方法都是和字体相关的。字体用于缓存个别字形符号,而字形符号又被存储在缓存结构中(缓存结构可以包含不同字体的字形符号)。缓存结构是持有多个缓冲区的一个重要的对象,有block集合、pixel缓冲区、OpenGL结构处理器,还有点阵缓冲区(也就是网格)。

1-qK4rIi_HDsEYPQQxFK5uPg

这个对象存储的数据结构比较简单:

  • 在字体渲染器中字体是存储在一个LRU缓存中的;
  • 字形符号分别存储在对应的map字体集合中(key就是字形文件的identifier);
  • 缓存结构使用一个块链表集合来记录空间的大小;
  • 像素缓冲区是一个uint8_t或者uint32_t类型的数组(作alpha值和RGBA的缓存);
  • 网格其实就是一个顶点数组,带有两个属性:x/y位置和u/v坐标;
  • 一个GLuint的处理器。

字体渲染器对不同类型的缓存结构提供了几种缓存纹理实例,也就是根据不同的大小区分,这个大小可能会根据不同设备而有所不同,这里这里说的是默认的大小(缓存的数量是硬编码的):

  • 1024*512 alpha缓存。
  • 2048*256 alpha缓存。
  • 2028*512alpha缓存。
  • 1024*512alpha缓存。
  • 2048*256alpha缓存。

当缓存纹理对象创建之后,其对应的缓冲区不会自动分配空间,除了1024*512的alpha缓存总是自动分配外,其它的都是根据需要来分配空间。

字形符号以列的形式打包在纹理中,只要字体渲染器遇到没有缓存的符号,它就会向缓存纹理请求响应的类型(存储在以上的有序列表中),然后缓存该符号。

这是上述的blocks列表使用到的地方,这个列表包含了当前已分配的列和所有未分配的空间。如果字形符号和已经存在的列匹配,那该字形符号就会被加到该列的底部。

如果所有列都被占用,从左边的剩余空间开辟新列。因为所有字体都是等宽的,渲染器会把每个字形的宽度弄成4像素的倍数(默认是4像素)。这是对列的重利用和字形打包的一个折衷,这个打包目前还不是很好,但是实现起来比较快。

所有的字形符号都存储在一个含有1个像素边框的结构中,这样在双线过滤采样的时候可以避免伪迹的产生。

在文字带有缩放变形操作的渲染中,了解文字何时被渲染也是非常重要的。这个变形操作直接到Skia/Freetype来处理,这就意味着字形符号是在缓存结构中变形存储的。这样可以改善渲染的质量。幸运的是,文字一般很少做缩放动画效果,就算是使用了,也只是设计很少的字形符号。本人做过很多实验,也没有找到一个实际使用的场景。

还有其它关于paint的属性会影响字形符号的栅格化和存储的:粗体、斜体、还有X缩放(在Canvas上做矩阵变换)、字体风格以及线条宽度等。

栅格化的可选方案

事实上,还有其它的方式去在GPU上处理文字字形符号。可以直接被渲染程向量,但是这样做开销很大。我调查过标记距离字段的方法,但是简单实现的时候遇到了精度的问题(创建曲线的时候会不稳定)。

本人建议读者可以看看Glyphy这个项目。这是一个开源库,作者是Harfbuzz。项目在标记距离字段技术上进行延伸,同时也解决了精度的问题。我暂时没有花太多时间看这个项目。但是上一次在做着色器的时候,发现这种技术在Android上是被禁止使用的。

预缓存技术

字形符号缓存是一定要做的。如果做预缓存的话,效果会更好。因为libhwui是一个延迟的渲染器(和Skia的快速模式正好相反),所有屏幕上出现的字形都是一帧一帧开始的。在一系列的显示操作(批处理和合并操作)中,字体渲染器需要尽可能多地缓存字形符号。

使用预缓存技术的主要优势在于,可以完全或者最小化纹理加载的时间。纹理加载操作是消耗非常大的,它会推延CPU或者GPU。甚至在帧渲染过程中,改变纹理还会在GPU体系结构带来更多内存的压力。

ImaginationTech的PowerVRml SGX GPUs使用了延迟叠加技术架构,可以提供很多有趣的特性。但如果在渲染帧时需要修改纹理,会强制要求驱动程序对纹理进行复制。因为字体结构相当大,如果不好好处理纹理加载的话,很容易就内存耗尽了。

这样的场景确实发生在Google Play的一个应用中。这个APP是一个简单的计算器,仅使用一些数学符号和数字进行简单的绘制按钮。字体渲染器在某的时候甚至渲染不出第一帧。因为按钮是连续进行绘制的,每一个按钮都会触发一个纹理加载,然后复制整个字体缓存。系统根本没有这么多内存去存储这么多缓存的备份。

清空缓存

因为用作字形缓存的纹理是非常大的,它们有时会被系统回收再利用,以便为其它程序更多的RAM。

当用户隐藏当前的应用时,系统给应用发送一条消息要求释放尽可能多的内存。很明显,这就需要销毁最大的字形缓存结构。在Android中,这个大缓存结构就是所有字形的缓存。除了默认第一个创建的以外(1024*512的默认缓存)。

纹理结构在没有存储空间的时会被清空。字体渲染器使用LRU算法对素有字体进行记录,仅仅是记录而已。如果需要,就会根据最近最少使用的纹理来清除内存。目前没有提供这个操作,但是它确实是一个不错的优化策略。

批处理和合并操作

Android4.3引入的绘制批处理和合并操作是一项重要的优化,彻底减少了大量往OpenGL驱动发送指令的问题。

为了进行合并操作,字体渲染器在进行多种绘制调用的时候会缓存文字,每个缓存纹理都会拥有一个客户端的2048 quads的数组(1 quad = 1 glyph)。当调用lilbhwui中的一个文字绘制API时,字体渲染器获取合适的网格为每个字形符号进行位置和u/v坐标的绘制。网格在批处理的末端被发送到GPU上(由延迟显示系统决定)。或者当一个quad的缓冲区满了的时候,可能会出现多网格渲染同一个字符串的情况——一个字符缓存占用一个网格。

这个优化过程很容易实现,对显示效果帮助也很大。因为字体渲染器使用多缓存结构,所以在一个字符串的渲染过程汇总,可能字形符号会来自不同的纹理。如果没有批处理好合并操作的话,每个绘制调用都要传递给GPU。字体渲染器就需要不断切换不同的缓存结构,这样会带来很大的消耗。

在测试字体渲染器的时候,我已经在一个测试App中发现了这个问题。这个App只是简单地用不同的样式和大小渲染一句“hello world”。其中字母“o”被存储在不同的纹理中,和其它的字符不一样。这种情况导致字体渲染器开始时只绘制了“hell”,然后渲染“o”,然后再渲染“w”,然后在渲染“o”,接着才是“rld”。这5个绘制调用和5个纹理进行绑定连接后,只有其中两个是实际需要的,现在渲染器先绘制“hell w rld”,然后在一起绘制两个“o”,这就是批处理和合并操作的好处了。


酷毙

雷人

鲜花

鸡蛋

漂亮
  • 快毕业了,没工作经验,
    找份工作好难啊?
    赶紧去人才芯片公司磨练吧!!

最新评论

关于LUPA|人才芯片工程|人才招聘|LUPA认证|LUPA教育|LUPA开源社区 ( 浙B2-20090187 浙公网安备 33010602006705号   

返回顶部