原文:Easier OpenGL ES debugging on ARM Mali GPUs with GL_KHR_debug
投稿人:janharaldfredriksen,2013 年 6 月 27 日
出于多方面的的原因,调试 OpenGL ES 应用程序具有挑战性。原因之一是 API 中的错误报告不够好。引发错误的时间及其原因并不总是十分明显。这是 API 的局限性,因为在大多数情况下,驱动程序完全知道所存在的问题,但不存在将该信息返回到应用程序的标准机制。
Khronos 开发了称为 GL_KHR_debug 的扩展,其提供了让图形驱动程序将相关事件通知给应用程序的机制。这包括但不限于 API 错误。该扩展可同时用于桌面 OpenGL 和 OpenGL ES。其已广泛用于 OpenGL,我们预计其也将用于 OpenGL ES。
最近,我们向 Midgard 架构 GPU 的 OpenGL ES 驱动程序增加了对该扩展的支持。在本文中,我将介绍如何用该扩展来改善错误报告,并使其更易调试 OpenGL ES 应用程序。
管理状态
OpenGL ES 保有大量状态,而当前错误是这种状态的组成部分。这有两个后果:可能因状态组合而出现错误,且只可以记录一个错误。
如果仅因 API 调用中的无效参数而发生错误,则它们的原因可能相对明显。不过,由于状态组合而产生可能错误,结合两个合法的命令序列可能会导致原因往往不太明显的错误。
例如:
// This does not generate any errors.
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, NULL);
GLenum error = glGetError(); // GL_NO_ERROR
// This does not generate any errors either.
GLuint fbo;
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
// But repeating the first sequence now, does generate an error.
GLenum error = glGetError(); // GL_INVALID_FRAMEBUFFER_OPERATION
OpenGL ES 状态仅存储单个错误,指定为产生的第一个错误。后续错误会被检测到,但对错误状态没有影响且不会进行报告。没有有关错误产生时间的任何信息。这些限制相结合使得难以确定错误原因,因此需要较长时间才能发现和解决问题。
更为糟糕的是,OpenGL ES API 返回的错误非常广泛,并不总是指向明显的原因。在 OpenGL ES 3.0 中,可用错误代码是:
每个 API 调用可能触发这些错误中的一个或多个。单个 API 调用也可能因多种原因而触发相同的错误。
最极端的例子是 GL_OUT_OF_MEMORY 错误,其可由 任意 API 调用产生,虽然其名称暗示内存不足条件,但其有时会由驱动程序用来报告其他错误条件(如 GPU 超时)。
OpenGL ES 规范不记录哪些错误可以由哪个 API 调用所产生。相反,它将所有 API 调用的大多数错误保留为隐含状态,仅记录这些隐含错误的例外和附加内容。这使得非常难以确定为何错误由特定的 API 调用设置。
OpenGL ES 3.0 手册页面确实记录每个 API 调用的错误,但这些页面并非权威参考。最近重组的 OpenGL 4.3 规范已使这一情况更为清晰,但该重组尚未应用到 OpenGL ES 规范。
部分解决方案
上述问题的部分解决方案一般归结为每隔一段时间检查错误 - 至少在调试版本中。这可以是少到每帧一次,多到每个 API 调用一次,或介于两者之间。
例如,错误检查可以用作某个函数的前和后置条件:
#ifdef _DEBUG
#define CHECK_GL_ERROR() check_gl_error_and_print()
#else
#define CHECK_GL_ERROR()
#endif
void check_gl_error_and_print(void)
{
GLenum error = glGetError();
if (error != GL_NO_ERROR)
printf("GL error detected: 0x%04x\n", error);
}
void load_texture(GLsizei width, GLsizei height, GLvoid *data)
CHECK_GL_ERROR();
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
另一种替代方法是通过将它们包裹在宏中对每个 API 调用进行错误检查:
#define CALL_GL(a) a; check_gl_error_and_print()
#define CALL_GL(a) a;
CALL_GL(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, image->width, image->height, 0, GL_RGB, GL_UNSIGNED_BYTE, image->data));
CALL_GL(glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR));
CALL_GL(glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR));
CALL_GL(glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT));
CALL_GL(glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT));
有关哪个行和文件产生该错误的信息可以轻松添加到这些宏中。图形调试工具也拥有自动插入这些检查的选项。
但这些方法都不完美。
一方面,很少或经常检查错误之间存在权衡。如果很少检查错误,就需要进行更多的工作来跟踪其原因和位置。如果经常检查错误,则代码的可读性可能会变差,因为调试代码会随处可见。更重要的是,这些方法无法解决缺乏详细信息这一根本问题。它们最多提供有关错误产生时间的信息,但无法提供有关其产生原因的任何其他信息。为此,我们需要扩展 API 的错误处理能力,而这正是GL_KHR_debug 扩展的目的。
通过 GL_KHR_debug 获取更好的错误消息
GL_KHR_debug 扩展通过允许检测到的所有错误报告携带详细的错误消息解决了上述问题。默认情况下,调试通知存储在应用程序可以查询的驱动程序内的日志里。但是,应用程序也可以注册驱动程序在具有新通知时随时调用的回调函数。这是使用该扩展的最简单方式,因此现在我们将重点讲述。
请思考以下示例:
glGetIntegerv(GL_CURRENT_PROGRAM, &prog);
pos = glGetAttribLocation(prog, "position");
glEnableVertexAttribArray(pos);
glVertexAttribPointer(pos, 3, GL_FLOAT, GL_FALSE, 0, triangle_corners);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, triangle_indices);
error = glGetError(); // GL_INVALID_OPERATION
在该列表中,尚不清楚产生 GL_INVALID_OPERATION 错误的原因和时间。通过GL_KHR_debug,该驱动程序向我们提供了以下信息:
Error:glGetAttribLocation::<program> could not be made part of current state. <program> is not linked
Error:glEnableVertexAttribArray::<index> is greater than or equal to GL_MAX_VERTEX_ATTRIBS
Error:glVertexAttribPointer::<index> is greater than or equal to GL_MAX_VERTEX_ATTRIBS
好多了!
如何使用 GL_KHR_debug
那么,我们如何启用该功能?正如我们将看到的,这相当简单。只需对我们的EGL 和 OpenGL ES 初始化代码进行一些更改即可。
调试信息只保证在所谓的调试上下文中提供,因此,我们需要创建其中之一。我们可以使用 EGL_KHR_create_context 扩展做到这一点:
EGLint ctx_attribs[] = {
EGL_CONTEXT_CLIENT_VERSION, 2,
EGL_CONTEXT_FLAGS_KHR, EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR,
EGL_NONE};
EGLContext *ctx = eglCreateContext(dpy, config, EGL_NO_CONTEXT, ctx_attribs);
对正常上下文创建的唯一更改是增加 EGL_CONTEXT_FLAGS_KHR属性及其相关联参数。
接下来,我们需要定义供 OpenGL ES 调用的回调函数:
static void on_gl_error(enum source, enum type, uint id, enum severity,
sizei length, const char* message, void *userParam)
printf("%s\n", message);
我们需要略加注意我们在回调函数内所进行的操作,因为其从 OpenGL ES 驱动程序内进行调用。特别是,我们无法调用任何 OpenGL ES 函数,因为这可能会产生严重问题 - 包括应用程序崩溃。
最后,我们需要告诉 OpenGL ES 我们的回调函数:
static void enable_debug_callbacks(void)
glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS_KHR, GL_TRUE);
glDebugMessageCallbackKHR(on_gl_error, NULL);
}1
就是这样。现在,驱动程序将在其生成调试消息时调用回调函数。该驱动程序需要为其检测到的每个 API 错误生成调试消息。我们不再需要在我们的代码中的任何其他地方检查错误!此外,通过在回调函数中设置断点,很容易找出不正确命令的调用来源。
注:当然,正如任何 OpenGL ES 扩展一样,您仅应在检查该扩展受支持并通过 eglGetProcAddress 检索扩展函数指针后进行该操作。对于 GL_KHR_debug 扩展特别需要注意的是将“KHR”后缀添加到所有新函数名称中的 OpenGL ES 版规范的后期更改。例如,“glDebugMessageCallback”更改为“glDebugMessageCallbackKHR”。由于这是非常迟的更改,因此某些实施可能仍使用无后缀版。为避免可移植性问题,应用程序应该同时查询有后缀和无后缀函数指针,以确定哪一个存在于任何给定实施上。
您可能想知道有关上个示例中对 glEnable 的额外调用的问题。这是必需的,因为某些 OpenGL ES 实施可能为内部多线程形式,因此可以同时检测和报告多个错误。通过启用同步输出,我们保证同一时间回调函数仅从一个线程进行调用。此外,我们还保证在返回 API 函数之前调用该回调函数。如果我们不启用该行为,则我们将必须确保我们的回调函数是线程安全的。
总结
在本文中,我重点介绍了改进的错误消息,但这并非GL_KHR_debug 扩展启用的唯一功能。
尤其是,它还使驱动程序能够生成与性能、弃用的和未定义的行为以及可移植性相关的调试警告。这些警告由与错误消息相同的机制进行报告。启用它们无需进一步的应用程序操作。
该扩展的所有实施都需要在发生错误时生成调试消息,但警告没有具体要求。警告产生时间完全由实施定义。目前,Midgard OpenGL ES 驱动程序不会报告任何警告,但我们预计会在未来进行添加。
该扩展的其他高级功能包括过滤消息、手动检索消息和标识对象。
我希望本文已激起您对该扩展的兴趣,它可以让您的应用程序开发工作更容易一些。喜欢它吗?请告诉我们。
ARM 媒体处理业务部软件架构团队主管 Jan-Harald Fredriksen 在完成他的研究时,Jan-Harald 听到一个称为 Falanx 的小公司正在开发名为 Mali 的 GPU 的传言。赢得由该公司安排的演示竞争后,他设法获聘,并于几年后 Falanx 被收购时加入 ARM。从那时起,他一直从事 Mali 驱动程序堆栈的大部分工作,主要集中在 OpenGL ES。目前,他的职责是领导 SW 架构团队并在 Khronos OpenGL ES 工作组中代表 ARM。