Draw
好,现在到了Draw这个步骤,当每个图块都被光栅化之后,合成器线程会为每个图块生成draw quads(在屏幕的指定位置绘制图块的指令,也包含了属性树里面的变换,特效等操作),这些draw quads会被封装在CompositorFrame对象里面,CompositorFrame对象也是Render Process的产物,它会被提交到Gpu Process中,我们平时提到的60fps输出帧率指的帧其实就是CompositorFrame。
Draw指的就是 把光栅化的图块,转换成draw quads的过程。
Display Compositor
Display
Draw操作完成之后,就生成了CompositorFrame,它会被输出到Gpu process,它会从多个来源的Render Process接收CompositorFrame。
多个来源:
- Browser Process也有自己的Compositor来生成Compositor Frame,这些一般是用来绘制Browser UI(导航栏,窗口等)
- 每次创建tab或者使用iframe,会创建一个独立的Render Process。
Display Compositor运行在Viz Compositor thread,Viz会调用OpenGL指令来渲染Compositor Frame里面的draw quads,把像素点输出到屏幕上。
VIz也是双缓冲输出的,它会在后台缓冲区绘制draw quads,然后执行交换命令最终让它们显示在屏幕上。
双缓冲机制:
在渲染的过程中,如果只对一块缓冲区进行读写,这样会导致一方面屏幕要等待去读,而GPU要等待去写,这样要造成性能低下。一个很自然的想法是把读写分开,分为:
- 前台缓冲区(Front Buffer):屏幕负责从前台缓冲区读取帧数据进行输出显示。
- 后台缓冲区(Back Buffer):GPU负责向后台缓冲区写入帧数据。
这两个缓冲区并不会直接进行数据拷贝(性能问题),而是在后台缓冲区写入完成,前台缓冲区读出完成,直接进行指针交换,前台变后台,后台变前台,那么什么时候进行交换呢,如果后台缓存区已经准备好,屏幕还没有处理完前台缓冲区,这样就会有问题,显然这个时候需要等屏幕处理完成。屏幕处理完成以后(扫描完屏幕),设备需要重新回到第一行开始新的刷新,这期间有个间隔(Vertical Blank Interval),这个时机就是进行交互的时机。这个操作也被称为垂直同步(VSync)。
垂直同步也会在后续进行介绍。
viz
viz是做什么的?
在 Chromium 中 viz
的核心逻辑运行在 GPU 进程中,负责接收其他进程(渲染进程?)产生的 viz::CompositorFrame(简称 CF)
,然后把这些 CF 进行合成,并将合成的结果最终渲染在窗口上。
CF是什么?
一个 CF 对象表示一个矩形显示区域中的一帧画面。内部存储了 3 类数据,分别是 CompositorFrameMetadata, TransferableResoruce 和 RenderPass/DrawQuad,如下图所示:
- CompositorFrameMetadata:CF的元数据,比如画面的缩放级别,滚动区域。。。
- TransferableResoruce :CF引用到的资源。
- RenderPass/DrawQuad:CF包含的绘制操作。
viz::RenderPass
由一系列相关的viz::DrawQuad
构成。
CF 是 viz
中的核心数据结构,它代表某块区域中UI的一帧画面,使用 DrawQuad 来存储 UI 要显示的内容。它代表了 viz 运行时的数据流。
CF合成
CF 合成指的是viz线程把 CF 中的内容或者多个 CF 合成到一起,形成一个完整的画面。
CF渲染
CF 的渲染主要由 viz::DirectRenderer
和 viz::OutputSurface
负责,他们将合成的结果渲染到程序选择的渲染目标上去。
CC
现在我们在统一的梳理一下cc的功能作用以及cc的工作流程。
我们先回顾一下,看看上面这个图,Blink 进行了DOM,Style,Layout,comp.assign,prepaint,paint。
我们可以发现,Paint是blink和cc对接的桥梁。
整体的流程其实可以 理解成:
Blink一顿操作 -> paint生成了cc模块的数据源(cc:layer)->commit->(Tiling->)Raster->Active->draw(submit)->Viz(呈像)
就是Blink一顿操作,并在paint阶段生成cc的数据源,cc进行一系列操作并最终在draw阶段将结果(CF)提交给viz。也就是说,Blink负责网页内容绘制,cc负责将绘制的结果合成并提交给viz。
cc的架构设计
cc的设计相对比较简单,我们可以把他理解成一个多线程调度的异步流水线,运行在 Browser 进程中的 cc 负责合成浏览器非网页部分的 UI,运行在 Renderer 进程中的 cc 负责网页的合成。
在chromium.googlesource.com/chromium/sr… how cc works官网有这样一张图,我觉得可以很好的反应cc的核心逻辑。
cc的多线程体现在在不同的阶段,cc运行在不同线程中,Paint
运行在 Main 线程,Commit
,Activate
,Submit
运行在 Compositor 线程,而 Raster
运行在专门的 Raster 线程。
下面我们开始对cc流水线的各个阶段进行分析。
Paint
Paint阶段会产生cc的数据源(cc:layer树),一个cc:layer会表示一个矩形区域的UI,它有很多子类,用于存储不同类型的UI数据:
- cc::PictureLayer。用于实现自绘型的UI组件,比如上层的各种 Button,Label 等都可以用它来实现。它允许外部通过实现
cc::ContentLayerClient
接口提供一个cc::DisplayItemList
对象,它表示一个绘制操作的列表,记录了一系列的绘制操作,比如画线,画矩形,画圆等。通过cc::PaintCanvas
接口可以方便的创建复杂的自绘 UI。cc::PictureLayer
还是唯一需要 Raster 的cc::Layer
。它经过 cc 的流水线之后转换为一个或多个viz::TileDrawQuad
存储在viz::CompositorFrame
中。 cc::TextureLayer
对应 viz 中的viz::TextureDrawQuad
,所有想要使用自己的逻辑进行 Raster 的 UI 组件都可以使用这种 Layer,比如 Flash 插件,WebGL等。cc::SurfaceLayer
对应 viz 中的viz::SurfaceDrawQuad
,用于嵌入其他的 CompositorFrame。Blink 中的 iframe 和视频播放器可以使用这种 Layer 实现。cc::SolidColorLayer
用于显示纯色的 UI 组件。cc::VideoLayer
以前用于专门显示视频,被 SurfaceLayer 取代。cc::UIResourceLayer/cc::NinePatchLayer
类似 TextureLayer,用于软件渲染。
Blink
通过以上各种 cc::Layer
来描述 UI 并实现和 cc 的对接。由于 cc::Layer
本身可以保存 Child cc::Layer
,因此给定一个 Layer 对象,它实际上表示一棵 cc::Layer
树,这个 Layer 树即主线程 Layer 树,因为它运行在主线程中,并且主线程有且只有一棵 cc::Layer
树。
Commit
Commit 阶段的核心作用是将保存在 cc::Layer
中的数据提交到 cc::LayerImpl
中。cc::LayerImpl
和 cc::Layer
一一对应,只不过运行在 Compositor 线程中(也称为 Impl 线程)。在 Commit 完成之后会根据需要创建 Tiles 任务,这些任务被 Post 到 Raster 线程中执行。
Tiling+Raster
在 Commit 阶段创建的 Tiles 任务(cc::RasterTaskImpl
)在该阶段被执行。Tiling 阶段最重要的作用是将一个 cc::PictureLayerImpl
根据不同的 scale 级别,不同的大小拆分为多个 cc::TileTask
任务。Raster 阶段会执行每一个 TileTask,将 DisplayItemList 中的绘制操作 Playback 到 viz 的资源中。 由于 Raster 比较耗时,属于渲染的性能敏感路径,因此Chromium在这里实现了多种策略以适应不同的情况。这些策略主要在两方面进行优化,一方面是 Raster 结果(也就是资源)存储的位置,一方面是 Raster 中 Playback 的方式。这些方案被封装在了 cc::RasterBufferProvider
的子类中,下面一一进行介绍:
cc::GpuRasterBufferProvider
使用 GPU 进行 Raster,Raster 的结果直接存储在 SharedImage 中。(前文以及提到过的硬件加速)cc::OneCopyRasterBufferProvider
使用 Skia 进行 Raster,结果先保存到 GpuMemoryBuffer 中,然后再将 GpuMemoryBuffer 中的数据通过 CopySubTexture 拷贝到资源的 SharedImage 中。GpuMemeoryBuffer 在不同平台有不同的实现,也并不是所有的平台都支持,在 Linux 平台上底层实现为 Native Pixmap(来自X11中的概念),在 Windows 平台上底层实现为 DXGI,在 Android 上底层实现为 AndroidHardwareBuffer,在 Mac 上底层实现为 IOSurface。cc::ZeroCopyRasterBufferProvider
使用 Skia 进行 Raster,结果保存到 GpuMemoryBuffer 中,然后使用 GpuMemoryBuffer 直接创建 SharedImage。cc::BitmapRasterBufferProvider
使用 Skia 进行 Raster,结果保存到共享内存中。
Raster最终会产生一个资源,这个资源被记录在了cc:PictureLayerImpl
中,他们会在Draw阶段被放在CF中。
Activate
在 Impl 端有三个 cc::LayerImpl
树,分别是 Pending,Active,Recycle 树。Commit 阶段提交的目标其实就是 Pending 树,Raster 的结果也被存储在了 Pending 树中。
在 Activate 阶段,Pending 树中的所有 cc::LayerImpl
会被复制到 Active 树中,为了避免频繁的创建 cc::LayerImpl
对象,此时 Pending 树并不会被销毁,而是退化为 Recycle 树。
和主线程 cc::Layer
树不同,cc::LayerImpl
树并不是自己维护树形结构的,而是由 cc::LayerTreeImpl
对象来维护 cc::LayerImpl
树的。三个 Impl 树分别对应三个 cc::LayerTreeImpl
对象。
Draw
Draw 阶段并不执行真正的绘制,而是遍历 Active 树中的 cc::LayerImpl
对象,并调用它的 cc::LayerImpl::AppendQuads
方法创建合适的 viz::DrawQuad
放入 CompositorFrame 的 RenderPass 中。cc::LayerImpl
中的资源会被创建为 viz::TransferabelResource
存入 CompositorFrame 的资源列表中。至此一个 viz::CompositorFrame
对象创建完毕,最后通过 cc::LayerTreeFrameSink
接口将该 CompositorFrame 发送到给 viz 进程(GPU进程)进行渲染。
总结
现在整个渲染流程就基本结束了,前端的代码已经变成了屏幕上的像素点了。