Chromium硬件加速渲染的OpenGL上下文调度过程分析.docx
《Chromium硬件加速渲染的OpenGL上下文调度过程分析.docx》由会员分享,可在线阅读,更多相关《Chromium硬件加速渲染的OpenGL上下文调度过程分析.docx(76页珍藏版)》请在冰豆网上搜索。
![Chromium硬件加速渲染的OpenGL上下文调度过程分析.docx](https://file1.bdocx.com/fileroot1/2023-1/8/0bdbba4f-5198-41a9-a0f6-349a2ebbc5bf/0bdbba4f-5198-41a9-a0f6-349a2ebbc5bf1.gif)
Chromium硬件加速渲染的OpenGL上下文调度过程分析
Chromium硬件加速渲染的OpenGL上下文调度过程分析
Chromium的每一个WebGL端、Render端和Browser端实例在GPU进程中都有一个OpenGL上下文。
这些OpenGL上下文运行在相同线程中,因此同一时刻只有一个OpenGL上下文处于运行状态。
这就引发出一个OpenGL上下文调度问题。
此外,事情有轻重缓急,OpenGL上下文也有优先级高低之分,优先级高的要保证它的运行时间。
本文接下来就分析GPU进程调度运行OpenGL上下文的过程。
在前面一文中提到,GPU进程中的所有OpenGL上下文不仅运行在相同线程中,即运行在GPU进程的GPU线程中,它们还处于同一个共享组中,如图1所示:
在Chromium中,每一个OpenGL上下文使用一个GLContextEGL对象描述。
每一个OpenGL上下文都关联有一个绘图表面。
对于WebGL端和Render端的OpenGL上下文来说,它关联的绘图表面是一个离屏表面。
这个离屏表面一般就是用一个Pbuffer描述。
在Android平台上,Browser端的OpenGL上下文关联的绘图表面是一个SurfaceView。
当一个OpenGL上下文被调度时,它以及它关联的绘图表面就会通过调用EGL函数eglMakeCurrent设置为GPU线程当前使用的OpenGL上下文和绘图表面。
从前面一文又可以知道,Chromium为WebGL端、Render端和Browser端创建的OpenGL上下文可能是虚拟的,如图2所示:
在Chromium中,每一个虚拟OpenGL上下文都使用一个GLContextVirtual对象描述。
每一个虚拟OpenGL上下文都对应有一个真实OpenGL上下文,即一个GLContextEGL对象,并且所有的虚拟OpenGL上下文对应的真实OpenGL上下文都是相同的。
虚拟OpenGL上下文也像真实OpenGL上下文一样,关联有绘图表面。
对于WebGL端和Render端的虚拟OpenGL上下文来说,它关联的绘图表面也是一个使用Pbuffer描述的离屏表面。
在Android平台上,Browser端的OpenGL上下文关联的绘图表面同样也是一个SurfaceView。
当一个虚拟OpenGL上下文被调度时,它对应的真实OpenGL上下文以及它关联的绘图表面就会通过调用EGL函数eglMakeCurrent设置为GPU线程当前使用的OpenGL上下文和绘图表面。
由于所有的虚拟OpenGL上下文对应的真实OpenGL上下文都是相同的,因此当一个虚拟OpenGL上下文被调度时,只需要通过调用EGL函数eglMakeCurrent将其关联的绘图表面设置为GPU线程当前使用的绘图表面即可。
前面提到,OpenGL上下文有优先级高低之分,具体表现为Browser端的OpenGL上下文优先级比WebGL端和Render端的高。
这是因为前者负责合成后者的UI显示在屏幕中,因此就要保证它的运行时间。
在Browser端的OpenGL上下文需要调度运行而GPU线程又被其它OpenGL上下文占有时,Browser端的OpenGL上下文就可以抢占GPU线程。
为达到这一目的,Chromium给Browser端与GPU进程建立的GPU通道设置IDLE、WAITING、CHECKING、WOULD_PREEMPT_DESCHEDULED和PREEMPTING五个状态。
这五个状态的变迁关系如图3所示:
当Browser端的GPU通道处于PREEMPTING状态时,Browser端的OpenGL上下文就可以要求其它OpenGL上下文停止执行手头上的任务,以便将GPU线程交出来运行Browser端的OpenGL上下文。
Browser端的GPU通道开始时处于IDLE状态。
当有未处理IPC消息时,就从IDLE状态进入WAITING状态。
进入WAITING状态kPreemptWaitTimeMs毫秒之后,就自动进入CHECKING状态。
kPreemptWaitTimeMs毫秒等于2个kVsyncIntervalMs毫秒,kVsyncIntervalMs定义为17。
假设屏幕的刷新速度是60fps,那么kVsyncIntervalMs毫秒刚好就是一个Vsync时间,即一次屏幕刷新时间间隔。
处于CHECKING状态期间时,Browser端的GPU通道会不断检查最早接收到的未处理IPC消息流逝的时间是否小于2次屏幕刷新时间间隔。
如果小于,那么就继续停留在CHECKING状态。
否则的话,就进入WOULD_PREEMPT_DESCHEDULED状态或者PREEMPTING状态。
图1所示的stub指的是一个GpuCommandBufferStub对象。
从前面Chromium硬件加速渲染的OpenGL上下文创建过程分析一文可以知道,在GPU进程中,一个GpuCommandBufferStub对象描述的就是一个OpenGL上下文。
因此,图1所示的stub指的是一个Browser端OpenGL上下文。
处于CHECKING状态期间时,如果最早接收到的未处理IPC消息流逝的时间大于等于2次屏幕刷新时间间隔,并且没有任何的Browser端OpenGL上下文自行放弃调度,那么Browser端的GPU通道就会进入PREEMPTING状态,表示它要抢占GPU线程,也表示要求当前正在调度的OpenGL上下文放弃占有GPU线程。
另一方面,如果这时候至少有一个Browser端OpenGL上下文自行放弃调度,那么Browser端的GPU通道就会进入WOULD_PREEMPT_DESCHEDULED状态,表示它现在不急于抢占GPU线程,因为这时候有OpenGL上下文自行放弃了调度,从而使得最早接收到的未处理消息所属的OpenGL上下文得到调度处理。
处于WOULD_PREEMPT_DESCHEDULED状态时,Browser端的GPU通道会继续检查是否有Browser端OpenGL上下文自行放弃调度。
如果没有,那么就进入PREEMPTING状态,表示要抢占GPU线程。
如果有,并且这时候Browser端的GPU通道接收到的IPC消息均已被处理,或者最早接收到的未处理IPC消息的流逝时间小于kStopPreemptThresholdMs毫秒,那么就进入IDLE状态。
否则的话,就继续维持WOULD_PREEMPT_DESCHEDULED状态。
kStopPreemptThresholdMs也定义为17,意思是Browser端的GPU通道处于WOULD_PREEMPT_DESCHEDULED状态时,允许最早接收到的未处理IPC消息延迟一次屏幕刷新时间间隔再进行处理。
Browser端的GPU通道处于PREEMPTING状态的最长时间为kMaxPreemptTimeMs毫秒。
kMaxPreemptTimeMs也定义为17,意思是Browser端的GPU通道抢占GPU线程的时间不能超过一个屏幕刷新时间间隔。
如果超过了一个屏幕刷新时间间隔,那么就会进入IDLE状态。
在处于PREEMPTING状态期间,如果Browser端的GPU通道接收到的IPC消息均已被处理,或者最早接收到的未处理IPC消息的流逝时间小于kStopPreemptThresholdMs毫秒,那么Browser端的GPU通道也会进入IDLE状态。
此外,在处于PREEMPTING状态期间,如果至少有一个Browser端OpenGL上下文自行放弃调度,那么Browser端的GPU通道就会进入WOULD_PREEMPT_DESCHEDULED状态。
注意,在图3所示的状态变迁图中,只有处于PREEMPTING状态时,Browser端的GPU通道才会强行抢占GPU线程。
这是为了保证Browser端的OpenGL上下文,至少应该需要在两个屏幕刷新时间间隔之内,得到一次调度,从而保证网页UI得到刷新和及时显示。
WebGL端和Render端的OpenGL上下文就没有这种待遇,毕竟它们的优先级没有Browser端的OpenGL上下文高。
Browser端GPU通道是以什么方式强行抢占GPU线程的呢?
我们通过图4说明,如下所示:
Browser端GPU通道有一个PreemptionFlag。
当它处于PREEMPTING状态时,就会将PreemptionFlag设置为True。
WebGL端和Render端GPU通道可以访问Browser端GPU通道的PreemptionFlag。
属于WebGL端和Render端GPU通道的OpenGL上下文在调度期间,会不断地检查Browser端GPU通道的PreemptionFlag是否被设置为True。
如果被设置为True,那么它就会中止执行,提前释放GPU线程。
WebGL端和Render端GPU通道和Browser端GPU通道是父子关系。
其中,WebGL端和Render端GPU通道是儿子,Browser端GPU通道是父亲。
Chromium规定,儿子GPU通道可以访问父亲GPU通道的PreemptionFlag。
有了前面这些背景知识之后,接下来我们就结合源码分析OpenGL上下文的调度过程。
从前面一文可以知道,GPU进程通过调用GpuChannel类的成员函数Init创建GPU通道,如下所示:
[cpp]viewplaincopy
voidGpuChannel:
:
Init(base:
:
MessageLoopProxy*io_message_loop,
base:
:
WaitableEvent*shutdown_event){
......
channel_=IPC:
:
SyncChannel:
:
Create(channel_id_,
IPC:
:
Channel:
:
MODE_SERVER,
this,
io_message_loop,
false,
shutdown_event);
filter_=
newGpuChannelMessageFilter(weak_factory_.GetWeakPtr(),
gpu_channel_manager_->sync_point_manager(),
base:
:
MessageLoopProxy:
:
current());
......
channel_->AddFilter(filter_.get());
......
}
这个函数定义在文件external/chromium_org/content/common/gpu/gpu_channel.cc中。
结合前面一文可以知道,WebGL端、Render端和Browser端发送过来的GPU消息由GpuChannel类的成员函数OnMessageReceived负责接收。
在接收之前,这些GPU消息首先会被GpuChannel类的成员变量filter_指向的一个GpuChannelMessageFilter对象的成员函数OnMessageReceived过滤。
注意,GpuChannel类的成员函数Init是在GPU线程中执行的,这意味着GpuChannel类的成员函数OnMessageReceived也将会在GPU线程中执行,但是它的成员变量filter_指向的GpuChannelMessageFilter对象的成员函数OnMessageReceived不是在GPU线程执行的,而是在负责接收IPC消息的IO线程中执行的。
接下来我们先分析GpuChannel类的成员函数OnMessageReceived的实现,后面分析Browser端GPU通道抢占GPU线程的过程时,再分析GpuChannelMessageFilter类的成员函数OnMessageReceived的实现。
GpuChannel类的成员函数OnMessageReceived的实现如下所示:
[cpp]viewplaincopy
boolGpuChannel:
:
OnMessageReceived(constIPC:
:
Message&message){
......
if(message.type()==GpuCommandBufferMsg_WaitForTokenInRange:
:
ID||
message.type()==GpuCommandBufferMsg_WaitForGetOffsetInRange:
:
ID){
//MoveWaitcommandstotheheadofthequeue,sotherenderer
//doesn'thavetowaitanylongerthannecessary.
deferred_messages_.push_front(newIPC:
:
Message(message));
}else{
deferred_messages_.push_back(newIPC:
:
Message(message));
}
OnScheduled();
returntrue;
}
这个函数定义在文件external/chromium_org/content/common/gpu/gpu_channel.cc中。
GpuChannel类将接收到的IPC消息保存在成员变量deferred_messages_描述的一个std:
:
deque中。
其中,类型为GpuCommandBufferMsg_WaitForTokenInRange和GpuCommandBufferMsg_WaitForGetOffsetInRange的GPU消息将会保存在上述std:
:
deque的头部,而其它GPU消息则保存在上述std:
:
deque的末部。
从前面和这篇文章可以知道,当GPU进程接收到类型为GpuCommandBufferMsg_WaitForTokenInRange和GpuCommandBufferMsg_WaitForGetOffsetInRange的IPC消息时,就表示它的Client端,即WebGL端、Render端和Browser端,正在检查其GPU命令缓冲区的执行情况,需要尽快得到结果,因此就需要将它们放在上述std:
:
deque的头部,以便尽快得到处理。
GpuChannel类的成员函数OnMessageReceived将接收到的GPU消息保存在成员变量deferred_messages_描述的std:
:
deque之后,接下来调用另外一个成员函数OnScheduled对它们进行调度处理,如下所示:
[cpp]viewplaincopy
voidGpuChannel:
:
OnScheduled(){
if(handle_messages_scheduled_)
return;
//Postatasktohandleanydeferredmessages.Thedeferredmessagequeueis
//notemptiedhere,whichensuresthatOnMessageReceivedwillcontinueto
//defernewlyreceivedmessagesuntiltheonesinthequeuehaveallbeen
//handledbyHandleMessage.HandleMessageisinvokedasa
//tasktopreventreentrancy.
base:
:
MessageLoop:
:
current()->PostTask(
FROM_HERE,
base:
:
Bind(&GpuChannel:
:
HandleMessage,weak_factory_.GetWeakPtr()));
handle_messages_scheduled_=true;
}
这个函数定义在文件external/chromium_org/content/common/gpu/gpu_channel.cc中。
GpuChannel类的成员函数OnScheduled通过向GPU线程的消息队列发送一个Task请求为当前接收到GPU消息的GPU通道执行一次调度。
该Task绑定的函数为GpuChannel类的成员函数HandleMessage。
GpuChannel类的成员变量handle_messages_scheduled_的值等于true时,表示当前接收到GPU消息的GPU通道已经请求过调度了,并且请求的调度还没有被执行。
在这种情况下,就不必再向GPU线程的消息队列发送Task。
GpuChannel类的成员函数HandleMessage的实现如下所示:
[cpp]viewplaincopy
voidGpuChannel:
:
HandleMessage(){
handle_messages_scheduled_=false;
if(deferred_messages_.empty())
return;
boolshould_fast_track_ack=false;
IPC:
:
Message*m=deferred_messages_.front();
GpuCommandBufferStub*stub=stubs_.Lookup(m->routing_id());
do{
if(stub){
if(!
stub->IsScheduled())
return;
if(stub->IsPreempted()){
OnScheduled();
return;
}
}
scoped_ptr:
Message>message(m);
deferred_messages_.pop_front();
boolmessage_processed=true;
currently_processing_message_=message.get();
boolresult;
if(message->routing_id()==MSG_ROUTING_CONTROL)
result=OnControlMessageReceived(*message);
else
result=router_.RouteMessage(*message);
currently_processing_message_=NULL;
if(!
result){
//Respondtosyncmessagesevenifrouterfailedtoroute.
if(message->is_sync()){
IPC:
:
Message*reply=IPC:
:
SyncMessage:
:
GenerateReply(&*message);
reply->set_reply_error();
Send(reply);
}
}else{
//Ifthecommandbufferbecomesunscheduledasaresultofhandlingthe
//messagebutstillhasmorecommandstoprocess,synthesizeanIPC
//messagetoflushthatcommandbuffer.
if(stub){
if(stub->HasUnprocessedCommands()){
deferred_messages_.push_front(newGpuCommandBufferMsg_Rescheduled(
stub->route_id()));
message_processed=false;
}
}
}
if(message_processed)
MessageProcessed();
//WewanttheEchoACKfollowingtheSwapBufferstobesentascloseas
//possible,avoidingschedulingotherchannelsinthemeantime.
should_fast_track_ack=false;
if(!
deferred_messages_.empty()){
m=deferred_messages_.front();
stub=stubs_.Lookup(m->routing_id());
should_fast_track_ack=
(m->type()==GpuCommandBufferMsg_Echo:
:
ID)&&
stub&&stub->IsScheduled();
}
}while(should_fast_track_ack);
if(!
deferred_messages_.empty()){
OnScheduled();
}
}
这个函数定义在文件external/chromium_org/content/common/gpu/gpu_channel.cc中。
GpuChannel类的成员函数HandleMessage首先检查成员变量deferred_messages_描述的一个std:
:
deque是否为空。
如果为空,那么就说明没有未处理的GPU消息,因此就直接返回。
否则的话,就取出头部的GPU消息m,并且根据这个GPU消息的RoutingID找到负责接收的一个GpuCommandBufferStub对象stub。
GpuChannel类的成员函数HandleMessage将GPU消息m分发给GpuCommandBufferStub对象stub处理之前,首先检查GpuCommandBufferStub对象stub是否自行放弃了调度,以及是否被抢占调度。
如果GpuCommandBufferStub对象stub自行放弃了调度,那么调用它的成员函数IsScheduled获得的返回值就为false。
如果GpuCommandBufferStub对象stub被抢占了调度,那么调用它的成员函数IsPreempted获得的返回值就为true。
在前一种情况下,GpuChannel类的成员函数HandleMessage什么也不做就返回。
在后一种情况下,GpuChannel类的成员函数HandleMessage调用前面分析过的成员函数OnScheduled向GPU线程的消息队列的末尾发送一个调度用的Task,也就是先将GPU线程让出来,以便GPU线程执行排在GPU线程消息队列头部的Task。
根据前面的分析,这时候排在GPU线程消息队列头部的Task可能就是用来调度Browser端OpenGL上下文的。
如果GpuCommandBufferStub对象stub既没有自行放弃调度,也没有被抢占调度,那么接下来GpuChan