硬件加速的PowerVR光线追踪API(一)

本文我将对PowerVR光线追踪团队创建的API进行简要介绍。该API允许开发人员访问Wizard 架构的PowerVR光线追踪硬件。我将简单概述硬件层发生的物理变化及其相比传统的GPU所变化的具体内容。随后,我将着重讨论新创建的供开发人员访问硬件的OpenGL ES 3.1扩展程序。

在2016年度游戏开发者大会(GDC)上,Imagination的 idc16开发人员会议对API进行了讨论,点击此处可获取API讨论资讯。

以下内容的前提是默认各位读者已经了解光线追踪的概念。若不了解,可以先查阅光线追踪的相关信息。本文不会深入探讨每个功能,详细的信息将在后续的文章中进行讨论。

前一段时间,我们介绍了启动光线追踪功能的四核集群PowerVR GR6500 GPU。最近,我们又展示了一些目前在GPU上运行的光线追踪应用程序。

下图是我们添加的用于加速光线追踪的硬件功能:

1

图表展示了光线追踪新的硬件模

光线数据管理
光线数据管理(RDM)模块将交叉点归至统一着色集群(USC)中,再由集群进行光线着色

场景层级生成器
场景层级生成器(SHG)模块即使用标准顶点着色器生成的世界坐标顶点,在光线交点处理器之后生成加速结构。

相干引擎
该模块是光线追踪装置(RTU)的一部分,并以相同的方式缓冲了一组遍历场景层级的光线且遵循类似的执行路径。这样,在内存中获取光线数据时便可以避免缓存缺失,并由此减少带宽和功耗。更多信息将在后续文章中详细介绍。同时,您还可以关注2014GDC论坛获取更多相干引擎的资讯。

光线交叉处理器
该模块是RTU的一部分,可以测试SGH生成的位于主内存的三角形光线及场景层级光线。

帧累加缓存
该硬件模块从USC中获取累积指令,并使用编写的缓存来加速某些在光线追踪着色器中可以使用的图像原子操作。这意味着我们可以发出只编写指令,这些指令将异步列队并执行。

PowerVR GR6500 GPU在PCIe卡上进行集成,如下图所示,使用RTU时其峰值性能为每秒3亿光线。SHG的目标是生成加速结构,使顶点输出从每秒1亿动态三角形开始加速。

2

The API

光线跟踪硬件对典型的GPU设计进行了延伸,使之不仅仅只是单一的硬件。我们决定通过添加光线追踪功能来扩展OpenGL ES 3.1,以充分利用这个开发人员较为熟知的OpenGL ES API。我们选择使用现代规范,如直接状态访问、无绑定等,因为预计这将是未来的API模式。Vulkan API的潜力不容忽视,但Vulkan仍处于待开发中,本文只介绍OpenGL ES扩展。

● 顶点处理

光线追踪器输入与光栅输入是不同的。光线追踪器处理的是世界坐标中的对象,这是因为光线追踪器需要访问整个场景。例如,其场景中的对象反映的摄像机获取的某个对象,而光栅中这却很难处理。

正因为如此,PowerVR硬件光线追踪API处理顶点输出时是在世界坐标而非光栅的剪辑空间中。这意味着我们将要以不同的帧率运行各层的顶点至光栅中——在光栅中,摄像机通常从帧到帧移动。即便不是,大多数游戏平台也将重新渲染场景,因为传输过程中某些信息发生了改变。

这不同于光线追踪器。我们从摄像机运动中解耦,在应用程序启动时运行顶点着色器,随后,如果场景中没有任何对象在世界坐标中发生移动,则无需再次运行顶点着色器。当然,当摄像机移动时,仍然需要在每一帧进行光线遍历,但为场景静态部分生成的场景层级则无需如此。

预期OpenGL ES的变量及其他特性运行如下:

layout(std140, binding=0) uniform UboPerObjectIn {
highp mat4 worldFromModel;
highp mat4 worldFromModelIT;
} UboPerObject[2];

