Chromium硬件加速渲染的GPU数据上传机制分析.docx
- 文档编号:10497636
- 上传时间:2023-02-14
- 格式:DOCX
- 页数:141
- 大小:265.04KB
Chromium硬件加速渲染的GPU数据上传机制分析.docx
《Chromium硬件加速渲染的GPU数据上传机制分析.docx》由会员分享,可在线阅读,更多相关《Chromium硬件加速渲染的GPU数据上传机制分析.docx(141页珍藏版)》请在冰豆网上搜索。
Chromium硬件加速渲染的GPU数据上传机制分析
Chromium硬件加速渲染的GPU数据上传机制分析
在Chromium中,WebGL端、Render端和Browser端通过命令缓冲区将GPU命令发送给GPU进程执行。
GPU命令携带的简单参数也通过命令缓冲区发送给GPU进程,但复杂参数,例如纹理数据,有可能太大,以致于命令缓冲区无法容纳,因此要通过其它机制传递给GPU进程。
本文接下来就主要以纹理数据上传为例,分析WebGL端、Render端和Browser端将GPU命令数据传递给GPU进程的机制。
WebGL端、Render端和Browser端将GPU命令附携带的大数据传递给GPU进程的基本思路通过其它的共享缓冲区进行传递。
也就是先将GPU命令携带的大数据写入到共享缓冲区中,然后再将GPU命令携带的大数据参数修改为前面已经写入了数据的共享缓冲区的ID。
GPU进程通过这个ID就可以找到对应的共享缓冲区,进而得到真正的GPU命令数据,最后就可以执行对应的OpenGL函数。
有些操作系统对能创建的共享内存的大小有限制。
当一个GPU命令携带的数据的大小超过这个限制的时候,那么就不能通过一块共享缓冲区一次性将数据传递给GPU进程。
这时候就需要对数据进行分块传输。
有些GPU命令的数据本身就支持分块传输,这种情况的处理就比较简单。
例如,对于纹理上传命令gles2:
:
cmds:
:
TexImage2D,可以通过gles2:
:
cmds:
:
TexSubImage2D命令对其携带的纹理数据进行分块传输,如图1所示:
在图1中,我们假设一个gles2:
:
cmds:
:
TexImage2D命令要上传的纹理数据可以划分为1、2和3三个子块,每一个子块都可以通过一块共享缓冲区进行传递。
这时候一个gles2:
:
cmds:
:
TexImage2D命令就被分拆成三个gles2:
:
cmds:
:
TexSubImage2D子命令,每一个gles2:
:
cmds:
:
TexSubImage2D子命令负责处理一个子数据块。
这些gles2:
:
cmds:
:
TexSubImage2D子命令最终在GPU进程中转化为OpenGL函数glTexSubImage2D调用,每一个glTexSubImage2D函数都负责上传一个数据子块到GPU中。
很不幸,并不是所有的GPU命令都像gles2:
:
cmds:
:
TexImage2D命令一样,存在对应的子命令,例如gles2:
:
cmds:
:
ShaderSource命令,它不存在对应的gles2:
:
cmds:
:
ShaderSubSource子命令。
这时候就需要使用一种称为Bucket的机制来分块上传GPU命令数据。
以gles2:
:
cmds:
:
ShaderSource命令为例,它携带的Shader源代码的分块上传机制如图2所示:
在图2中,我们同样假设gles2:
:
cmds:
:
ShaderSource命令要上传的数据可以划分为1、2和3三个子块,每一个子都可以通过一块共享缓冲区进行传递。
每一个数据子块都是通过一个gles2:
:
cmds:
:
SetBucketData命令保存在GPU进程中的同一个Bucket中的。
每一个Bucket都具有一个ID,前面已经准备好了数据的Bucket的ID接下来再通过一个gles2:
:
cmds:
:
ShaderSourceBucket命令传递给GPU进程。
GPU进程有了这个Bucket的ID之后,就可以获得它里面的数据,进而可以调用OpenGL函数glShaderSource,从而完成对gles2:
:
cmds:
:
ShaderSource命令的处理。
不难发现,上面描述的两种GPU命令数据分块上传机制都是通过共享缓冲区进行的,这就涉及到这些共享缓冲区的管理问题,也就是分配和释放的问题。
为了更好地认识这个问题,我们首先简单介绍一下Chromium的纹理上传机制。
Chromium提供了同步和异步纹理上传机制。
我们知道,在Chromium中,所有的GPU命令都是在GPU进程的一个线程中执行的,这个线程称为GPU主线程。
将纹理上传命令全部交给GPU主线程执行就称为同步纹理上传,如图3所示:
在图3中,我们假设一个命令缓冲区有五个GPU命令需要执行,其中第一个GPU命令是同步纹理上传命令gles2:
:
cmds:
:
TexImage2D,对应的OpenGL函数是glTexImage2D。
由于使用的是同步纹理上传方式,因此,在纹理上传命令执行完成之前,后面的四个命令是不能执行的。
与GPU的图形计算和渲染速度相比,将数据从CPU传递到GPU的速度是相当慢的。
这就使得纹理上传操作是GPU的一个瓶颈,特别是数据量很大的纹理。
对于图3来说,就会造成后面的四个命令需要等待比较长时间才会被执行。
为了解决纹理上传速度慢的问题,Chromium提供了另外一种纹理上传方式——异步纹理上传,如图4所示:
在异步纹理上传方式中,GPU进程中使用一个专门的线程用作纹理上传,这个线程称为GPU传输线程。
在图4中,我们同样假设一个命令缓冲区有五个GPU命令需要执行,其中第一个GPU命令是异步纹理上传命令gles2:
:
cmds:
:
AsyncTexImage2DCHROMIUM。
这个异步纹理上传命令被GPU主线程发送给GPU传输线程处理。
GPU传输线程调用OpenGL函数执行异步纹理上传命令。
与此同时,GPU主线程也在执行后面的四个命令。
GPU传输线程上传纹理完毕,会将通过EGL函数eglCreateImageKHR将已经上传的纹理封装成一个EGLImageKHR对象。
GPU主线程会在空闲的时候检查异步上传的纹理是否已经上传完成。
对于已经上传完成的纹理,GPU主线程会通过OpenGL函数glEGLImageTargetTexture2DOES将绑定在当前激活的OpenGL上下文中。
这意味着GPU主线程可以访问在GPU传输线程中上传完毕的纹理。
这是一种跨线程的纹理共享机制。
正是由于这个纹理共享机制,才使得异步纹理上传成为可能。
有时候,GPU进程的Client端需要知道一个异步纹理上传命令什么时候执行完成,这时候它可以向GPU进程发送一个gles2:
:
cmds:
:
WaitAsyncTexImage2DCHROMIUM。
GPU主线程在处理gles2:
:
cmds:
:
WaitAsyncTexImage2DCHROMIUM的时候,就会等待GPU传输线程完成纹理上传。
当然,如果这时候GPU传输线程已经完成纹理上传,那么GPU主线程就不用等待。
GPU主线程结束等待之后,也会检查已经上传完成的纹理是否已经绑定在当前激活的OpenGL上下文中。
如果还没有绑定,那么也会通过OpenGL函数glEGLImageTargetTexture2DOES将绑定在当前激活的OpenGL上下文中。
从图4我们就可以看到,通过异步纹理上传方式,后面的四个命令可以与前面的纹理上传命令并发执行,从而提高了全部五个命令的执行时间,从而可以在一定程度上解决纹理上传速度慢的问题。
GPU进程的Client端向GPU进程发送的同步纹理上传命令gles2:
:
cmds:
:
TexImage2D和异步纹理上传命令gles2:
:
cmds:
:
AsyncTexImage2DCHROMIUM,都指定了一个共享缓冲区,这个共享缓冲区保存了要上传的纹理数据。
GPU进程的Client端不知道这个共享缓冲区什么时候释放,因为它不知道GPU进程什么时候使用完成这个共享缓冲区,也就是不知道纹理上传命令什么时候被执行。
但是GPU进程的Client端必须知道上述共享缓冲区什么时候使用完成,以便可以对它进行回收。
那么GPU进程的Client端是通过什么方式知道一个共享缓冲区什么时候不再被GPU进程使用的呢?
我们分同步纹理上传和异步纹理上传两种情况讨论。
在同步纹理上传方式中,GPU主线程执行完成一个gles2:
:
cmds:
:
TexImage2D命令,就意味着该gles2:
:
cmds:
:
TexImage2D命令引用的共享缓冲区已经使用完毕。
GPU进程的Client端往命令缓冲区写入一个gles2:
:
cmds:
:
TexImage2D命令之后,会接着在后面再写入一个gpu:
:
cmd:
:
SetToken命令,如图5所示:
上述gpu:
:
cmd:
:
SetToken命令关联了一个同步Token值。
这个同步Token值由GPU进程的Client端维护,并且与前面被gles2:
:
cmds:
:
TexImage2D命令引用的共享缓冲区对应。
GPU进程的Client每往命令缓冲区写入一个gpu:
:
cmd:
:
SetToken命令,都会将其维护的同步Token值增加1,作为下一个gpu:
:
cmd:
:
SetToken命令的同步Token值。
GPU主线程处理完成一个gpu:
:
cmd:
:
SetToken命令之后,会在当前激活的OpenGL上下文中记录该gpu:
:
cmd:
:
SetToken命令关联的同步Token值。
这样,GPU进程的Client端通过比较一个共享缓冲区的同步Token值与该Client端在GPU进程中对应的OpenGL上下文记录的当前同步Token值的大小,就可以知道该共享缓冲区是否可以进行回收。
在异步纹理上传方式中,通过上述的同步Token机制,不能确定一个共享缓冲区是否能够进行回收,因为紧跟在异步纹理上传命令后面的gpu:
:
cmd:
:
SetToken命令有可能比异步纹理上传命令本身要提前执行完成。
这时候需要使用另外一种异步Token机制,如图6所示:
异步纹理上传命令不仅gles2:
:
cmds:
:
AsyncTexImage2DCHROMIUM不仅指定了一个共享缓冲区,还指定了一个异步Token值。
这个异步Token值同样也是由GPU进程的Client端维护的,并且与异步纹理上传命令指定的共享缓冲区相关联。
GPU传输线程执行完成一个异步纹理上传命令之后,会通过函数SetAsyncUploadToken将该异步纹理上传命令指定的异步Token值设置到当前激活的OpenGL上下文中去。
这样,GPU进程的Client端通过比较一个共享缓冲区的异步Token值与该Client端在GPU进程中对应的OpenGL上下文记录的当前异步Token值的大小,就可以知道该共享缓冲区是否可以进行回收。
通过上述的同步和异步Token机制,GPU的Client端就可以对那些用来传递数据的共享缓冲区进行管理了。
接下来我们就首先分析这些共享缓冲区的管理。
在前面一文中提到,GPU进程的Client端在初始化OpenGL上下文的过程中,会创建和初始化一个TransferBuffer对象和一个BufferTracker对象,如下所示:
[cpp]viewplaincopy
boolGLES2Implementation:
:
Initialize(
unsignedintstarting_transfer_buffer_size,
unsignedintmin_transfer_buffer_size,
unsignedintmax_transfer_buffer_size,
unsignedintmapped_memory_limit){
......
if(!
transfer_buffer_->Initialize(
starting_transfer_buffer_size,
kStartingOffset,
min_transfer_buffer_size,
max_transfer_buffer_size,
kAlignment,
kSizeToFlush)){
returnfalse;
}
mapped_memory_.reset(
newMappedMemoryManager(
helper_,
base:
:
Bind(&GLES2Implementation:
:
PollAsyncUploads,
//Themappedmemorymanagerisownedby|this|here,and
//sinceitsdestroyedbeforebeforewedestroyourselves
//wedon'tneedextrasafetymeasuresforthisclosure.
base:
:
Unretained(this)),
mapped_memory_limit));
......
buffer_tracker_.reset(newBufferTracker(mapped_memory_.get()));
......
returntrue;
}
这个函数定义在文件external/chromium_org/gpu/command_buffer/client/gles2_implementation.cc中。
GLES2Implementation类的成员变量transfer_buffer_指向的是一个TransferBuffer对象,GLES2Implementation类的成员函数Initialize调用它的成员函数Initialize对它进行初始化。
这个TransferBuffer对象初始化完成后,GPU进程的Client端就可以通过它分配一些共享缓存区,用来和GPU进程传递数据。
GLES2Implementation类的成员函数Initialize接下来创建了一个MappedMemoryManager对象,并且以这个MappedMemoryManager对象为参数,创建了一个BufferTracker对象,保存在成员变量buffer_tracker_中。
以后GPU进程的Client端也可以通过这个BufferTracker分配共享缓冲区,用来和GPU进程传递数据。
接下来,我们就继续分析TransferBuffer类和BufferTracker类的实现,以便了解它们是如何管理共享缓冲区的。
我们从TransferBuffer类的成员函数Initialize开始分析TransferBuffer类的实现,如下所示:
[cpp]viewplaincopy
boolTransferBuffer:
:
Initialize(
unsignedintdefault_buffer_size,
unsignedintresult_size,
unsignedintmin_buffer_size,
unsignedintmax_buffer_size,
unsignedintalignment,
unsignedintsize_to_flush){
result_size_=result_size;
default_buffer_size_=default_buffer_size;
min_buffer_size_=min_buffer_size;
max_buffer_size_=max_buffer_size;
alignment_=alignment;
size_to_flush_=size_to_flush;
ReallocateRingBuffer(default_buffer_size_-result_size);
returnHaveBuffer();
}
这个函数定义在文件external/chromium_org/gpu/command_buffer/client/transfer_buffer.cc中。
各个参数的含义如下所示:
default_buffer_size:
表示默认分配的共享缓冲区的大小。
result_size:
当TransferBuffer用来将数据从GPU进程返回给GPU进程的Client端时,分配出来的缓冲区的头部用来填写返回值。
这个头部的大小就由参数result_size描述。
min_buffer_size:
表示允许分配的共享缓冲区的最小值。
max_buffer_size:
表示允许分配的共享缓冲区的最大值。
alignment:
分配出来的共享缓冲区被划分为一个个的子缓冲区进行使用,这些子缓冲区的大小要对齐到参数alignment描述的值。
size_to_flush:
当从共享缓冲区分配出去的子缓冲区的大小达到参数size_to_flush描述的值后,GPU进程的Client端就会请求GPU进程执行命令缓冲区的命令,以便可以回收这些命令引用的子缓冲区。
TransferBuffer类的成员函数Initialize将上述参数分别保存在对应的成员变量中后,就调用另外一个成员函数ReallocateRingBuffer分配共享缓冲区,如下所示:
[cpp]viewplaincopy
voidTransferBuffer:
:
ReallocateRingBuffer(unsignedintsize){
//Whatsizebufferwouldweaskforifweneededanewone?
unsignedintneeded_buffer_size=ComputePOTSize(size+result_size_);
needed_buffer_size=std:
:
max(needed_buffer_size,min_buffer_size_);
needed_buffer_size=std:
:
max(needed_buffer_size,default_buffer_size_);
needed_buffer_size=std:
:
min(needed_buffer_size,max_buffer_size_);
if(usable_&&(!
HaveBuffer()||needed_buffer_size>buffer_->size())){
if(HaveBuffer()){
Free();
}
AllocateRingBuffer(needed_buffer_size);
}
}
这个函数定义在文件external/chromium_org/gpu/command_buffer/client/transfer_buffer.cc中。
TransferBuffer类的成员函数ReallocateRingBuffer首先根据参数size和成员变量result_size_、min_buffer_size_和max_buffer_size_计算出应该分配的共享缓冲区的大小needed_buffer_size。
TransferBuffer类的成员变量usable_的值初始化为true,接下来如果因为大小限制不能成功分配到共享缓冲区,那么该成员变量的值就会被设置为false。
当TransferBuffer类的成员变量buffer_的值不等于NULL时,它指向的是一个Buffer对象,该Buffer对象描述的就是一个共享缓冲区,这时候调用TransferBuffer类的成员函数HaveBuffer得到的返回值就为true,并且调用该Buffer对象的成员函数size可以获得的它描述的共享缓冲区的大小。
因此,TransferBuffer类的成员函数ReallocateRingBuffer所做的事情就是判断要求分配的共享缓冲区的大小是否合适。
如果合适,并且之前还没有分配过共享缓冲区,或者之前已经分配过,但是分配的大小小于前面计算出来的应该分配的大小,那么就需要重新分配一块大小等于needed_buffer_size的共享缓冲区。
当然,如果之前已经分配过共享缓冲区,那么这块共享缓冲区会首先被释放掉,这是通过调用TransferBuffer类的成员函数Free实现的。
最后,TransferBuffer类的成员函数ReallocateRingBuffer通过调用另外一个成员函数AllocateRingBuffer分配一块大小等于needed_buffer_size的共享缓冲区,如下所示:
[cpp]viewplaincopy
voidTransferBuffer:
:
AllocateRingBuffer(unsignedintsize){
for(;size>=min_buffer_size_;size/=2){
int32id=-1;
scoped_refptr : Buffer>buffer= helper_->command_buffer()->CreateTransferBuffer(size,&id); if(id! =-1){ DCHECK(buffer); buffer_=buffer; ring_buffer_.reset(newRingBuffer( alignment_, result_size_, buffer_->size()-result_size_, helper_, static_cast buffer_id_=id; result_buffer_=buffer_->memory(); result_shm_offset_=0; return; } //wefailedsodon'ttrylargerthanthis. max_buffer_size_=size/2; } usable_=false; } 这个函数定义在文件external/chromium_org/gpu/command_buffer/client/transfer_buffer.cc中。 TransferBuffer类的成员函数AllocateRingBuffer在保证要分配的共享缓冲区的大小size不小于允许的最小值min_buffer_size_的前提下,分配一个大小等于size的共享缓冲区。 如果能成功分配,那么分配出来的共享缓冲区使用一个Buffer对象来描述。 这个Buffer对象保存在TransferBuffer类的成员变量buffer_中。 并且分配出的共享缓冲区的ID保存在TransferBuffer类的成员变量buffer_id_中。 前面提到,分配出来的共享缓冲区的头部用来保存从GPU进程读取数据时的结果,因此这个头部的地址就等于分配出来的共享缓冲区的起始地址,保存在TransferBuffer类的成员变量result_buffer_中。 同时,TransferBuffer类的成员变量result_shm_offset_表示上述用来保存结果的头部位于分配出来的共享缓冲区的偏移位置,它的值被设置为0。 如果不能成功分配,那么就可能是请求分配的大小太大了,于是就尝试减少一半的大小,即以(size/2)的大小,再次尝试分配。 这时候也需要相应地调整允许分配的共享缓冲区的最大值max_buffer_size_,也就是允许分配的共享缓冲区的最大值max_buffer_size_就等于(size/2)。 这个过程一直持续下去,直到分配成功,或者请求分配的大小小于允许的最小值min_buffer_size_为
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Chromium 硬件加速 渲染 GPU 数据 上传 机制 分析
![提示](https://static.bdocx.com/images/bang_tan.gif)