摘要:Chromium中GPU进程架构导致多个GPU客户端会同时访问GPU服务,而多个GPU客户端可能存在数据依赖关系,例如渲染WebGL页面时,因此需要提供一种同步机制保证GPU操作的先后次序。本文讨论的就是多进程架构下GPU客户端之间的同步问题,以及同步点(SyncPoint)机制的基本原理。
GPU进程架构等基本概念
我们知道,Chromium是一个多进程架构的软件系统。出于安全和稳定性方面的考虑,Chromium有个专门的进程(或者线程)和GPU设备进行交互,执行GL操作,也就是说,任何一条GL命令都必须经由这个进程处理后才提交给GPU驱动,这个进程就是GPU进程。Renderer进程或者Browser进程需要通过IPC才能与GPU进程交互,告诉GPU进程需要执行哪些GL命令。在这个过程中,Renderer进程或Browser进程是GPU进程的客户端,而GPU进程本身扮演着服务端的角色。
GPU客户端和服务端之间GL命令传输是通过命令缓冲区(CommandBuffer)完成的。CommandBuffer是Chromium为解决多进程架构下高效传输数据而设计的,缓冲区使用共享内存存储客户端向服务器端发起的GL命令,GPU客户端发起的每条GL命令,命令名和参数都会被序列化为字符串存储在命令行缓冲区中。当客户端需要将存储在缓冲区中的所有GL命令发送给GPU服务端请求执行时,客户端会向服务端发送GpuCommandBufferMsg_AsyncFlush消息,服务器端收到这条消息后会根据指定的偏移从CommandBuffer中将GL命令反序列化。
在Chromium中,页面的渲染和合成过程都启用了GPU硬件加速,任何一个需要GPU加速的进程都是GPU进程的客户端,换句话说,Renderer进程是GPU客户端,因为它需要请求GPU进程绘制和合成页面的内容,Browser进程也是GPU客户端,因为Browser需要将页面的内容与地址栏等元素进行最终的合成,并显示在屏幕上。
不同GPU客户端是通过mailbox机制进行texture共享的,简单的说,mailbox机制就是给每个texture生成唯一标识,并由GPU进程统一管理名标识和texture之间的映射关系。
GPU客户端之间的同步问题
从上述描述不难看出,GPU进程需要处理来自多个GPU客户端的请求,并且这些GPU客户端可能存在纹理(Texture)数据依赖关系。
下面以Android平台上渲染WebGL页面http://get.webgl.org 为例,说明为什么不同GPU客户端会存在数据依赖关系。
首先,Browser进程是一个GPU客户端,它创建了一个Browser端的合成器(Compositor)请求GPU进程将页面的内容和地址栏等UI元素(如果有)进行最终的合成,并将其渲染到SurfaceView上。Renderer进程也是GPU客户端,它也会创建一个Renderer端的合成器用于页面的合成,而且这个客户端还包含了两个独立的3D上下文,一个用于页面的渲染,另一个用于WebGL的渲染。
其次,在Android平台上,默认启用了委托模式(DelegatedRenderer)的渲染器,因此,Renderer端合成器管理的所有GPU资源(包括WebGL)都会转交给Browser端的合成器,再由Browser端合成器进行统一的合成,当最终的合成操作完成之后,Browser端合成器则会告诉Renderer端资源使用完毕,可以安全删除了。
最后,再来看看WebGL的渲染过程。在硬件加速的页面渲染机制中,页面的内容是由多个渲染层(RenderLayer)构成的,每个渲染层的存储后端对应一个Texture,WebGL正是这样一个渲染层,它具有一个独立的Texture存储后端。在渲染每一帧内容时,Renderer进程会请求GPU进程创建一个新的Texture来存放WebGL的渲染结果,并通过Framebuffer对象将WebGL渲染到这个Texture中。待 WebGL命令完毕后,Renderer端的合成器再将这个Texture以资源形式转交给Browser端的合成器并将其渲染到SurfaceView的指定坐标上。当Browser端合成器完成WebGL的合成之后,会通知Renderer进程可以将这个Texture删除。
那么,从上面的描述中可以推断出整个过程存在哪些问题呢?
第一,Renderer端的合成器和Browser端合成器存在数据依赖关系,是生产者-消费者的关系,即Renderer端的WebGL上下文生成Texture内容,Browser端合成器使用该内容;
第二,WebGL上下文和Browser端合成器存在同步问题。由于所有的GL命令都是在同一线程中执行,而3D上下文是GPU线程的调度基本单位,也就是只有同一个3D上下文中GL命令才能按照顺序逐条执行的。一方面,不同GPU客户端可能运行在不同的进程或者线程中,另一方面,同一GPU客户端的不同3D上下文可能运行在不同的线程中,例如Renderer进程的合成器运行在一个单独的线程中,而WebGL运行在Renderer进程的主线程中。那么显然,不同3D上下文中GL命令执行不会按照指定的次序关系执行,这里就引申出一个问题,对于WebGL页面渲染来说,如何保证:
- 只有当WebGL上下文的GL命令执行完毕后,Browser端合成器才能使用;
- 只有当Browser端合成器使用完WebGL的Texture后,WebGL上下文才能安全删除;
这就是GPU客户端之间的同步问题,本文接下来将重点讨论Chromium是如何解决这个问题的。
同步点机制的基本原理
Chromium通过GL扩展接口设计了一套同步机制解决不同GPU客户端之间的同步问题,这套机制必须满足两个条件:
第一,上下文A可以等待上下文B执行完GL命令后再执行后续的GL命令;
第二,上下文A中的这种等待必须是非阻塞的,也就是说GPU客户端代码的执行不能被阻塞;
根据gpu/gles2/extensions/GL_CHROMIUM_sync_point.text文件所述,同步点机制定义了两个特定于Chromium的GL扩展接口:
uint InsertSyncPointCHROMIUM() void WaitSyncPointCHROMIUM(uint sync_point)
InsertSyncPointCHROMIUM在当前上下文中创建一个同步点并将其插入到命令流中,这个同步点起到一个防护墙的作用,当这个同步点之前的命令都已经提交到服务器,或者上下文被销毁时,会向该同步点发个信号。返回同步点的标识符。收到信号后,这个同步点会被删除点。在同一个服务器上同步点标识符在所有上下文中是唯一的,包括同一共享组的上下文。
WaitSyncPointCHROMIUM导致当前上下文暂停提交GL命令,直到指定同步点收到信号,被实现为服务端的等待。参数sync_point为InsertSyncPointCHROMIUM返回的同步点标识符。如果sync_point参数无效,这个命令相当于no-op操作并且不会报错。
以上文档化的描述读起来有些晦涩难懂,下面通过一个简化的例子更直观地说明SyncPoint在Chromium中的运行原理:
假设有两个上下文A和B,它们可能在不同的GPU客户端中,但属于同一个共享组(ShareGroup),GPU客户端代码中通过调用InsertSyncPointCHROMIUM在上下文A的命令流中插入一个同步点sp,而在上下文B中,调用WaitSyncPointCHROMIUM等待同步点sp,最终在GPU服务端执行GL命令的顺序为,只有当上下文A中同步点sp之前的GL命令A1,A2和A3执行完毕后,才会执行上下文B中的GL命令B3。
更进一步地说,上下文B中调用WaitSyncPointCHROMIUM(sp),实际上是告诉GPU服务端停止向GPU设备提交上下文B后续的GL命令,取而代之的是:
- 如果此时上下文A的同步点sp之前的GL命令都已经执行完毕,即同步点sp收到信号,已经从被删除了,那么可以忽略WaitSyncPointCHROMIUM了,继续执行上下文B中后续的GL命令;
- 如果此时上下文A的同步点sp之前的GL命令尚未执行,那么取而代之的是将等待上下文A中sp点之前的GL命令执行完毕。这个等待发生在GPU服务端的,不会阻塞GPU客户端后续代码的运行(如Renderer进行的运行)。
综上所述,同步点机制允许客户端设定不同上下文之间GL命令执行的次序,上下文B等待上下文A的同步点实际上是保证A中同步点之前的命令在B中后续命令之前执行。
未完待续...下节将从Chromium源代码中解读同步点机制是如何实现的