简介
OpenGL* 提供了两种存储和检索数据的机制,作为着色器的输入和输出。 游戏开发人员可以选择着色器存储缓冲区对象 (SSBO) 或原子计数缓冲区(ACB)。 本文演示了编写图形密集型游戏时,使用 ACB 替代 SSBO 没有实际的性能优势。
本文随附了一个可在 SSBO 和 ACB 之间交替的简单 C++ 应用。 游戏开发人员可以看到两种方法对渲染性能的效果(毫秒/帧)。 尽管本文适用于图形游戏开发人员,但这些概念适用于使用 OpenGL 4.3 及更高版本的所有应用。 示例代码用 C ++ 编写,专为 Windows 8.1 和 10 设备而设计。
要求
构建和运行示例应用需要具备以下条件:
- 采用第六代智能英特尔®酷睿™ 处理器(代号为 Skylake)的计算机
- OpenGL 4.3 或更高版本
- Microsoft Visual Studio* 2013 或更高版本
原子计数器缓冲区与着色器存储缓冲区对象
SSBO 是一个用于在 OpenGL 中存储和检索数据的缓冲区对象,为着色器的输入和输出提供了一个通用机制。 另一个存储选项是 ACB,支持原子内存操作的 OpenGL 存储机制。
相比 SSBO,ACB 在使用上更有优势。如果您符合其限制,如 OpenGL foundation 的网站所示:https://www.opengl.org/wiki/Atomic_Counter,我们一般推荐使用 ACB 而不是 SSBO。
- 原子计数器只能是无符号整数
- 它们只能递增或递减 1
- 原子计数器内存访问不一致,也就是说,它不使用普通的 OpenGL 内存模型
从性能角度而言,ACB 相比 SSBO 没有优势。 这是因为 ACB 在内部实现为 SSBO 原子操作,因此利用 ACB 没有实际的性能优势。
本文随附的应用通过在 SSBO 和 ACB 之间交替来说明这一点,同时显示当前的每帧毫秒数和每秒帧数。 按空格键可在 SSBO 和 ACB 之间切换。 出现这种情况时,图像将呈现动画显示效果,以表明发生了变化。
Skylake 处理器图形
第六代智能英特尔酷睿处理器也称为 Skylake,提供卓越的二维和三维图形性能,最高可达 1152 GFLOPS。 其多核架构可提高性能并增加每个时钟周期的指令数。
与前几代相比,第六代智能英特尔酷睿处理器具有许多新的优势,可显著提升整体计算性能和视觉性能。 示例增强功能包括一个 GPU,加上 CPU 的增强计算能力,可提供比之前的英特尔® 处理器显卡高出 40% 的图形性能。 第六代智能英特尔酷睿处理器经过重新设计,可提供更高保真度的视频输出,更高分辨率的视频播放,以及对更低功耗系统的更加无缝的响应能力。 Skylake 支持 4K 视频播放和扩展超频,是游戏开发商的理想之选。
GPU 内存访问包含原子最小值、最大值以及共享本地内存或全局内存中 32 位浮点值的比较和交换。 这一新架构还为到同一地址的紧接原子提供了性能改进。 平铺资源包括对部分驻留(稀疏)的大型纹理和缓冲区的支持。 读取未映射磁贴会返回零,并且对其的写入操作将被丢弃。 此外还有用于固定 LOD 和获取操作状态的新着色器指令。 现在还支持较大的纹理和缓冲区。 (例如,您最大可使用 128k x 128k x 8B 的细化 2D 纹理。)
由图形 API 提供支持时,无边界资源可将一个着色器可使用的动态资源数量从大约 256 个增加至 2,000,000 个。 这一改变可降低与更新绑定表相关的开销,为程序员提供更高的灵活性。
执行单元也改进了本地 16 位浮点支持。 使用半精度时,这种增强的浮点支持可带来功耗和性能方面的双重优势。
显示功能进一步提供了多平面重叠选项,拥有缩放、转换、颜色校正和显示时组合多个曲面的硬件支持。 曲面可来自使用不同更新频率和分辨率的单独交换链(例如,在放大、较低分辨率的帧渲染基础上组合的全分辨率 GUI 元素),从而提供显著增强。
其架构支持最多三个切片的 GPU(提供 72 个 EU)。 这一架构还提供了更高的功率门控和时钟域灵活性,这是值得利用的。
构建和运行应用
按照下面的步骤编译和运行示例应用。
- 下载包含示例应用源代码的 ZIP 文件,将其解压到工作目录下。
- 双击打开lesson4_ACBvsSSBO/lesson4.sln文件,启动 Microsoft Visual Studio 2013。
- 选择 <Build>/<Build Solution>,构建应用。
- 构建成功后,您可以在 Visual Studio 中运行示例。
应用运行时,主窗口将打开,您将看到一个使用 SSBO 渲染的图像,以及 Microsoft Visual Studio 2013 控制台窗口中显示的性能测量结果。 按空格键在 SSBO 和 ACB 之间交替,查看性能差异。 切换模式时,图像将呈现动画显示效果以直观地反映变化。 按 ESC 退出应用。
代码亮点
这一示例针对 ACB 和 SSBO 使用单独的着色器;其定义如下。 动画使用一个单独的着色器,以表示应用已在 ACB 和 SSBO 之间切换。
// Fragment shader used for animation gets output color from a texture static std::string aniFragmentShader = "#version 430 core\n""\n""uniform sampler2D texUnit;\n""\n""smooth in vec2 texcoord;\n""\n""layout(location = 0) out vec4 fragColor;\n""\n""void main()\n""{\n"" fragColor = texture(texUnit, texcoord);\n""}\n" ; // Fragment shader used bor ACB gets output color from a texture static std::string acbFragmentShader = "#version 430 core\n""\n""uniform sampler2D texUnit;\n""\n""layout(binding = 0) uniform atomic_uint acb[" s(nCounters) "];\n""\n""smooth in vec2 texcoord;\n""\n""layout(location = 0) out vec4 fragColor;\n""\n""void main()\n""{\n"" for (int i=0; i<" s(nCounters) "; ++i) atomicCounterIncrement(acb[i]);\n"" fragColor = texture(texUnit, texcoord);\n""}\n" ; // Fragment shader used for SSBO gets output color from a texture static std::string ssboFragmentShader = "#version 430 core\n""\n""uniform sampler2D texUnit;\n""\n""smooth in vec2 texcoord;\n""\n""layout(location = 0) out vec4 fragColor;\n""\n""layout(std430, binding = 0) buffer ssbo_data\n""{\n"" uint v[" s(nCounters) "];\n""};\n""\n""void main()\n""{\n"" for (int i=0; i<" s(nCounters) "; ++i) atomicAdd(v[i], 1);\n"" fragColor = texture(texUnit, texcoord);\n""}\n" ;
着色器进行了编译和准备,以便使用。
// compile and link the shaders into a program, make it active vShader = compileShader(vertexShader, GL_VERTEX_SHADER); anifShader = compileShader(aniFragmentShader, GL_FRAGMENT_SHADER); acbfShader = compileShader(acbFragmentShader, GL_FRAGMENT_SHADER); ssbofShader = compileShader(ssboFragmentShader, GL_FRAGMENT_SHADER); aniProgram = createProgram({ vShader, anifShader }); acbProgram = createProgram({ vShader, acbfShader }); ssboProgram = createProgram({ vShader, ssbofShader }); aniOffset = glGetUniformLocation(aniProgram, "offset"); GLCHK; aniTexUnit = glGetUniformLocation(aniProgram, "texUnit"); GLCHK; acbOffset = glGetUniformLocation(acbProgram, "offset"); GLCHK; acbTexUnit = glGetUniformLocation(acbProgram, "texUnit"); GLCHK; ssboOffset = glGetUniformLocation(ssboProgram, "offset"); GLCHK; ssboTexUnit = glGetUniformLocation(ssboProgram, "texUnit"); GLCHK;
ACB 和 SSBO 一样。
// create and configure the Atomic Counter Buffer glGenBuffers(1, &acb); GLCHK; glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, 0, acb); GLCHK; glBufferData(GL_ATOMIC_COUNTER_BUFFER, nCounters * 4, NULL, GL_STATIC_COPY); GLCHK; glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, 0, acb); GLCHK; // create and configure the Shader Storage Buffer Object glGenBuffers(1,&ssbo); GLCHK; glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, ssbo); GLCHK; glBufferData(GL_SHADER_STORAGE_BUFFER, nCounters * 4, NULL, GL_STATIC_COPY); GLCHK; glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, ssbo); GLCHK; GLuint idx = glGetProgramResourceIndex(ssboProgram, GL_SHADER_STORAGE_BLOCK, "ssbo_data"); GLCHK; glShaderStorageBlockBinding(ssboProgram, idx, 0); GLCHK; // configure texture unit glActiveTexture(GL_TEXTURE0); GLCHK; glUseProgram(aniProgram); GLCHK; glUniform1i(aniTexUnit, 0); GLCHK; glUseProgram(acbProgram); GLCHK; glUniform1i(acbTexUnit, 0); GLCHK; glUseProgram(ssboProgram); GLCHK; glUniform1i(ssboTexUnit, 0); GLCHK;
最后,准备纹理。
// create and configure the textures glGenTextures(1, &texture); GLCHK; glBindTexture(GL_TEXTURE_2D, texture); GLCHK; glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); GLCHK; glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); GLCHK; glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); GLCHK; glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); GLCHK; // load texture image std::vector<GLubyte> img; unsigned w, h; if (lodepng::decode(img, w, h, "sample.png")) __debugbreak(); // upload the non-pow2 image to vram glBindTexture(GL_TEXTURE_2D, texture); GLCHK; glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, &img[0]); GLCHK; }
绘制图像时,选择正确的着色器程序:
// GLUT display function. Draw one frame's worth of imagery. void display() { // attributeless rendering glClear(GL_COLOR_BUFFER_BIT); GLCHK; glBindTexture(GL_TEXTURE_2D, texture); GLCHK; if (animating) { glUseProgram(aniProgram); GLCHK; glUniform1f(aniOffset, animation); GLCHK; } else if (!selector) { glUseProgram(acbProgram); GLCHK; glUniform1f(acbOffset, 0.f); GLCHK; } else { glUseProgram(ssboProgram); GLCHK; glUniform1f(ssboOffset, 0.f); GLCHK; } glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); GLCHK; if (!animating && selector) { glMemoryBarrier(GL_ALL_BARRIER_BITS); GLCHK; } glutSwapBuffers(); }
每次绘制视频帧时,控制台中将更新性能输出,应用检查空格键或 ECS 是否已按下。 按空格键会让应用移动到阵列中的下一组组合;按 ESC 即可退出应用。 在 ACB 和 SSBO 之间交替时,性能测量结果将重置,图像将呈现动画显示效果,以表示发生了变化。 如果没有按下任何键,则渲染下一帧。
// GLUT idle function. Called once per video frame. Calculate and print timing reports and handle console input. void idle() { // Calculate performance static unsigned __int64 skip; if (++skip < 512) return; static unsigned __int64 start; if (!start && !QueryPerformanceCounter((PLARGE_INTEGER)&start)) __debugbreak(); unsigned __int64 now; if (!QueryPerformanceCounter((PLARGE_INTEGER)&now)) __debugbreak(); unsigned __int64 us = elapsedUS(now, start), sec = us / 1000000; static unsigned __int64 animationStart; static unsigned __int64 cnt; ++cnt; // We're either animating if (animating) { float sec = elapsedUS(now, animationStart) / 1000000.f; if (sec < 1.f) { animation = (sec < 0.5f ? sec : 1.f - sec) / 0.5f; } else { animating = false; selector ^= 1; skip = 0; cnt = start = 0; print(); } } // Or measuring else if (sec >= 2) { printf("frames rendered = %I64u, uS = %I64u, fps = %f, milliseconds-per-frame = %f\n", cnt, us, cnt * 1000000. / us, us / (cnt * 1000.)); if (swap) { animating = true; animationStart = now; swap = false; } else { cnt = start = 0; } } // Get input from the console too. HANDLE h = GetStdHandle(STD_INPUT_HANDLE); INPUT_RECORD r[128]; DWORD n; if (PeekConsoleInput(h, r, 128, &n) && n) if (ReadConsoleInput(h, r, n, &n)) for (DWORD i = 0; i < n; ++i) if (r[i].EventType == KEY_EVENT && r[i].Event.KeyEvent.bKeyDown) keyboard(r[i].Event.KeyEvent.uChar.AsciiChar, 0, 0); // Ask for another frame glutPostRedisplay(); }
结论
出于各种原因,OpenGL foundation 推荐使用 ACB 替代 SSBO,但这样做并不是为了提高性能。 这是因为 ACB 在内部实现为 SSBO 原子操作;因此,利用 ACB 没有实际的性能优势。 决策通常取决于您是否能够承受 ACB 的局限性。
通过将这种技术与第六代智能英特尔酷睿处理器的优势相结合,图形游戏开发商可以确保其游戏按照设计的方式运行。
下载代码示例
下面是 Github 上的代码示例的链接
https://github.com/IntelSoftware/OpenGLBestPracticesfor6thGenIntelProcessor
参考资料
第六代智能英特尔® 酷睿™ 处理器(代号为 Skylake)概述
第六代智能英特尔® 酷睿™ 处理器图形 API 开发人员指南
关于作者
Praveen Kundurthy 任职于英特尔® 软件和服务事业部。 他拥有计算机工程硕士学位。 他主要专注于移动技术、Microsoft Windows* 和游戏开发领域。