原文链接:https://developer.chrome.com/native-client/devguide/coding/3D-graphics
注意:已针对ChromeOS以外的平台公布了此处所述技术的弃用。
请访问我们的 迁移指南 了解详情。
3D图形
Native Client应用程序使用OpenGL ES 2.0 API进行3D渲染。本文档介绍如何在Native Client模块中调用OpenGL ES 2.0接口以及如何构建高效的呈现循环。它还解释了如何验证GPU驱动程序和测试特定的GPU功能,并提供了有助于确保渲染代码高效运行的提示。
注意:3D绘图和OpenGL是复杂的主题。本文档仅涉及与Native Client环境中的编程直接相关的问题。要了解有关OpenGL ES 2.0本身的更多信息,请参阅OpenGL ES 2.0编程指南。
验证客户端图形平台
Native Client是一种软件技术,它允许您对应用程序进行一次编码并在多个平台上运行,而无需担心每个可能的目标平台上的实现细节。在硬件级别提供相同的支持很困难。图形硬件来自许多不同的制造商,并由不同质量的驱动程序控制。特定的GPU驱动程序可能不支持每个OpenGL ES 2.0功能,并且已知某些驱动程序具有可被利用的漏洞。
即使GPU驱动程序可以安全使用,您的程序也应该在启动应用程序之前执行验证检查,以确保驱动程序支持您需要的所有功能。
用JavaScript审核驱动程序
在启动时,应用程序应执行一些可在其托管网页上以JavaScript实现的其他测试。执行这些测试的脚本应该包含在模块的embed
标记之前,理想情况下,embed
只有在这些测试成功时,标记才会出现在托管页面上。
首先要检查的是你是否可以创建图形上下文。如果可以,请使用上下文确认是否存在任何所需的OpenGL ES 2.0扩展。在检查扩展时,您可能需要引用扩展注册表并包含供应商前缀。
在Native Client中审核驱动程序
创建一个上下文
一旦您通过了JavaScript验证测试,就可以安全地将Native Client embed标记添加到托管网页并加载模块。作为模块初始化代码的一部分,您必须通过创建C ++ Graphics3D
对象或调用PPB_Graphics3D
API函数为应用程序创建图形上下文Create
。不要以为这总会成功; 你仍然可能在创建上下文时遇到问题。如果您处于开发模式且无法创建上下文,请尝试创建更简单的版本,以查看是否要求不支持的功能或超出驱动程序资源限制。您的生产代码应始终检查上下文是否已创建,如果不是这样,则应正常失败。
检查扩展和功能
并非每个GPU都支持每个扩展或具有相同数量的纹理单元,顶点属性等。在启动时,调用glGetString(GL_EXTENSIONS)
并检查扩展和所需的功能。例如:
- 如果您使用mipmaps的非2次幂纹理,请确保
GL_OES_texture_npot
存在。 - 如果您使用浮点纹理,请确保
GL_OES_texture_float
存在。 - 如果使用的是DXT1,DXT3,DXT5或纹理,确保相应的扩展
EXT_texture_compression_dxt1
,GL_CHROMIUM_texture_compression_dxt3
以及GL_CHROMIUM_texture_compression_dxt5
存在的。 - 如果您正在使用的功能
glDrawArraysInstancedANGLE
,glDrawElementsInstancedANGLE
,glVertexAttribDivisorANGLE
,或PPAPI接口PPB_OpenGLES2InstancedArrays
,确保相应的扩展GL_ANGLE_instanced_arrays
存在。 - 如果您正在使用该功能
glRenderbufferStorageMultisampleEXT
或PPAPI接口PPB_OpenGLES2FramebufferMultisample
,请确保GL_CHROMIUM_framebuffer_multisample
存在相应的扩展名。 - 如果您正在使用的功能
glGenQueriesEXT
,glDeleteQueriesEXT
,glIsQueryEXT
,glBeginQueryEXT
,glEndQueryEXT
,glGetQueryivEXT
,glGetQueryObjectuivEXT
,或PPAPI接口PPB_OpenGLES2Query
,确保相应的扩展GL_EXT_occlusion_query_boolean
存在。 - 如果您正在使用的功能
glMapBufferSubDataCHROMIUM
,glUnmapBufferSubDataCHROMIUM
,glMapTexSubImage2DCHROMIUM
,glUnmapTexSubImage2DCHROMIUM
,或PPAPI接口PPB_OpenGLES2ChromiumMapSub
,确保相应的扩展GL_CHROMIUM_map_sub
存在。
检查系统功能glGetIntegerv
并相应地调整着色器程序以及纹理和顶点数据:
- 如果在顶点着色器中使用纹理,请确保
glGetIntegerv(GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS, ...)
并glGetIntegerv(GL_MAX_TEXTURE_SIZE, ...)
返回大于0的值。 - 如果在单个着色器中使用的纹理超过8个,请确保
glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, ...)
返回的值大于或等于所需的同时纹理数。
在Chrome网上应用店中审核驱动程序
如果您选择将应用程序放在Chrome Web Store中,则其Web Store 清单文件可以webgl
在requirements参数中包含该功能。它看起来像这样:
"requirements": {
"3D": {
"features": ["webgl"]
}
}
虽然WebGL在技术上是一个JavaScript API,但指定该webgl
功能也适用于OpenGL ES 2.0,因为两个接口都使用相同的驱动程序。
此清单项目不是必需的,但如果您将其包含在内,那么如果浏览器在不支持OpenGL ES 2.0或使用已知列入黑名单的GPU驱动程序的计算机上运行,Chrome网上应用店将阻止用户安装该应用程序可以邀请一次攻击。
如果Web Store确定用户的驱动程序不足,则应用程序将不会显示在商店的磁贴显示中。但是,它会出现在商店搜索结果中,或者如果用户直接链接到它,在这种情况下,用户仍然可以下载它。但是当用户到达安装页面时将检查清单要求,如果出现问题,浏览器将显示消息“此计算机不支持此应用程序。安装已被禁用。“
基于清单的检查仅适用于直接从Chrome网上应用店下载。通过内联安装加载应用程序时不会执行此操作。
遇到问题时该怎么办
使用上述审查程序,您应该能够在应用程序运行之前检测最常见的问题。如果存在问题,您的代码应尽可能清楚地描述问题。如果缺少功能,这很容易。无法创建图形上下文更难以诊断。至少,您可以建议用户尝试更新驱动程序。您可能希望转到描述如何进行更新的Chrome页面。
如果用户无法更新驱动程序,或者问题仍然存在,请务必收集有关其图形环境的信息。询问Chrome about:gpu
页面的内容 。
记录不可靠的驱动程序
在用户文档中包含有关已知可疑驱动程序的信息会很有帮助。这可能有助于确定流氓驱动程序是否是问题的原因。GPU驱动程序黑名单有很多来源。可以在Chromium项目 和Khronos找到两个这样的列表。您可以使用这些列表在文档中包含警告用户有关危险驱动程序的信息。
测试你的防御
您可以通过使用以下标志运行Chrome(一次性全部)并观察应用程序如何响应来测试您的驱动程序验证代码:
--disable-webgl
--disable-pepper-3d
--disable_multisampling
--disable-accelerated-compositing
--disable-accelerated-2d-canvas
调用OpenGL ES 2.0命令
在Native Client中编写OpenGL ES 2.0调用有三种方法。
使用“纯”OpenGL ES 2.0函数调用
您可以通过Pepper扩展库进行OpenGL ES 2.0调用。SDK示例以examples/api/graphics_3d
这种方式工作。在文件中 graphics_3d.cc
,密钥初始化步骤如下:
-
在文件顶部添加以下内容:
-
#include <GLES2/gl2.h> #include "ppapi/lib/gl/gles2/gl2ext_ppapi.h"
定义功能
InitGL
。确切的规范attrib_list
将是特定于应用程序的。 -
包含逻辑bool InitGL(int32_t new_width, int32_t new_height) { if (!glInitializePPAPI(pp::Module::Get()->get_browser_interface())) { fprintf(stderr, "Unable to initialize GL PPAPI! "); return false; } const int32_t attrib_list[] = { PP_GRAPHICS3DATTRIB_ALPHA_SIZE, 8, PP_GRAPHICS3DATTRIB_DEPTH_SIZE, 24, PP_GRAPHICS3DATTRIB_WIDTH, new_width, PP_GRAPHICS3DATTRIB_HEIGHT, new_height, PP_GRAPHICS3DATTRIB_NONE }; context_ = pp::Graphics3D(this, attrib_list); if (!BindGraphics(context_)) { fprintf(stderr, "Unable to bind 3d context! "); context_ = pp::Graphics3D(); glSetCurrentContextPPAPI(0); return false; } glSetCurrentContextPPAPI(context_.pp_resource()); return true; }
Instance::DidChangeView
以InitGL
在必要时调用:在应用程序启动时(当图形上下文为NULL时)以及模块的View更改大小时。
使用Regal
如果您正在移植OpenGL ES 2.0应用程序,或者习惯使用OpenGL ES 2.0编写,那么您应该坚持使用上面描述的Pepper API或纯OpenGL ES 2.0调用。如果要移植使用不在OpenGL ES 2.0中的功能的应用程序,请考虑使用Regal。Regal是一个支持许多OpenGL版本的开源库。Regal最近添加了对Native Client的支持。Regal将大多数OpenGL调用直接转发到底层图形库,但它也可以模拟其他未包含的调用(当存在硬件支持时)。有关 详细信息,请参阅libregal。
使用Pepper API
您的代码可以直接调用Pepper PPB_OpenGLES2 API,就像任何Pepper接口一样。当您以这种方式编写时,每次调用OpenGL ES 2.0函数都必须以对Pepper接口的引用开始,第一个参数是图形上下文。要调用该函数glCompileShader
,您的代码可能如下所示:
ppb_g3d_interface->CompileShader(graphicsContext, shader);
这种方法专门针对Pepper API。每个调用对应一个OpenGL ES 2.0函数,但语法对于Native Client是唯一的,因此源文件不可移植。
实现渲染循环
图形应用程序需要以高频率运行的连续帧渲染和重绘循环。要获得最佳帧速率,了解Native Client模块中的OpenGL ES 2.0代码如何与Chrome进行交互非常重要。
Chrome和Native Client流程
Chrome是一款多进程浏览器。每个Chrome标签都是一个单独的进程,运行具有自己主线程的应用程序(我们称之为Chrome主线程)。当应用程序启动Native Client模块时,该模块将在新的单独沙盒进程中运行。模块的进程有自己的主线程(Native Client线程)。Chrome和Native Client进程在其主线程上使用Pepper API调用相互通信。
当Chrome主线程调用Native Client线程(例如键盘和鼠标回调)时,Chrome主线程将阻止。这意味着Native Client线程上的冗长操作可以从Chrome中窃取周期,并且在Native Client线程上执行阻止操作可能会使您的应用程序停顿。
Native Client使用回调函数来同步两个进程的主线程。只有某些Pepper函数使用回调; SwapBuffers 就是其中之一。
SwapBuffers
及其回调函数
SwapBuffers
是无阻碍的; 它从Native Client线程调用并立即返回。当SwapBuffers
被调用时,它异步运行的Chrome的主线程上。它切换图形数据缓冲区,处理任何所需的合成操作,并重绘屏幕。屏幕更新完成后,SwapBuffer
将从Chrome线程调用作为参数之一包含的回调函数,并在Native Client线程上执行。
要创建渲染循环,Native Client模块应该包含一个执行渲染工作然后执行的函数SwapBuffers
,并将自身作为SwapBuffer
回调传递。如果您的渲染代码高效且运行速度快,则此方案将实现最高的帧速率。该文档SwapBuffers
解释了为什么这是最佳的:因为仅当插件的当前状态实际在屏幕上时才执行回调,此功能提供了一种速率限制动画的方法。通过在绘制下一帧之前等待图像在屏幕上,您可以确保不会比屏幕更新更快地生成更新。
下图说明了Chrome和Native Client进程之间的交互。特定于应用程序的呈现代码在Draw
Native Client线程上调用的函数中运行。蓝色向下箭头阻止从主线程到Native Client的调用,绿色向上箭头是SwapBuffers
从Native Client到主线程的非阻塞 调用。所有OpenGL ES 2.0调用都是Draw
在Native Client线程中进行的。
SDK示例 graphics_3d
SDK示例graphics_3d
使用函数MainLoop
(in hello_world.cc
)创建如上所述的呈现循环。MainLoop
调用Render
执行渲染工作,然后调用SwapBuffers
,将自身作为回调传递。
void MainLoop(void* foo, int bar) {
if (g_LoadCnt == 3) {
InitProgram();
g_LoadCnt++;
}
if (g_LoadCnt > 3) {
Render();
PP_CompletionCallback cc = PP_MakeCompletionCallback(MainLoop, 0);
ppb_g3d_interface->SwapBuffers(g_context, cc);
} else {
PP_CompletionCallback cc = PP_MakeCompletionCallback(MainLoop, 0);
ppb_core_interface->CallOnMainThread(0, cc, 0);
}
}
管理OpenGL ES 2.0管道
OpenGL ES 2.0命令无法在Chrome或Native Client进程中运行。它们被传递到共享存储器中的FIFO队列,最好将其理解为GPU命令缓冲区。命令缓冲区由专用GPU进程共享。通过使用单独的GPU流程,Chrome实现了另一层运行时安全性,在将所有OpenGL ES 2.0命令及其参数发送到GPU之前对其进行审查。通过FIFO缓冲命令也可以加快代码速度,因为Native Client线程中的每个OpenGL ES 2.0调用都会立即返回,而处理可能会因GPU降低FIFO中排队的命令而延迟。
在更新屏幕之前,所有介入的OpenGL ES 2.0命令必须由GPU处理。程序员经常尝试通过在渲染代码中使用glFlush
和glFinish
命令来确保这一点 。在Native Client的情况下,这通常是不必要的。该SwapBuffers
命令执行隐式刷新,Chrome团队不断调整GPU代码以尽快使用OpenGL ES 2.0 FIFO。
有时3D应用程序可以以难以处理的方式写入FIFO。命令管道可能会填满,您的代码必须等待GPU刷新FIFO。如果是这种情况,您可以添加glFlush
调用以加速OpenGL ES 2.0命令FIFO的流程。在开始添加自己的刷新之前,首先尝试通过监视每帧的渲染时间并查找不一致落在同一OpenGL ES 2.0调用上的不规则尖峰来确定管道饱和度是否真的成为问题。如果您确信管道需要加速,请插入glFlush
在启动不生成OpenGL ES 2.0命令的处理块之前调用代码。例如,在开始任何多线程粒子工作之前发出刷新,这样当您再次开始执行OpenGL ES 2.0调用时,命令缓冲区将会清除。确定呼叫的地点和频率glFlush
可能很棘手,您需要尝试找到最佳点。
渲染和非活动标签
用户通常会在多标签浏览器中切换选项卡。执行3D渲染的性能良好的应用程序应暂停任何实时处理,并在其选项卡变为非活动状态时为其他进程产生循环。
在Chrome中,非活动选项卡将继续执行定时功能(例如 setInterval
和setTimeout
),但定时器间隔将自动覆盖,并且在选项卡处于非活动状态时限制为不少于一秒。此外,SwapBuffers
在选项卡再次处于活动状态之前,不会发送与呼叫关联的任何回叫。除了SwapBuffers
选项卡处于非活动状态之外,您可能会从函数接收异步回调。根据应用程序的设计,您可以选择在它们到达时处理它们,或者将它们排入缓冲区并在选项卡变为活动状态时对它们进行处理。
标签处于非活动状态时经过的时间可能相当大。如果主线程脉冲基于SwapBuffers
回调,则当选项卡处于非活动状态时,您的应用将不会更新。Native Client模块应该能够检测并响应其运行的选项卡的状态。例如,当选项卡变为非活动状态时,您可以在Native Client线程中设置一个原子标志,该标志将跳过3D渲染并SwapBuffers
调用并继续每隔30毫秒左右调用主线程。这提供了时间来更新仍应在后台运行的功能,如音频。调用sched_yield
或usleep
在任何工作线程上释放资源并将循环停止到操作系统也可能会有所帮助 。
处理主线程中的选项卡激活
您可以在托管页面上使用JavaScript检测并响应激活或停用标签页。添加一个EventListener visibilitychange
,将消息发送到Native Client模块,如下例所示:
document.addEventListener('visibilitychange', function(){
if (document.hidden) {
// PostMessage to your Native Client module
document.nacl_module.postMessage('INACTIVE');
} else {
// PostMessage to your Native Client module
document.nacl_module.postMessage('ACTIVE');
}
}, false);
处理来自Native Client线程的选项卡激活
您还可以直接从Native Client模块检测并响应选项卡的激活或取消激活,方法是在函数中包含代码,pp::Instance::DidChangeView
只要模块视图发生更改,就会调用该代码 。代码可以调用ppb::View::IsPageVisible
以确定页面是否可见。不可见页面的最常见原因是页面位于后台选项卡中。
提示和最佳实践
以下是编写安全代码并使用Pepper 3D API获得最佳性能的一些建议。
这样做的
-
确保启用attrib 0. OpenGL要求您启用attrib 0,但OpenGL ES 2.0不启用。例如,您可以定义具有2个属性的顶点着色器,编号如下:
glBindAttribLocation(program, "positions", 1); glBindAttribLocation(program, "normals", 2);
在这种情况下,着色器不使用attrib 0,如果Chrome在OpenGL上模拟OpenGL ES 2.0,Chrome可能必须执行一些额外的工作。即使您不使用attrib 0,启用attrib 0也总是更有效。
- 检查着色器如何编译。着色器可以在不同系统上进行不同的编译,这可能导致
glGetAttrib*
函数返回不同的结果。每次重新编译着色器时,请确保顶点属性索引与相应的名称匹配。 - 谨慎更新指数。出于安全原因,必须验证所有索引。如果更改索引,Native Client将再次验证它们。构建代码,以便不经常更新索引。
- 使用较小的插件,让CSS缩放它。如果您遇到填充问题,通过CSS执行扩展可能会有所帮助。插件渲染的大小由
<embed>
模块元素的width和height属性决定。网页上显示的实际大小由应用于元素的CSS样式控制。 - 避免矩阵到矩阵的转换。对于某些版本的Mac OS,编译着色器时存在驱动程序问题。如果您遇到矩阵变换的编译器错误,请避免矩阵到矩阵的转换。例如,在通过mat4转换它之前,将vec3转换为vec4,而不是将mat4转换为mat3。
注意事项
- 不要使用客户端缓冲区。OpenGL ES 2.0可以使用
glVertexAttribPointer
和使用客户端数据glDrawElements
,但这确实很慢。尽量避免客户端缓冲区。请改用顶点缓冲区对象(VBO)。 - 不要混合顶点数据和索引数据。默认情况下,Pepper 3D将缓冲区绑定到单个点。您可以创建一个缓冲区,并将其绑定到两个
GL_ARRAY_BUFFER
和GL_ELEMENT_ARRAY_BUFFER
,但是这将是昂贵的开销,所以不推荐。 - 在渲染过程中不要调用glGet *或glCheck *。这是OpenGL程序的常规建议,但对于Chrome上的3D尤为重要。调用名称以这些字符串开头的任何OpenGL ES 2.0函数都会阻塞Native Client线程。这包括
glGetError
; 避免在发布版本中调用它。 - 不要使用固定点(GL_FIXED)顶点属性。OpenGL ES 2.0不支持定点属性,因此在OpenGL ES 2.0中模拟它们的速度很慢。默认情况下,
GL_FIXED
在Pepper 3D API中关闭支持。 - 不要从GPU读取数据。不要打电话
glReadPixels
,因为它很慢。 - 不要更新大缓冲区的一小部分。在当前的OpenGL ES 2.0实现中,当您更新缓冲区的一部分(
glSubBufferData
例如)时,必须重新处理整个缓冲区。要避免此问题,请将静态和动态数据保存在不同的缓冲区中。 - 不要调用glDisable(GL_TEXTURE_2D)。这是一个OpenGL ES 2.0错误。每次调用时,Chrome的
about:gpu
标签中都会显示错误消息 。
CC-By 3.0许可下提供的内容