首页 > 代码库 > Chromium Graphics: GPU客户端之间同步机制的原理和实现分析-Part II
Chromium Graphics: GPU客户端之间同步机制的原理和实现分析-Part II
摘要:Part I分析了GPU客户端之间存在的同步问题,以及Chromium的GL扩展同步点机制的基本原理。本文将源代码的角度剖析同步点(SyncPoint)机制的实现方式。同步点机制的实现主要涉及到是如何跨进程实现两个GL扩展接口InsertSyncPointCHROMIUM和WaitSyncPointCHROMIUM的实现方式,以及如何实现GPU服务端的同步点等待。
GPU客户端
GPU客户端将所有的GL命令都封装在GLES2Implementation中,GLES2Implementation将客户端的GL命令序列化后存放在命令行缓冲区中,客户端的每个WebGraphicsContext3DImpl都会创建一个GLES2Implementation实例,调用GLES2Implementation的方法看上去就像直接调用OpenGL方法直接操作GPU设备一样。GPU客户端与GPU进程之间的交互封装在GpuCommandBufferProxy类中,这个代理类会向GPU服务端的GpuCommandBufferStub发送IPC消息请求执行某些GPU操作,如发送GpuCommandBufferMsg_AsyncFlush消息提交(Flush)命令缓冲区。
同步点机制首先需要在GLES2Implementation类中实现这两个GL扩展接口,以便允许客户端代码使用同步点机制。GLES2Implementation::InsertSyncPointCHROMIUM通过GpuCommandBufferProxy向GPU服务端发送一条同步IPC消息GpuCommandBufferMsg_InsertSyncPoint请求注册一个新的同步点,在调用CommandBufferProxyImpl::InsertSyncPoint之前,GLES2Implementation会将客户端的所有GL命令通过Flush命令行缓冲区都提交到服务端,如下代码:
GLuint GLES2Implementation::InsertSyncPointCHROMIUM() { GPU_CLIENT_SINGLE_THREAD_CHECK(); GPU_CLIENT_LOG("[" << GetLogPrefix() << "] glInsertSyncPointCHROMIUM"); helper_->CommandBufferHelper::Flush(); return gpu_control_->InsertSyncPoint(); } ... uint32 CommandBufferProxyImpl::InsertSyncPoint() { if (last_state_.error != gpu::error::kNoError) return 0; uint32 sync_point = 0; Send(new GpuCommandBufferMsg_InsertSyncPoint(route_id_, true, &sync_point)); return sync_point; }
同步IPC消息GpuCommandBufferMsg_InsertSyncPoint的参数sync_point返回由GPU服务端统一分配的唯一标识。
而与InsertSyncPointCHROMIUM不同的是,WaitSyncPointCHROMIUM不是通过发送专门的IPC消息给GPU服务端,而是作为一条GL扩展命令添加到CommandBuffer中,GPU服务端处理这条GL命令时再决定如何执行暂停后续命令的提交,等待同步点的完成。
GPU服务端
GPU服务端既可以是一个独立的进程,也可以是一个Browser进程的一个线程,不论是进程还是线程,所有GPU操作了都是在同一个线程上执行,或称为GPU线程。GPU线程除了向GPU设备提交GL命令之外,还需要管理多个GpuChannel。
GpuChannel的消息处理方式
每个GpuChannel都对应了一个GPU客户端(Renderer进程或者Browser进程),GpuChannel::OnMessageReceived收到来自客户端的IPC消息后,并不会立即对消息进行处理,而是其添加到一个未处理消息队列中,然后再向主消息循环添加一个任务GpuChannel::HandleMessage来处理所有消息队列中所有未处理的消息。这种缓存IPC消息的处理方式至少有两个好处:
第一,便于根据IPC消息序列的特定模式优化对多上下文的消息处理;
第二,便于控制多个GpuChannel之间GL命令执行的同步方式,也就是本文所讨论的同步点机制;
HandleMessage任务中会逐一遍历所有未经处理的IPC消息,根据IPC消息的路由信息,将其交给对应的GpuCommandBufferStub来处理,见如下代码:
void GpuChannel::HandleMessage() { handle_messages_scheduled_ = false; if (deferred_messages_.empty()) return; bool should_fast_track_ack = false; // 根据IPC消息的路由信息查找对应的GpuCommandBufferStub实例 IPC::Message* m = deferred_messages_.front(); GpuCommandBufferStub* stub = stubs_.Lookup(m->routing_id()); do { if (stub) { if (!stub->IsScheduled()) // 当GpuCommandBufferStub处于“不可调度”状态时,直接返回 return; if (stub->IsPreempted()) { OnScheduled(); return; } } // 从消息队列中弹出队头消息并交由GpuCommandBufferStub处理 scoped_ptr<IPC::Message> message(m); deferred_messages_.pop_front(); bool message_processed = true; currently_processing_message_ = message.get(); bool result; if (message->routing_id() == MSG_ROUTING_CONTROL) result = OnControlMessageReceived(*message); else result = router_.RouteMessage(*message); currently_processing_message_ = NULL; if (!result) { // Respond to sync messages even if router failed to route. if (message->is_sync()) { IPC::Message* reply = IPC::SyncMessage::GenerateReply(&*message); reply->set_reply_error(); Send(reply); } } else { // If the command buffer becomes unscheduled as a result of handling the // message but still has more commands to process, synthesize an IPC // message to flush that command buffer. if (stub) { if (stub->HasUnprocessedCommands()) { deferred_messages_.push_front(new GpuCommandBufferMsg_Rescheduled( stub->route_id())); message_processed = false; } } } if (message_processed) MessageProcessed(); // 此处省略一部分无关代码 } while (should_fast_track_ack); // 如果消息队列不为空,调用OnScheduled继续往主消息循环中添加HandleMessage任务 if (!deferred_messages_.empty()) { OnScheduled(); } }
GpuCommandBufferStub是GPU进程端一个非常重要的类,它封装了大部分在GPU服务端用来执行GL命令的功能,包括上下文的创建和初始化,响应客户端的GPU操作请求,调度执行分析和反序列化命令行缓冲区中的GL命令等工作。 GPU客户端请求创建一个新的3D上下文时,实际上是请求一个新的GpuCommandBufferStub实例,因此,对于包含硬件加速canvas的Renderer进程,其对应的GpuChannel会管理多个GpuCommandBufferStub实例,以及根据IPC消息头部信息将IPC消息派发给相应的GpuCommandBufferStub实例上。
同步点的插入和等待
为了对IO线程上收到的IPC消息进行预处理,每个GpuChannel都会安装一个运行在IO线程上的消息过滤器,每条来自GPU客户端的IPC消息首先要经由消息过滤器的预处理,再转发给GpuChannel做进一步处理。
当GPU服务端收到GpuCommandBufferMsg_InsertSyncPoint消息后,IO线程上的消息过滤器GpuChannelMessageFilter分两步处理:
1. IO线程上请求SyncPointManager::GenerateSyncPoint生成同步点的唯一标识,并立即发送给GPU客户端;
2. 向GPU服务端的主消息循环队列添加一个新的任务InsertSyncPointOnMainThread,即在主线程上向GpuChannel添加一个同步点;
static void InsertSyncPointOnMainThread( base::WeakPtr<GpuChannel> gpu_channel, scoped_refptr<SyncPointManager> manager, int32 routing_id, bool retire, uint32 sync_point) { // This function must ensure that the sync point will be retired. Normally // we'll find the stub based on the routing ID, and associate the sync point // with it, but if that fails for any reason (channel or stub already // deleted, invalid routing id), we need to retire the sync point // immediately. if (gpu_channel) { GpuCommandBufferStub* stub = gpu_channel->LookupCommandBuffer(routing_id); if (stub) { stub->AddSyncPoint(sync_point); if (retire) { GpuCommandBufferMsg_RetireSyncPoint message(routing_id, sync_point); gpu_channel->OnMessageReceived(message); } return; } else { gpu_channel->MessageProcessed(); } } manager->RetireSyncPoint(sync_point); }
InsertSyncPointOnMainThread除了调用GpuCommandBufferStub::AddSyncPoint向当前上下文添加同步点之外,还会显式调用GpuChannel::OnMessageReceived向当前GpuChannel发送一条 GpuCommandBufferMsg_RetireSyncPoint消息,这条消息将会添加到GpuChannel的消息队列队尾,那么对于同时处理多个GpuChannel的主线程来说,可能会发生以下两种情况(这里假设GpuChannelA调用AddSyncPoint了):
情况I: GpuChannel A在主线程中得到了充分的调度,所有未处理的IPC消息都被处理了,包括插入同步点时要求的GpuCommandBufferMessage_AsyncFlush消息,以及添加同步点之后GPU进程端创建的GpuCommandBufferMsg_RetireSyncPoint消息,这就表明,在同步点之前的所有GL命令此时都已经提交给GPU驱动了,可以让这个同步点“退休”了,也就是上述文档中提到的,同步点收到信号将自动被删除。如果其他GpuChannel有调用WaitSyncPointCHROMIUM的GL命令,等价于不执行任何操作。
情况II: GpuChannel A在主线程中没有得到充分的调度,同时,主线程调度了GpuChannel B,其中某个上下文中调用WaitSyncPointCHROMIUM去等待A中创建的同步点。上面提到,WaitSyncPointCHROMIUm作为一条扩展的GL命令掺入到命令缓冲区中,所以GpuChannelB中的GpuCommandBufferStub在逐条解析并执行命令缓冲区的GL命令时,当遇到WaitSyncPointCHROMIUM命令时,GLES2DecoderImpl::HandleWaitSyncPointCHROMIUM会执行如下操作:
- 触发GpuCommandBufferStub注册的WaitSyncPoint回调函数GpuCommandBufferStub::OnWaitSyncPoint,该函数决定当前GpuChannelB中的GpuCommandBufferStub究竟是继续解析GL命令,还是停止解析GL命令等待同步点的“退休”;
- WaitSyncPoint会首先判断同步点是否已经“退休”,如果是,则返回true表明GpuChannelB的当前GpuCommandBufferStub可以继续GL命令的解析和执行。否则,将当前GpuCommandBufferStub设置为“不可被调度”状态,并请求SyncPointManager为等待的同步点添加一个回调函数OnSyncPointRetired,这个回调函数的作用就是恢复GpuCommandBufferStub为“可调度”状态。
细心的读者可能会发现,这里还有两个问题没有交代清楚:
第一,GpuCommandBufferStub的调度状态是如何被使用的?
第二,SyncPointManager什么时候会调用回调函数GpuCommandBufferStub::OnSyncPointRetired?
前面提到,GpuChannel::HandleMessage会逐一遍历所有未被处理的IPC消息,并交给对应的GpuCommandBufferStub来处理,而只有GpuCommandBufferStub处于“可调度”状态时才能处理这个IPC消息,否则直接从HandleMessage方法中返回,因此GpuChannel的消息处理也将停滞不前,也就是说,此时GpuChannelB将停止下来等待同步点的“退休”,直到GpuCommandBufferStub::OnSyncPointRetired回调函数被触发。
一旦主线程上GpuChannel B的消息处理停止了,GpuChannel A则有更多的机会得到主线程的调度,GpuChannelA中积压的GpuCommandBufferMsg_AsyncFlush和GpuCommandBufferMsg_RetireSyncPoint消息将会一并被处理。GpuCommandBufferMsg_AsyncFlush消息负责将所有客户端的GL命令通过GLES2DecoderImpl提交给GPU驱动,而GpuCommandBufferMsg_RetireSyncPoint消息则会触发GpuCommandBufferStub::OnRetireSyncPoint消息处理函数,请求SyncPointManager将给定的同步点删除,在删除同步点时,SyncPointManager将会运行所有与该同步点关联的回调函数,所以此时GpuCommandBufferStub::OnSyncPointRetired被调用,GpuChannelB的消息处理又可以开始了。
需要特别说明的,由于 GpuCommandBufferStub的调度状态而导致GpuChannelB的消息处理停止,只是针对服务端执行GL命令而言的,并没有停止接受来自客户端的消息,客户端仍然可以向GpuChannelB发送IPC消息,不过这些消息将会一直会被缓存在消息队列中,直到所等待的同步点“退休”,这就是上述提到的“服务端的等待”。
小结
Chromium为解决多个GPU客户端的数据同步问题,引入同步点(SyncPoint)机制的GL扩展,从而允许客户端可以定制不同上下文GL命令执行的先后次序,同步点机制保证了当前上下文在执行后续GL命令之前,其等待的同步点之前的所有GL命令都已经提交给GPU设备。同步点的等待是服务端的等待,客户端代码并不会被阻塞。在实现上,同步点机制依赖于GpuChannel的消息处理机制,当等待的同步点尚未“退休”时,当前的GpuCommandBufferStub将会被置为“不可调度”状态,直到同步点之前的所有的GL命令均已提交。
Chromium Graphics: GPU客户端之间同步机制的原理和实现分析-Part II