渲染流水线中的光栅化(一)
Rasterization,光栅化,又称为栅格化,它用于执行绘图指令生成像素的颜色值。光栅化是渲染流水线中的一个重要环节,但是不同的 UI Toolkit 和不同浏览器渲染引擎使用的光栅化策略并不一样,本文主要讨论各种不同的光栅化策略和它们各自的优劣。
渲染流水线
上图是一个渲染流水线的极简版示意图,适用于大部分的 UI Toolkit 和浏览器渲染引擎,当然实际的细节可能会有出入。
- DOM/View - 构建或者改变 DOM 树(UI Toolkit 一般称为 View 或者 Widget);
- Style/Layout - 计算样式和重排版(又称为布局);
- Layerize - 图层化,将不同的 DOM/View 子树按一定的规则归属到不同的图层,构建或者更新图层树/列表;
- Paint - 绘制图层,输出 DisplayList(2D 绘图指令的列表);
- Rasterization - 执行 DisplayList 中的绘图指令,生成图层区域的像素颜色值;
- Composite - 将光栅化的结果最终输出到目标 Surface,如果目标 Surface 是一个 On Screen Window,那也就是输出到显示屏上;
我们一般将 1 ~ 4 归为渲染流水线的前半段,5 和 6 归为渲染流水线的后半段,在 Chromium 里面,将 Rasterization 和 Composite 归为 Graphics。
更多关于实际的渲染流水线设计,可以参考我之前的一些文章,比如Flutter 渲染流水线浅析。
光栅化策略
直接光栅化 (Direct Rasterization)
在所有光栅化策略中,直接光栅化是最简单的一种。它就是直接将所有可见图层的 DisplayList 中可见区域内的绘图指令,执行光栅化直接在目标 Surface 的像素缓冲区上生成像素的颜色值。如果是完全的直接光栅化,这时,其实就不需要后面合成的步骤了。
一般来说渲染的 Viewport 是由 Root Layer 当前的 Scroll Offset 和目标 Surface 的大小来决定。如果图层在 Viewport 范围内,它就是可见的。
间接光栅化
像 Android 和 Flutter,它们的 UI Toolkit 主要使用直接光栅化的策略,但是同时也支持间接光栅化。它们允许为指定图层分配额外的像素缓冲区,该图层的光栅化会先写入自身的像素缓冲区,渲染引擎再将这些图层的像素缓冲区通过合成输出到目标 Surface 的像素缓冲器。
无论是直接光栅化还是间接光栅化,它们都是所谓的同步光栅化,也就是说光栅化和合成通常都在同一个线程,即使不在同一个线程,也会通过线程同步的方式来保证光栅化和合成的执行顺序。这种同时使用直接和间接光栅化的方式,有时我们也称为即时光栅化(On Demand Rasterization)。
Android 提供了 View.setLayerType 允许应用来为指定 View 分配像素缓冲区,Flutter 目前应该没有提供类似的 API,只是内部根据一定的规则来决定为特定图层分配额外的像素缓冲区。
异步分块光栅化(Async Tiling Rasterization)
上图显示了 Google 搜索页面的图层(浅黄色)和分块(浅绿色)的边界
Chromium 使用的是异步分块光栅化的策略,除了一些特殊图层外(比如 Canvas,Video):
- 图层会按一定的规则切割成同样大小的分块,这些分块会覆盖整个图层;
- 在 Viewport 范围内或者附近的分块会分配大小跟分块相同的像素缓冲区,当 Viewport 发生变化时,会重新分配或者回收这些像素缓冲区;
- 光栅化是以分块为单位进行,每个光栅化任务执行对应图层的对应分块区域内的绘图指令,结果写入该分块的像素缓冲区;
- 光栅化和合成不在同一个线程执行,并且不是同步的,如果合成过程中某个分块没有完成光栅化,那它就会保留空白或者绘制一个棋盘格的图形(Checkerboard);
对于异步光栅化来说,为图层分配额外的像素缓冲区是必须的,而使用分块的方式比起分配一个完整大小的像素缓冲区有很多优势:
- 为超大图层分配一个完整大小的像素缓冲区可能超过硬件支持的范围;
- 超大图层只有部分可见,为不可见的部分分配像素缓冲区会导致内存的浪费;
- 如果一个图层只有部分区域发生变化,只需要重新光栅化关联的分块;
- 尺寸大小固定的小分块,可以通过一个资源池(Resource Pool)统一管理这些像素缓冲区,方便回收和重分配;