背景
在in-process-gpu模式下,GPU模块以GPU线程的形式存在于Browser进程中。发现InitializeGLOneOff这个GL初始换的调用一个进程只能调用一次,在Browser主线程初始化gl,和gpu线程的初始化gl存在线程冲突,即使将主线程的gl初始化延迟到gpu模块的gl初始化之后也是没用的。结果会出现奔溃到各种gl命令的问题。
chromium GPU渲染框架
chromium按照功能模块划分成Browser/Render/GPU进程,以渲染功能来说,Render进程负责将网页从html+css文本渲染成图形,Browser负责将网页图形以及Aura UI框架合成并显示到屏幕,他们的底层绘制接口,都是通过一套GPU command buffer机制,将gl命令传递给GPU进程,让GPU进程负责执行真正的egl/gl命令。in process gpu模式下,GPU进程以线程的方式运行在Browser进程中。
Browser/Render进程发送GPU命令给GPU进程(或线程,后面相同)有两种方式:
第一种是通过GLES2Implementation访问GPU进程;
第二种是引用头文件:gpu/GLES2/gl2chromium_autogen.h,然后调用标准gl/egl命令,之所以可以这样,是因为头文件已经通过宏定义替换了gl/egl命令,以glBindBuffer为例:
#define glBindBuffer GLES2_GET_FUN(BindBuffer)
#define GLES2_GET_FUN(name) gles2::GetGLContext()->name
gles2::GetGLContext()获得一个GLES2Interface接口,最终glBindBuffer命令通过调用GLES2Interface接口的成员函数BindBuffer请求GPU进程执行glBindBuffer命令
在GPU进程中,gl/egl的共享库是通过dlopen动态加载的,所以执行gl/egl命令需要提前获取gl/egl函数指针。在chromium中,GPU进程执行gl/egl命令,需要包含头文件gl_bindings.h,其中用宏定义了每个gl/egl命令,例如:
#define glDeleteTextures ::gl::g_current_gl_context->glDeleteTexturesFn
#define g_current_gl_context g_current_gl_context_tls->Get()->Api
g_current_gl_context_tls->Get()->Api返回一个RealGLApi对象,glDeleteTextures最终调用RealGLApi中的glDeleteTexturesFn这个函数指针,这个函数指针定义在RealGLApi对象中,会在GPU线程首次MakeCurrent时,从libGLESv2.so中用dlsym的动态获取。
egl函数初始化的时机更早一些,在in process gpu thread模型中,in process gpu thread进程初始化过程中就会创建一个类型为RealEGLApi的全局变量g_current_egl_context,通过dlopen/dlsym的方式从libegl.so获取egl函数指针保存到RealEGLApi中。egl命令实际上就是通过g_current_egl_context指针获取到egl函数指针:
#define eglMakeCurrent ::gl::g_current_egl_context->eglMakeCurrentFn
gl/egl函数之所以需要通过函数指针的方式处理,是因为chromium对gl/egl函数抽象出了一个Api层即:RealGLApi/RealEGLApi,在打开chromium gpu调试开关的情况下,Api会被替换成DebugEGLApi/DebugGLApi,他们会在每一条gl/egl命令的前后增加调试信息。
在主线程绘制第三方组件
第三方组件在主线程中绘制的情况,要求主线程为组件提供一个真实的egl/gl上下文,因为组件自身链接的是libgl/libegl,也即他们走不到CommandBuffer。或者将组件集成到GPU模块,从设计上来说是不合适的,GPU模块对外提供的CommandBuffer接口功能单一,没有额外的业务逻辑处理能力,将第三方组件功能暴露到Browser/Renderer相当费力。
在Chromium的框架中,拥有真正egl/gl上下文的,只有gpu模块,browser/render进程中都是模拟的egl/gl上下文,通过command buffer机制传送gl/egl命令到GPU进程的真实egl/gl上下文中执行。
如果选择在主线程集成,可以包含头文件gl_bindings.h,其中调用的gl/egl命令都是RealGLApi对象中的真实的gl/egl函数指针。前面说过,RealGLApi中定义的gl命令会在gpu线程首次MakeCurrent时,从libGLESv2.so中dlopen/dlsym的方式动态获取。g_current_gl_context的初始化依赖于in process gpu thread的初始化,为了防止in process gpu thread还没来得及初始化,在组件线程的初始化阶段主动调用gl::InitializeGLOneOff来触发g_current_gl_context的初始化,以及获取所有的gl/egl函数指针。
InitializeGLOneOff导致的奔溃
出现egl奔溃问题后,组件模块的gl环境初始化即:InitializeGLOneOff调用进行延后,发现奔溃概率不降反升。调查了InitializeGLOneOff的实现后发现了原因是组件线程执行的InitializeGLOneOff和GPU线程执行的InitializeGLOneOff存在冲突。以glDeleteTextures为例:
1. glDeleteTextures实际上是调用::gl::g_current_gl_context的函数指针glDeleteTexturesFn
#define glDeleteTextures ::gl::g_current_gl_context->glDeleteTexturesFn
2 g_current_gl_context实际上是g_current_gl_context_tls里面的成员:Api
#define g_current_gl_context g_current_gl_context_tls->Get()->Api
3. g_current_gl_context_tls的初始化是由InitializeGLOneOff触发,并最终在这里执行:
void InitializeStaticGLBindingsGL() {
g_current_gl_context_tls = new base::ThreadLocalPointer<CurrentGL>;
..
}
看到这里并未上锁,g_current_gl_context_tls是一个base::ThreadLocalPointer<CurrentGL>的指针类型的全局变量,也就是每一个线程有一个类型为CurrentGL的线程局部变量,CurrentGL是在MakeCurrent时设值。因为框架使用了in process gpu thread模型,GPU线程和组件线程都在Browser进程内,所以共享g_current_gl_context_tls指针,组件线程在GPU线程之后执行InitializeGLOneOff,g_current_gl_context_tls就会被重置,如果这时GPU线程已经执行过MakeCurrent,并且开始调用gl命令,调用gl命令又是通过重置后的g_current_gl_context_tls指针获取的CurrentGL中的gl函数指针,这里获取的gl函数指针也将是无效指针,导致奔溃。
解决方案
按照上面的分析,InitializeGLOneOff在一个进程中只能调用一次。但是组件的渲染和GPU线程没有必然关联,如果组件内使用了gl_bindings.h定义的接口,又必须等待gl环境初始化完成。所以我们想到的一个方案是,寻找一个在GPU线程完成gl初始化后的回调,在回调里初始化组件,这样能够保证组件线程调用gl命令时,gl环境已经完成初始化,去掉了组件线程内调用的InitializeGLOneOff避免了线程冲突。我们发现这个接口满足需求:
BrowserGpuChannelHostFactory::instance()->EstablishGpuChannel(gpu::GpuChannelEstablishedCallback)
它的作用,是在GPU线程初始化完成后,建立一个Browser和GPU线程之间的IPC通道(与线程间通信相同),他保证了gl初始化 => 组件初始化的时序,可以完满解决奔溃问题。