1-输出调试信息

在编写OpenGL程序时,难免会出错。一些比较明显的错误(如写错函数名等)是容易被发现并纠错的,但还有一些不明显的错误(写错参数等)不容易发现,我们只能看错误的输出结果干瞪眼。还好,OpenGL为我们提供了一些输出调试信息的途径,如glGetError()glDebugMessageCallback()等。

接下来简单看看如何应用它们。

输出调试信息

glGetError()

static void GLClearError()
{
	while (glGetError() != GL_NO_ERROR);
}

static bool GLLogCall(const char* functionName, const char* filename, int line)
{
	while (GLenum error = glGetError())
	{
		std::cerr << std::format("[OpenGL ERROR]:code {:#x} at {}, in {}({}).\n", error, functionName, filename, line);
		return false;
	}
	return true;
}

// MSVC编译器触发中断
#define GL_ASSERT(x) if (!(x)) __debugbreak()
#define GLCall(x) GLClearError();\
	x;\
	GL_ASSERT(GLLogCall(#x, __FILE__, __LINE__))
// 这里#x就是把传进来的x看成字符串,方便获取函数名

如上述代码所示,通过调用GLCall(x)宏进行调试信息的输出。首先用GLClearError()清空OpenGL累积的错误,然后尝试执行OpenGL函数x,最后检查x是否出错,出错就触发断点并输出错误信息。

glGetError()有良好的兼容性,但这样得到的错误状态仍不明确:

  • 它向OpenGL询问错误状态,而这个错误的来源具体是哪个函数引发的不清楚。
  • 返回的是错误码,需要开发者自己寻找出错的原因。

glDebugMessageCallback()

在OpenGL4.3中添加,它允许我们定义错误产生时进行处理的回调函数,处理起问题更加详细灵活。在使用它之前,得先在OpenGL上下文中配置调试环境:

glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GL_TRUE);

要想检查该OpenGL上下文是否启用DEBUG,可以使用以下代码检查:

int flags;
glGetIntegerv(GL_CONTEXT_FLAGS, &flags);
if (flags & GL_CONTEXT_FLAG_DEBUG_BIT)
{
    // 成功启用DEBUG,配置DebugMessage相关内容...
}

在配置相关内容前,得先自定义回调函数,它的原型如下:

typedef void (APIENTRY *DEBUGPROC)(GLenum source,
            GLenum type,
            GLuint id,
            GLenum severity,
            GLsizei length,
            const GLchar *message,
            const void *userParam);

其中,调试信息的来源source如下表:

source解释
GL_DEBUG_SOURCE_APIOpenGL API
GL_DEBUG_SOURCE_WINDOW_SYSTEM窗口系统
GL_DEBUG_SOURCE_SHADER_COMPILER着色器的编译器
GL_DEBUG_SOURCE_THIRD_PARTY第三方库
GL_DEBUG_SOURCE_APPLICATION应用程序
GL_DEBUG_SOURCE_OTHER其他

调试信息的类型type如下表:

type解释
GL_DEBUG_TYPE_ERROR错误(glError)
GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR尝试使用已弃用功能
GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR未定义行为
GL_DEBUG_TYPE_PORTABILITY可移植性问题
GL_DEBUG_TYPE_PERFORMANCE性能问题
GL_DEBUG_TYPE_MARKER注解
GL_DEBUG_TYPE_PUSH_GROUP调用了glPushDebugGroup()
GL_DEBUG_TYPE_POP_GROUP调用了glPopDebugGroup()
GL_DEBUG_TYPE_OTHER其他行为

调试信息的消息等级severity如下表:

severity描述
GL_DEBUG_SEVERITY_HIGH致命错误,如OpenGL错误,着色器编译失败等
GL_DEBUG_SEVERITY_MEDIUM警告,如可移植性能警告等
GL_DEBUG_SEVERITY_LOW一般消息,如冗余的状态切换
GL_DEBUG_SEVERITY_NOTIFICATION最普通的通知消息

另外两项是调试信息和用户自定义参数。

下例是自定义函数的一个可能实现:

// glDebug.h
#ifdef NEW_GL_DEBUG
#define GLCall(x) GLLogCall(#x, __FILE__, __LINE__);\
	x;\
	glPopDebugGroup()

void GLLogCall(const char* funcName, const char* filename, int line);

// 回调函数
void GLAPIENTRY GLDebugCallback(unsigned source,
								unsigned type,
								unsigned id,
								unsigned severity,
								int length,
								const char* message,
								const void* userParam);
#endif
// glDebug.cpp
#ifdef NEW_GL_DEBUG
void GLLogCall(const char* funcName, const char* filename, int line)
{
	std::string simpleFilename(filename);
	simpleFilename = simpleFilename.substr(simpleFilename.find_last_of('\\') + 1);
	const std::string log = std::format("{}({}): {}", simpleFilename, line, funcName);
	glPushDebugGroup(GL_DEBUG_SOURCE_APPLICATION, 1, log.size(), log.c_str());
}

struct debugMsg
{
	std::string msg;
	unsigned int id;
};

// 回调函数
void GLAPIENTRY GLDebugCallback(unsigned source,
								unsigned type,
								unsigned id,
								unsigned severity,
								int length,
								const char* message,
								const void* userParam)
{
	std::string sourceStr;
	switch (source)
	{
		case GL_DEBUG_SOURCE_API:				sourceStr = "API"; break;
		case GL_DEBUG_SOURCE_WINDOW_SYSTEM:		sourceStr = "Window System"; break;
		case GL_DEBUG_SOURCE_SHADER_COMPILER:	sourceStr = "Shader Compiler"; break;
		case GL_DEBUG_SOURCE_THIRD_PARTY:		sourceStr = "Third Party"; break;
		case GL_DEBUG_SOURCE_APPLICATION:		sourceStr = "Application"; break;
		case GL_DEBUG_SOURCE_OTHER:				sourceStr = "Other"; break;
		default:								sourceStr = "Unknown"; break;
	}

	std::string typeStr;
	switch (type)
	{
		case GL_DEBUG_TYPE_ERROR:				typeStr = "Error"; break;
		case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR:	typeStr = "Deprecated Behavior"; break;
		case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR:	typeStr = "Undefined Behavior"; break;
		case GL_DEBUG_TYPE_PORTABILITY:			typeStr = "Portability"; break;
		case GL_DEBUG_TYPE_PERFORMANCE:			typeStr = "Performance"; break;
		case GL_DEBUG_TYPE_MARKER:				typeStr = "Marker"; break;
		case GL_DEBUG_TYPE_PUSH_GROUP:			typeStr = "Push Group"; break;
		case GL_DEBUG_TYPE_POP_GROUP:			typeStr = "Pop Group"; break;
		case GL_DEBUG_TYPE_OTHER:				typeStr = "Other"; break;
		default:								typeStr = "Unknown"; break;
	}

	static std::vector<debugMsg> msgQueue;
	if (type == GL_DEBUG_TYPE_POP_GROUP)
	{
		if (!msgQueue.empty() && msgQueue.at(msgQueue.size() - 1).id != 1)
		{
			for (auto& debugMsg : msgQueue)
			{
				switch (severity)
				{
					case GL_DEBUG_SEVERITY_HIGH:			LOG_CRITICAL("{0}", debugMsg.msg); GL_ASSERT(false); break;
					case GL_DEBUG_SEVERITY_MEDIUM:			LOG_ERROR("{0}", debugMsg.msg); break;
					case GL_DEBUG_SEVERITY_LOW:				LOG_WARN("{0}", debugMsg.msg); break;
					case GL_DEBUG_SEVERITY_NOTIFICATION:
					default:								LOG_INFO("{0}", debugMsg.msg); break;
				}
			}
			msgQueue.clear();
		}
		else
		{
			msgQueue.clear();
		}
	}
	else
	{
		msgQueue.push_back({
			std::format("[GL {}] [{}({})] {}", sourceStr, typeStr, id, message),
			id
		});
	}
}
#endif

这里我为了准确定位调试信息,在GLLogCall()中将包含函数名、文件、行数等调试信息插入到GL_DEBUG_TYPE_PUSH_GROUP中。这样子得到调试信息的格式如下:

[OpenGL Application] [Push Group(1)] xxx.cpp(xx): glXXXX()
[OpenGL API] [Other(123456)] 可能出现的调试信息
[OpenGL Application] [Pop Group(1)] xxx.cpp(xx): glXXXX()

大多数情况下控制台只会输出Push/Pop信息,反而显得更繁琐,于是我在下面简单实现了只输出Push信息和调试信息的功能(就是msgQueue那几行),效果如下(这里改用spdlog库了,很好用!):

可能有更好的解决方案,欢迎讨论~

参考资料

  • S10.错误处理-OpenGL-The Cherno
  • docs.gl
  • OpenGL程序调试输出_opengl开发三维应用程序怎么输出-CSDN博客