@rogeryi
2015-05-05T14:15:12.000000Z
字数 11025
阅读 5517
Compositor
Rendering
作者: 易旭昕 (@roger2yi)
说明: 访问 Cmd Markdown 版本可以获得最佳阅读体验
本文主要描述 Chromium Compositor (下文简称 cc) 的架构设计和在 Chromium Android WebView (下文简称 CAW) 上的实现细节。cc 是一个基于图层的合成器,类似于 iOS/MacOS 的 CoreAnimation,它包含了如下特性:
- 支持图层的动画,比如 Transform 和 Opacity;
- 支持输入事件的处理,比如图层的滚动;
- 支持选择同步或者异步的合成模式;
- 同时支持硬件和软件的输出模式;
- 支持多个合成器构成级联的关系,子合成器的输出是父合成器的输入;
cc 被设计为一个通用的合成器,不仅仅用于合成网页的内容,只要给 cc 输入一个合法的图层结构,就可以通过它合成任意的内容。实际上在非 CAW 的其它平台上,浏览器的其它 UI 元素也是通过 cc 进行合成,而 Blink 输出的网页内容,也必须先在 Content Renderer 里面转换成一棵合法的 cc::Layer 树才可以通过 cc 进行合成。
也就是说,Content 必然会包含一个 cc 的实例用于对接 Blink,但是 cc 本身并不依赖于 Blink 和 Content,它可以独立地被使用,我们可以在 Content,WebView 或者 Browser 里面创建额外的 cc 实例,只有认识到这一点,我们才能够更好地理解 cc 的设计理念。
我们经常可以在官方文档和讨论中看到 cc instance 这个术语,而一个抽象的 cc 实例,一般是指包含以下主要对象的一个组合:
从上图可以看出,cc 可以分为 main side 和 impl side,main side 是合成内容的来源,impl side 负责合成的输出,main side 所在的线程称为 main thread,impl side 所在的线程称为 impl thread 或者 compositor thread,它们可以是不同的线程,也可以是同一个线程,这取决于某一个具体 cc 实例的实际配置。
名称 | 说明 |
---|---|
LayerTreeHost | cc 在 main side 的宿主,负责管理 main side 的图层树,当 main side 发生变化时,负责向 impl side 发起 commit 请求 |
Layer | cc 在 main side 的一个合成图层,包含了图层的属性和内容,Layer 本身是一个抽象类型,不同的图层有不同的实现 |
Proxy | 顾名思义,负责 cc main side 和 impl side 之间的通讯和数据交换,Proxy 本身是一个抽象类型,拥有多个不同的实现,cc 实例的线程模型会决定它所使用的 Proxy 实现 |
Scheduler | 由 Proxy 所拥有,根据 cc 当前的状态和自身的调度策略,负责 cc 任务的调度,实际的调度策略会受到 cc 实例的属性设置的影响 |
LayerTreeHostImpl | cc 在 impl side 的宿主,负责管理 impl side 的图层树和合成输出,LTHI 管理两棵图层树,active tree 是当前正在输出的,pending tree 是准备输出的,当 pending tree 准备好后,它就会取代当前的 active tree,成为新的 active tree |
LayerTreeImpl | 一棵 impl side 图层树的管理者 |
LayerImpl | 跟 Layer 相对应,cc 在 impl side 的一个合成图层,包含了图层的属性和内容,LayerImpl 本身是一个抽象类型,不同的图层有不同的实现 |
Renderer | LTHI 首先生成当前 LayerImpl 树的 RenderPass,然后调用 Renderer 绘制 RenderPass 输出下一帧,Renderer 是一个抽象类型,拥有多个不同的实现 |
OutputSurface | 为具体的 Renderer 提供绘制的目标,OutputSurface 是一个抽象类型,拥有多个不同的实现 |
ContextProvider | 为 OutputSurface 的绘制提供绘图上下文,ContextProvider 是一个抽象类型,拥有多个不同的实现 |
不同的具体 cc 实例,它们可能会拥有不同的 Proxy,Renderer,OutputSurface 和 ContextProvider 的实现。有了这些基础概念后,我们就比较容易理解 BeginMainFrame,Commit,BeginImplFrame 这些术语的实际含义了。
在 Android 4.4 WebView 上,Chromium 只使用了一个合成器,也就是只有一个 cc 实例位于 Content 层中,负责将网页的内容绘制到当前 Activity 所创建的 Window 上。
这个合成器的 main side 运行在 Render 线程,跟 Content Renderer 和 Blink 对接,从 Blink 获取网页图层结构和内容,合成器的 impl side 运行在 Android UI 线程,在 WebView.DrawGL 的调用过程中合成图层到当前的 Window 上。
名称 | 实现 | 说明 |
---|---|---|
Proxy | ThreadProxy | 多线程合成器的 Proxy 实现 |
Renderer | GLRenderer | 直接绘制 LTHI 输入的 RenderPass, 输出 GLFrame |
OutputSurface | SynchronousCompositorOutputSurface | 这个是 CAW 专用的 OutputSurface,对接 Content 层的 SynchronousCompositor,实现同步合成模式 |
ContextProvider | ContextProviderInProcess | 单进程版本的 ContextProvider 的实现,提供一个 GLInProcessContext 的绘图上下文,其中的 Command Buffer 不需要跨进程传递缓存的 GL Commands |
所谓同步合成模式是指:
- 合成器的输出是由上层驱动的,驱动的路径是 WebView -> Content -> cc。如果网页的内容发生变化,cc 必须先通知 WebView 输出下一帧(最后调用 WebView.invalidate),而当 WebView 要求绘制当前帧时,cc 必须立即满足 WebView 的绘制要求,同步进行绘制;
- 当 cc 处理输入事件并导致某个图层滚动时,这个图层的滚动必须在 WebView 下一帧的绘制中生效;
- 当 WebView 的大小和位置坐标发生变化,新的 Viewport 会在下一帧的绘制请求中传递给 cc,并且需要在这一帧的绘制中立即生效;
CAW 使用同步合成模式,这是因为在 CAW 上,Chromium 必须对外提供一个具备标准 Android View 行为的 WebView,这意味着:
- Chromium 在 CAW 上没有自己独立的输出窗口,也没有自己独立的合成器线程,Android UI 线程扮演了合成器线程的角色,网页的绘制是输出到 WebView 所属 Activity 所创建的 Window 上,并且必须在 UI 线程上,在 WebView.DrawGL 调用中进行实际的绘制;
- WebView 的滚动位置,大小,和位置坐标的改变等都需要在 Android 发出的 View Hierarchy 下一帧绘制请求中立即生效;
为了适配 Android 5.0 多线程的渲染架构,Chromium 在 CAW 上引入了 Ubercomp 级联合成器的设计,CAW 将拥有两个合成器,分别是子合成器和父合成器,子合成器的输出是父合成器的输入。Content 拥有的是子合成器,跟 4.4 比较类似,它用于跟 Blink 对接,接受来自 Blink 的输入,WebView,确切地说是 AwContents, 会创建另外一个合成器,它是所谓的父合成器,接收来自子合成器的输出,完成最终绘制。
Content 所拥有的子合成器跟 4.4 版本的比较,除了 impl side 的输出不一样外,其它部分都比较相似,main side 运行在 Render 线程,跟 Content Renderer 和 Blink 对接,从 Blink 获取网页图层结构和内容,impl side 运行在 Android UI 线程,在 WebView.onDraw 的调用过程输出一个 CompositorFrame。
名称 | 实现 | 说明 |
---|---|---|
Proxy | ThreadProxy | 多线程合成器的 Proxy 实现 |
Renderer | DelegatingRenderer | 绘制 LTHI 输入的 RenderPass,但是输出的是 CompositorFrame 而不是 GLFrame |
OutputSurface | SynchronousCompositorOutputSurface | 这个是 CAW 专用的 OutputSurface,对接 Content 层的 SynchronousCompositor,实现同步合成模式 |
ContextProvider | ContextProviderInProcess | 提供一个不需要 GLSurface,Onscreen 的 GLInProcessContext |
WebView 所拥有的父合成器跟子合成器差别很大,首先它运行在单线程模式下,无论是 main side 和 impl side 都运行在 Android Render 线程,Android Render 线程跟 Chromium Render 线程完全没有关系,也不是同一个线程。WebView 会在 sync mode DrawGL 时将 CompositorFrame 从子合成器取出,传递给父合成器,然后在 draw mode DrawGL 时通过父合成器绘制 CompositorFrame,输出 GLFrame。
下图是 Android 5.0 多线程渲染架构的一个渲染流程示意图,供参考。
名称 | 实现 | 说明 |
---|---|---|
Proxy | SingleThreadProxy | 单线程合成器的 Proxy 实现 |
Renderer | GLRenderer | 绘制 LTHI 输入的 RenderPass,输出 GLFrame |
OutputSurface | ParentOutputSurface | 这个是 CAW 父合成器专用的 OutputSurface,实现很简单,基本上就是 Flush 一下 GLContext |
ContextProvider | ContextProviderInProcess | 提供一个需要 GLSurface,Onscreen 的 GLInProcessContext,跟子合成器的 InProcessCommandBuffer 使用同一个 InProcessCommandBuffer::Service,保证 GL Commands 执行的顺序性 |
关于父合成器的额外说明
理论上父合成器不需要一个完整的 cc 实例来绘制 CompositorFrame,只需要 impl side 用于输出的部分 - Renderer,OutputSurface,ContextProvider 就够了,实际上一个完整的 cc 实例反而造成了额外的时间开销,增加了一个无谓的 main side 到 impl side 的 commit 动作。
在官方论坛上咨询的结果是:因为 Renderer 不是 cc 的 Public API,在当前的设计中也还不能独立地被使用,其它模块要使用合成器必须要创建一个完整的 cc 实例,不过这一点未来有可能会发生改变。
目前 U4 WebView 的代码是基于 5.0 的代码,所以合成器的整体架构跟 5.0 WebView 是一样的。不过,我们在 4.x 上做了一些兼容性处理,在 WebView.onDraw 后 fake 一个 sync mode DrawGL,将 CompositorFrame 从子合成器传递到父合成器,而父合成器在接下来的 WebView.DrawGL 中被调用,直接运行在 Android UI 线程。这种方式比较简单安全,但是存在一定的效率损失。
无论是 4.4 的 WebView 或者 5.0 的 WebView 的合成器都支持软件输出模式,当 WebView 所在的 Window 不开启硬件加速或者需要截图时,使用的就是软件输出模式,此时使用的是 Content 所拥有的 cc 实例。
名称 | 实现 | 说明 |
---|---|---|
Renderer | SoftwareRenderer | 在软件输出模式下,LTHI 会创建一个临时的 SoftwareRenderer,直接将 RenderPass 绘制到 Window FrameBuffer 或者 Bitmap 上 |
OutputSurface | SynchronousCompositorOutputSurface | 在软件输出模式下,返回一个 SoftwareOutputDevice,为 Renderer 提供 FrameBuffer 或者 Bitmap 的访问,实际上就是提供一个已经设置好对 FrameBuffer 或者 Bitmap 进行绘制的 SkCanvas |
要理解 cc 里面资源的概念,我们首先要理解 cc 跟 gpu 模块,也就是 CommandBuffer 之间的关系。CommandBuffer 提供了一套类似 GL 的 API 接口,其中一部分对应真正的 GL API,而另外一些则完全是 Chromium 自己的扩展 API,用来对外提供 CommandBuffer 自身的功能。cc 是 CommandBuffer 最主要的 Client,它对 CommandBuffer 的调用都是通过这套 API 接口来进行。cc 定义了 ContextProvider 这个抽象接口,由 gpu 模块来实现,通过 ContextProvider,cc 可以获得一个 CommandBuffer GL 上下文,下面的代码显示了 cc 如何通过 ContextProvider 获得 CommandBuffer GL 上下文和如何像调用真正的 GL API 一样使用它:
GLES2Interface* ResourceProvider::ContextGL() const {
ContextProvider* context_provider = output_surface_->context_provider();
return context_provider ? context_provider->ContextGL() : NULL;
}
GLint ResourceProvider::GetActiveTextureUnit(GLES2Interface* gl) {
GLint active_unit = 0;
gl->GetIntegerv(GL_ACTIVE_TEXTURE, &active_unit);
return active_unit;
}
cc 里面的 Resource,基本上相当于一个句柄,这个 Resource 对应的各种 Buffer,包括用于光栅化和纹理上传的 Pixel Buffer 和合成时使用的 Texture,都是由 CommandBuffer 负责分配,而 cc 通过对应的 ID 来持有和访问这些 Buffer。
上图显示了 Resources 相关的主要对象,黄色标识的为 cc::Resources 模块,红色标识为 gpu 模块。TileManager 在分派 Tile 的光栅化任务前,需要通过 ResourcePool 为 Tile 分配一个 Resource,如果 ResourcePool 没有可用的 Resource,它需要通过 ResourceProvider 创建。Tile 关联的 Resource 实际上只是一个 resource id,它用于映射到 ResourceProvider 内部管理的 ResourceProvider::Resource 对象,而 ResourceProvider::Resource 则通过 gl_id,gl_pixel_buffer_id 来持有和访问纹理缓存和光栅化缓存。
在 Android 4.4 的时候,为 ResourceProvider::Resource 分配的缓存是 GpuMemoryBuffer,内部的实现是 Android GraphicBuffer,但是在 5.0 的时候又改成了使用普通的纹理,关于 4.4 的部分,可以参考这里 Resource 的创建和使用
除了还在完善中的 GPU 光栅化外,其它光栅化的方式差别都不太大,Android 4.4 的时候,为 Resource 分配的光栅化缓存是 GpuMemoryBuffer,相应的光栅化器是 ZeroCopyRasterWorkerPool(原来的名字叫 ImageRasterWorkerPool,后来改了名字),对应的纹理上传方式是 zero copy texture upload,上述内容可以参考 Rasterize。
当前 Chromium 的代码(40.0.2214.89),在 CAW 上为 Resource 分配的光栅化缓存是 PixelBuffer,相应的光栅化器是 PixelBufferRasterWorkerPool,对应的纹理上传方式是 async pixel buffer texture upload,这种方式比 zero copy texture upload 要复杂的多,相对来说也占用了更多的内存用于临时使用的光栅化缓存。不过总体而言,这种方式也提供了更好的兼容性,性能表现也比较稳定,在硬件支持的情况下,甚至可以使用独立的纹理上传线程(AsyncPixelTransferDelegateEGL),从而避免频繁阻塞合成器所在的 gpu 线程,进一步提高性能。
当 TileManager 为 Tile 发起一个光栅化任务时,PixelBufferRasterWorkerPool::AcquireBufferForRaster 需要通过 ResourceProvider::AcquirePixelBuffer 为 ResourceProvider::Resource 分配一块 PixelBuffer 作为光栅化缓存,然后通过 ResourceProvider::MapPixelBuffer 获取这块缓存的地址:
void ResourceProvider::AcquirePixelBuffer(ResourceId id) {
Resource* resource = GetResource(id);
GLES2Interface* gl = ContextGL();
if (!resource->gl_pixel_buffer_id)
resource->gl_pixel_buffer_id = buffer_id_allocator_->NextId();
gl->BindBuffer(GL_PIXEL_UNPACK_TRANSFER_BUFFER_CHROMIUM,
resource->gl_pixel_buffer_id);
unsigned bytes_per_pixel = BitsPerPixel(resource->format) / 8;
gl->BufferData(GL_PIXEL_UNPACK_TRANSFER_BUFFER_CHROMIUM,
resource->size.height() *
RoundUp(bytes_per_pixel * resource->size.width(), 4u),
NULL,
GL_DYNAMIC_DRAW);
gl->BindBuffer(GL_PIXEL_UNPACK_TRANSFER_BUFFER_CHROMIUM, 0);
}
uint8_t* ResourceProvider::MapPixelBuffer(ResourceId id, int* stride) {
...
gl->BindBuffer(GL_PIXEL_UNPACK_TRANSFER_BUFFER_CHROMIUM,
resource->gl_pixel_buffer_id);
uint8_t* image = static_cast<uint8_t*>(gl->MapBufferCHROMIUM(
GL_PIXEL_UNPACK_TRANSFER_BUFFER_CHROMIUM, GL_WRITE_ONLY));
gl->BindBuffer(GL_PIXEL_UNPACK_TRANSFER_BUFFER_CHROMIUM, 0);
return image;
}
上面的代码演示了 ResourceProvider 需要分配一个 gl_pixel_buffer_id,然后通过 CommandBuffer 提供的 API BindBuffer 和 BufferData 分配一块缓存跟 gl_pixel_buffer_id 绑定,这块缓存分配在 MemoryChunk 上,实际上是 CommandBuffer 一块 TransferBuffer 其中的一部分,以后 cc 所有涉及这块缓存的操作都可以通过 gl_pixel_buffer_id 来进行,比如通过 MapBufferCHROMIUM 获取缓存的地址。
当 PixelBufferRasterWorkerPool 完成一个光栅化任务后会请求纹理上传,使用的是 async pixel buffer texture upload 的方式,整个上传的过程大致如下:
error::Error GLES2DecoderImpl::HandleAsyncTexImage2DCHROMIUM(
uint32 immediate_data_size,
const void* cmd_data) {
...
// Setup the parameters.
AsyncTexImage2DParams tex_params = {
target, level, static_cast<GLenum>(internal_format),
width, height, border, format, type};
AsyncMemoryParams mem_params(
GetSharedMemoryBuffer(c.pixels_shm_id), c.pixels_shm_offset, pixels_size);
// Set up the async state if needed, and make the texture
// immutable so the async state stays valid. The level info
// is set up lazily when the transfer completes.
AsyncPixelTransferDelegate* delegate =
async_pixel_transfer_manager_->CreatePixelTransferDelegate(texture_ref,
tex_params);
texture->SetImmutable(true);
delegate->AsyncTexImage2D(
tex_params,
mem_params,
...);
return error::kNoError;
}
上面显示的是 CommandBuffer Service 执行 AsyncTexImage2DCHROMIUM 命令时的主要代码,它会为相关的 Texture 创建一个 AsyncPixelTransferDelegate,然后调用 AsyncPixelTransferDelegate::AsyncTexImage2D 方法来启动纹理上传。Chromium 根据 GPU 的类型来选择使用 AsyncPixelTransferDelegateEGL 或者 AsyncPixelTransferDelegateIdle,代码在 async_pixel_transfer_manager_android.cc,根据条件选择创建 AsyncPixelTransferManagerEGL 或者是 AsyncPixelTransferManagerIdle 返回给 GLES2DecoderImpl。
AsyncPixelTransferDelegateIdle 比较简单,真正的纹理上传发生在 CommandBuffer Service 所在的 gpu 线程,或者在强制等待完成时调用,或者在 gpu 线程 idle 时调用,不过因为 CAW 当前没有独立的 gpu 线程,所以 gpu 线程的 idle 其实也不能完全算真正的 idle,这种方式相对而言在有大量纹理上传时比较容易阻塞 gpu 线程而导致跳帧。
AsyncPixelTransferDelegateEGL 则复杂很多,它通过使用 EGLImage 机制在两个不同线程的 GL 上下文中实现纹理数据共享,创建了一个额外的线程专门用于纹理上传。简单来说就是在独立的纹理线程创建一个额外的 Texture 然后上传纹理数据,然后再创建一个绑定这个 Texture 的 EGLImage,然后再在原线程将纹理上传线程创建的 EGLImage 和原来的 Texture 进行绑定,这样两个 Texture 就可以通过 EGLImage 共享同样的纹理数据。这种方式对于不需要强制等待完成的纹理上传是比较有用的,避免了大量纹理上传导致 gpu 线程阻塞,不过也增加了一个额外的 GL 上下文的内存占用,并且很多 GPU 不支持这种方式,从代码上看也就是 Mali 和 Adreno 3xx 支持。