关于 Viz 的系列文章
Mojo 快速入门
因为后面的内容需要读者对 Mojo 有一定了解,如果以前没接触过的话,这部分内容提供了一个快速的入门。
简单的说 Mojo 是一个 Client/Service 的通讯机制,支持跨进程。一个 Mojo 链接建立的过程一般如下:
- Client 端创建一个 Mojo 接口对象;
- Client 端为该接口对象生成一个链接请求对象;
- Client 端将该链接请求对象发送给 Service 端(一般是通过另外一个已经建立链接的 Mojo 接口对象);
- Service 端接收到链接请求对象后,会跟一个对应 Mojo 接口的实现对象进行绑定,这样 Client/Service 的链接就建立起来了;
- Client 端通过上面的 Mojo 接口对象给 Service 端发送消息(通过接口的方法调用,可以在链接还没建立之前就发送);
- Service 端接收到该消息,会转交给之前创建的 Mojo 接口的实现对象处理(调用实现对象的对应方法);
- 如果该消息需要返回值,会将返回值回传给 Client 端;
如果是 Client 端需要接收 Service 端的回调,建立链接的过程则有所不同:
- Client 端创建一个 Mojo 接口对象;
- Client 端为该接口对象生成一个链接请求对象;
- Client 端将该链接请求对象跟一个对应 Mojo 接口的实现对象进行绑定;
- Client 端将上述的 Mojo 接口对象发送给 Service 端(一般是通过另外一个已经建立链接的 Mojo 接口对象);
- Service 端收到 Mojo 接口对象后,则可以使用该接口对象给 Client 端发送消息;
更多关于 Mojo 的内容可以阅读官方的文档 Intro to Mojo & Services。
合成器架构
当我们考察 Chromium 的合成器架构的时候,总是会有一些疑问:
- 为什么 Chromium 需要在 Layer Compositor 之上再构建一个 Display Compositor,并且还将 Display Compositor 包装成 Viz Service 服务的一部分,为什么 Layer Compositor 不能自己使用一个 Renderer 直出?
- Surface 跟 Layer 有什么不同?
我尝试根据自己的理解来回答上述的问题:
Surface 的确某种程度跟 Layer 有相似之处,但是 Surface 提供了一个更简单抽象的概念(CompositorFrame Queue),更容易用于结构简单的非网页内容的合成输出,比如浏览器 UI,插件等。
Surface 的父子关系并不是预先确定的,实际上 Surface 之间是相互独立的,它们的父子关系是由某个 Surface 当前的 CompositorFrame 包含一个 SurfaceDrawQuad 指向另外一个 Surface 形成的嵌套关系来决定的,默认 Display Compositor 会主动创建一个 Surface 作为 Root Surface 来 Attach 外部来源的 Surface。这种方式比较 Layer 需要事先确定树结构来说更为灵活,当然这也是因为 Surface 之间不会有太复杂的嵌套关系。
以 Surface/CompositorFrame 为基础构建的 Display Compositor,并且被包装成 Viz Service 服务的一部分,使得一棵 Surface 树是可以支持跨进程的(父子 Surface 来源不同的进程),这对于 OOPIF(进程外 iframe)和插件来说是必须的。
部分网页元素如 Video,VR/AR,Offscreen Canvas 理论上也可以使用 Layer,但是使用独立的 Surface,让 Layer 作为 Surface 的 placeholder,可以使得它们的后续更新能够跳过 Layer Compositor 冗长的处理步骤,直接通过 Display Composior 输出。从而使得这些元素本身的更新更高效,输出延迟更低,也更省电。这也是因为它们的内容来源不是网页本身,不需要通过 Layer Compositor 光栅化,并且不需要跟其它 DOM 元素同时更新。
但是反过来又有另外的问题了,假设我们只需要合成输出一个比较普通的网页,不需要绘制浏览器 UI,没有 Video,VR/AR,OffscreenCanvas 元素或者它们不使用 Surface,没有或者不打算支持进程外 iframe 和插件,目前 Chromium 复杂的合成器架构是不是反而增加了很多额外的开销,导致可能的性能下降和耗电?
我个人觉得这个问题的答案,很不幸地会是 Yes,跨进程的 IPC,复杂的 Display Composior 架构的确会带来一些额外的开销,对普通的网页来说,即使线程并发能够抵消一部分性能损失,但是它的绘制输出延迟和耗电还是会增加,有时鱼和熊掌就是不可得兼。
下面的内容会继续介绍 Chromium 目前在 Android 平台的合成器架构,在普通网页上的三种不同的实际应用方案。
Android WebView 的合成器架构
Android WebView 的合成器架构跟 Chromium 其它的 Configurations 有很大的差异,造成这种现象的原因主要是:
- WebView 没有自己独立的 Window 作为 Display 的 OutputSurface,而是需要绘制到 WebView 所在 Activity 的 Window;
- WebView 的 Display Compositor 运行在 Android Render 线程,也就是 Android UI 的 GPU 线程,它不能自己控制合成输出的时机,只能等待 Android UI 的回调;
- Android Render 线程没有消息循环(或者说没有对外暴露),WebView 无法控制该线程的消息处理,只能通过在 UI 线程 Invalidate WebView 然后等待 Android Render 线程回调的方式来获得合成输出的机会;
在 Android WebView,实际上是没有 Viz Service 的,WebView 自己做了一套特殊的适配机制 SynchronousCompositor,将 Layer Compositor 和 Display Compositor 联系起来,而没有通过 Viz Service 服务。
在 Android WebView 合成器架构中:
对接 Layer Compositor 的 LayerTreeFrameSink 的实现是 SynchronousLayerTreeFrameSink。Layer Compositor 输出 CompositorFrame 并不是 cc 自己主动 Push,而是等待 SynchronousCompositor 的 Host 端向 Proxy 端发起请求。Host 端在 Browser 的 UI 线程(部分接收回传消息的对象在 IO 线程),Proxy 在 Renderer 的 Compositor 线程,之间通过 Mojo 通讯。Host 端在 WebView.onDraw 被调用的过程中,请求 Proxy 输出 CompositorFrame,Proxy 通过 SynchronousLayerTreeFrameSink 的回调获得 CompositorFrame 并返回给 Host,这是一种 Pull 的模型,区别于其它 Configurations 使用的 Push 模型。
SurfacesInstance 包含了一个 Display Compositor 的实例,同时也包括一个 FrameSinkManager 的实现作为单机版的 Viz Service,这个 Display Compositor 的实例供所有 WebView 共享,它的 OutputSurface 实际上只是当前 GL Context Window Surface 的 Wrapper。
HardwareRenderer 会通过 SurfacesInstance 申请一个 Surface,它被一个 CompositorFrameSinkSupport 对象所持有,跟所属 WebView 对应。当 HardwareRenderer 被 Android Render 线程回调获得合成输出的机会时,会从 SynchronousCompositor 的 Host 端获取 CompositorFrame,然后置入申请的 Surface,再请求 SurfacesInstance 的 Display Compositor 将该 Surface Attch 到 Root Surface 上,最后合成输出。
因为缺少完整的 Viz Service 服务,也使得 Android WebView 当前的合成输出功能有较大的局限性,每个 WebView 现在只有一个对应 Surface 对应网页本身,不支持 OOPIF,插件,独立 Surface 的 Video 等,当然 WebView 也不需要自己来绘制 UI。
理论上 WebView 应该也可以支持完整的合成输出功能,主 Surface 继续沿用目前 Pull 的模型,次级 Surface 可以使用标准的 Push 模型。不过目前来说还没有更多的信息。
非 OOP-D 的合成器架构
对于作为独立浏览器的 Chrome for Android 来说,它的合成器架构跟 Android WebView 的特殊用法差异比较大,这才是 Viz 的标准用法。OOP-D(进程外 Display Compositor)是新的合成器架构,在 M75 版本已经默认开启,不过在这里我们还是先介绍一下非 OOP-D 的合成器架构。
无论是否使用 OOP-D,当 Renderer 创建 Layer Compositor 的时候,它都是创建一个 AsyncLayerTreeFrameSink 作为 CompositorFrame 的提交目标对象。AsyncLayerTreeFrameSink 在 Compositor 线程被初始化,同时建立了 CompositorFrameSink 接口跟 Viz Service 端的链接,后续会使用该接口向 Display Compositor 提交 CompositorFrame。
Browser 端的合成器位于 CompositorImpl,CompositorImpl 内部有一个 CompositorDependencies 单例,它包括了一个 HostFrameSinkManager 作为 FrameSinkManager 接口的 Wrapper。没有开启 OOP-D 时,CompositorDependencies 会直接创建一个 FrameSinkManagerImpl 对象作为 HostFrameSinkManager 的 Local Manager,在本地直接处理 Renderer 发过来的 CompositorFrameSink 接口的链接请求。
同时 CompositorImpl 还会在本地创建一个 Display Compositor,通过上面的 FrameSinkManagerImpl 来创建和访问 Surface,当 Renderer 的 Layer Compositor 提交新的 CompositorFrame 的时候,就会触发该 Display Compositor 的合成输出。
![None OOPD](http://static.zybuluo.com/rogeryi/9n09ph2nnnh2c9ygbf4devlc/none-oopd-viz.png?x-oss-process=image/resize,w_1400/format,webp)
为了调试方便,截图使用的 Chromium 都运行在单进程模式
上图显示了页面被拖动时绘制一帧的流程,从 Browser UI 线程收到 VSync 开始,Layer Compositor 收到 BeginFrame 信号后产生新的 CompositorFrame 发送给位于 Browser UI 线程的 Display Compositor 合成输出,合成输出的 GL 指令在 GPU 线程被执行。
![None OOPD](http://static.zybuluo.com/rogeryi/sgskineghxxmntcnhauhubzi/none-oopd-viz-2.png?x-oss-process=image/resize,w_1400/format,webp)
非 OOP-D 的合成器架构,绘图来源自 Compositor Stack
参考上图,实际的合成器架构比上面的描述要复杂,在 Chrome for Android,Browser 和 Renderer 都会各创建一个 Layer Compositor,Browser 创建的 Layer Compositor 会创建一个 SurfaceLayer 作为 Renderer Layer Compositor 对应 Surface 的 placeholder,Browser Layer Compositor 同时还可以 Attach 其它 UILayer 用来绘制浏览器 UI,当 Display Compositor 聚合 Surface 时,Browser Layer Compositor 对应的 Surface 会嵌套 Renderer Layer Compositor 对应的 Surface,网页和浏览器 UI 最终被聚合成一个完整的 CompositorFrame 一起合成输出。
OOP-D 的合成器架构
![OOPD](http://static.zybuluo.com/rogeryi/mokvk5b9lqg3fel5jswushxm/oopd-viz-2.png?x-oss-process=image/resize,w_1400/format,webp)
OOP-D 的合成器架构,绘图来源自 Compositor Stack
对于 OOP-D 的合成器架构,CompositorDependencies 会发送一个 FrameSinkManager 接口的链接请求给在 Viz 进程 VizCompositor 线程的 Viz Main Service,然后把该 FrameSinkManager 接口传递给 HostFrameSinkManager。当 HostFrameSinkManager 接收到 Renderer 端对 CompositorFrameSink 接口的链接请求时,它会通过 FrameSinkManager 接口转交给远端的 Viz Service 处理。
void HostFrameSinkManager::CreateCompositorFrameSink(
const FrameSinkId& frame_sink_id,
mojom::CompositorFrameSinkRequest request,
mojom::CompositorFrameSinkClientPtr client) {
...
frame_sink_manager_->CreateCompositorFrameSink(
frame_sink_id, std::move(request), std::move(client));
}
frame_sink_manager_ 在开启 OOP-D 时,是一个 Mojo 接口对象,如果没有开启 OOP-D,则指向一个 Local FrameSinkManagerImpl 对象
CompositorImpl 同样需要为 Browser Layer Compositor 建立一个 CompositorFrameSink 接口的链接,它是一个 Root CompositorFrameSink,远端的 Viz Service 在建立该 CompositorFrameSink 接口链接的同时,也创建了一个 Display Compositor 用于合成输出。
![OOPD](http://static.zybuluo.com/rogeryi/4okgbz3r3ah5yg0uxxqpyu3m/oopd-viz.png?x-oss-process=image/resize,w_1400/format,webp)
为了调试方便,截图使用的 Chromium 都运行在单进程模式
从上图我们可以看到跟非 OOP-D 相比,除了 VSync 的触发,Display Compositor 的合成输出都迁移到了 Viz 进程的 VizCompositor 线程外,其它并没有太大的差异。不过 Display Compositor 的迁移仅仅是 OOP-D 的第一阶段,真正的差异会在后续的改动中体现,包括:
- Display Compositor 移除 Command Buffer 的使用,因为 VizCompositor 线程和 GPU 线程已经同在一个进程,它们之间可以使用线程通讯而不需要使用 Command Buffer;
- Display Compositor 移除 Command Buffer 后,可以使用 Vulkan 来取代 GL,因为 Command Buffer 不支持 Vulkan 也不打算支持,所以 1 是 2 的前置条件;