out gl_PerVertex {
vec4 gl_Position;
float gl_PointSize;
};
out PerVertexData {
vec3 vertexNormal;
};
void main() {
gl_Position = UboPerObject[gl_BuildIDIMG].worldFromModel * vec4(inVertex, 1.0); // Output in world space
vertexNormal = (UboPerObject[gl_BuildIDIMG].worldFromModelIT * vec4(inNormal, 0.0)).xyz

(gl_BuildIDIMG explained below)

然而,由于世界坐标中的顶点处理与光栅顶点处理不同,因此我们需要不同的API来进行顶点处理……

● 场景构造

对顶点和变量进行处理时将生成对象的坐标位置,使用该位置便可以生成场景层级加速结构。光线遍历阶段需要使用此加速结构。计算加速结构工作量相对较大。这就是为何在硬件中设计了场景层级加速器模块——可以有效地生成场景层级加速结构。我们还设计了API,这样开发人员就可以将场景分为组件、组件集、场景和场景集。

这样分解使得开发人员得以更有效地使用硬件。
当我们发出构建命令时,将在场景中指定的部分执行三角形的顶点着色器。随后,便使用该部分的输出计算加速结构。

Components

3

将各组件连接到顶点阵列对象。其与调用glDraw*()的定义类似。不过,并非是立即执行该调用,而是对其进行记录和延期,直至我们在包含这些组件的组件集中发出一个构建命令。

还可以对该组件设置其他属性,以便在光线遍历阶段进行交互:
•正面(顺时针或逆时针方向)
•几何图形是否遮挡光线
•几何图形是否对对某类光线可见
•可见表面(前或后)
生成组件并设置属性的例子如下:

glGenVertexArrays(1, &vertexArray);
// ... upload index data and model space vertex data as usual
glCreateComponentsIMG(1, &componentHandle);
glComponentVertexArrayIMG(componentHandle, vertexArray);
glComponentIndexedGeometryIMG(componentHandle, GL_TRIANGLES, numIndices, GL_UNSIGNED_SHORT, 0, 0);
glComponentBufferRangeIMG(componentHandle, GL_UNIFORM_BUFFER, 0, transformUBOHandle, 0, transformUBOSize);
glComponentVisibleFaceIMG(componentHandle, GL_FRONT_AND_BACK);
glComponentOccluderIMG(componentHandle, GL_TRUE);

请注意,GPU尚未有待执行的对象。此刻,仅指定组件的数据。同样要注意的是,顶点缓冲区需要保持激活状态,因为组件需要对其进行参照——并非复制。

组件集

组件集

组件集是最小的可分割处理单元,可用于创建或合并命令。

glCreateComponentGroupsIMG(1, &componentGroupHandle);
componentGroupHandle = glGetComponentGroupHandleIMG(componentGroupHandle);
glComponentGroupExtentIMG(componentGroupHandle, &extentMin, &extentMax); // Set the extents

我们还可以指定最小/最大几何图形区段,即创建组件集后在先前创建的组件之外对其进行构建。这里我们可以将顶点着色器和光线追踪器与组件联系起来。在这个点上界定组件的材质。构建命令以启动硬件SHG部分:

componentHandle = glGetComponentProgramHandleIMG(componentHandle, vertexAndRayShaderProgram);
std::vector components = {{componentHandle}};
glBuildComponentGroupIMG(0, componentGroupHandle, components.size(), components.data());
// Create a fence sync for the scene generation so that we know when it's ready
sceneHierarchyGenerationSync = glFenceSync(GL_SYNC_SHG_COMMANDS_COMPLETE_IMG, 0);

同时还可以发出合并命令。合并命令将两个组件集合并在一起。这样开发人员便可以选择以不同的帧率重建场景的某些部分。构建场景子集,并与已经建立的子集合并,尽管要权衡场景层级的质量,但合并相比完全重建子集更加节省功耗。

以汽车在地面移动为例。在启动应用程序时便创建地面组件集,且使之一直保持不变。随后,当汽车在地面移动时,创建汽车组件集,必要时每一帧画面都可创建。

std::vector componentGroups = {{...}};
glMergeComponentGroupsIMG(mergedComponentGroupResultHandle, componentGroups.size(), componentGroups.data(), GL_DONT_CARE);
sceneHierarchyGenerationSync = glFenceSync(GL_SYNC_SHG_COMMANDS_COMPLETE_IMG, 0);

上述代码中,GL fences可供硬件使用来同步访问内存。它还使得多缓冲区操作变成一种可能。调用合并/构建后,立刻等待fence直至硬件执行完命令,且这是在启动应用程序时构建场景层级的一种方式。不过这样CPU side stall在运行时便会出现状态不佳。但相反,我们可以多缓存合并/构建命令,以便在GPU硬件读/写一组对象时在CPU上设置第二组对象。而为了更容易实现这一点,可以将第一个参数输入glBuildComponentGroupIMG()命令中。通过glsl builtin gl_BuildIDIMG将该指数输入顶点着色器中,且通过上述示例可见,这将有助于多缓存对象数据UBO。这意味着我们可以在世界坐标中移动组件集,且无需暂停CPU。

场景阵列

场景阵列包含了一个或更多在单个调度内可遍历的组件集。我们需要指定每个场景阵列的光线规格,以上可以通过glSceneArrayRayBlockSizeIMG完成。

GLuint sceneArray = glCreateSceneArrayIMG();
glSceneArrayRayBlockSizeIMG(sceneArray, 0, 0); // Primary rays
glSceneArrayRayBlockSizeIMG(sceneArray, 1, 12); // Shadow rays
glBindSceneArrayComponentGroupIMG(sceneArray, 0, componentGroupHandle);

单个场景阵列中的多个组件集对几何图形层次细节 (LOD)如进行光栅化时很用用处。例如,当我们光线追踪具有复杂几何图形的场景时,我们可能需要场景的多个LOD以帮助加速交叉测试。在着色器中可以切换至不同的场景,这样便可以使用距离等测试切换至更低一层的LOD场景中。

更简单更具逻辑性的一个例子是具有虚拟监控和闭路电视摄像头的场景。当我们用光线追踪监控器时,可以切换到中央电视台现场并启动摄像机视角的光线。

glBindSceneArrayComponentGroupIMG(sceneArray, 0, monitorComponentGroup);
glBindSceneArrayComponentGroupIMG(sceneArray, 1, cameraViewComponentGroup);

多类场景需要多个场景阵列,例如在光线追踪游戏视觉层次的同时又想沿着该层次光线追踪声音的路径。相比视觉信息,声音场景需要在光线和几何图形中存储不同的信息。我们可以在不同的场景阵列中单独区分这些细节:
glBindSceneArrayIMG(sceneArrayVisuals);
glDispatchRaysIMG(0, 0, 0, frame.w, frame.h, GL_NO_WAIT_BIT_IMG);
glBindSceneArrayIMG(sceneArrayAudio);
glDispatchRaysIMG(0, 0, 0, 1, numSounds * 2, GL_NO_WAIT_BIT_IMG);

或者可以使用单个场景阵列和多类光线。

我们将这个绑定的场景阵列也称之为场景,这样在本文中涉及到场景时才说的通。

无绑定

在常规OpenGL中使用纹理时,我们需要首先调用glActiveTexture,然后是glBindTexture,且只有一组有限的绑定点。这在单一的对象层比较适用,因为对于下一个对象,我们可以再次调用这些函数且改变对象的纹理——不需要担心其他对象的纹理。

启动光线追踪时,我们需要知道所有对象的纹理和着色器。在光线遍历时我们无法变更绑定,但又不想仅限于少量的纹理。因此需要另一种机制将这些纹理绑定到对象着色器中。这就是新的IMG_bindless_texture扩展。可以使用以下函数替代上述的gl调用来绑定纹理:

GLuint64 textureHandle = glGetTextureHandleIMG(textureObject);
// Similar to glUniform
glProgramUniformHandleui64IMG(rayProgramHandle, samplerLocationInShader, textureHandle);

// Now in glsl we can access the texture:
layout(bindless_sampler) uniform sampler2D sTexture;

光线遍历

2

光线追踪流程

通过glDispatchRaysIMG调用进入光线遍历部分。我们需要定义一组glRayBounceLimitIMG光线反射数量的最大值。还需要其他一些有助于提高硬件执行效率的辅助工具。glProgramMaxRayEmitsIMG限制了发送的光线数量。对于阴影光线,将此值设置为1,例如灯光照射得到的硬阴影。这将有助于硬件了解是否在执行光线着色器,且最多可以发送一条阴影光线。

未完待续

声明:
本文为原创文章,转载需注明作者、出处及原文链接,否则,本网站将保留追究其法律责任的权利

--电子创新网--
粤ICP备12070055号