Debug Callback
OpenGL didn't have a comprehensive error notification system at its inception.
The programmer was limited to querying glError()
after commands, and the
information provided in the return value was nothing more than the code for the
error. However since then, OpenGL has introduced the ability for programmers to
register a callback function which delivers crucial debugging messages like
errors and warnings to your code.
The history of the debug callback dates to OpenGL 3.0, where AMD created the
vendor specific extension AMD_debug_output
providing this functionality.
This extensions was later approved by the OpenGL Architecture Review Board (ARB)
as the extension ARB_debug_output
in 2010. Finally, since OpenGL
4.3, the extension is a core part of the API, as well as the extension
KHR_debug
. This article covers usage of
both ARB_debug_output
extension and the 4.3+ API. KHR_debug
however covers
so much more than what was introduced in the original extension.
Here is an example of the type of messages you will receive from OpenGL when you have a debugging callback setup after some custom formatting.
[ERR 2023-02-16T16:37:18] (GL) DebugSourceApi/DebugTypeError (1281): GL_INVALID_VALUE error generated. ObjectLabel: unknown vertex array object <name>
[WRN 2023-02-16T16:37:18] (GL) DebugSourceApi/DebugTypePerformance (131218): Program/shader state performance warning: Vertex shader in program 1 is being recompiled based on GL state.
First and foremost, you should create your OpenGL context with the Debug flag set. Without the debug flag OpenGL will not generate debugging messages nor send them to your callback function.
using OpenTK;
using OpenTK.Graphics;
// ...
GameWindow window = new GameWindow(
width, height, // Window width.
GraphicsMode.Default, // Context graphics mode.
title, // Window title.
GameWindowFlags.Default, // GameWindow flags.
DisplayDevice.Default, // The display to create the window in.
3, 3, // OpenGL context version major then minor.
GraphicsContextFlags.Debug); // OpenGL context flags.
Then you should create a function which will execute when OpenGL has debugging information to relay to your program.
using System;
using System.Runtime.InteropServices;
using OpenTK.Graphics.OpenGL4; // Or alternatively OpenTK.Graphics.OpenGL
// ...
private static void OnDebugMessage(
DebugSource source, // Source of the debugging message.
DebugType type, // Type of the debugging message.
int id, // ID associated with the message.
DebugSeverity severity, // Severity of the message.
int length, // Length of the string in pMessage.
IntPtr pMessage, // Pointer to message string.
IntPtr pUserParam) // The pointer you gave to OpenGL, explained later.
{
// In order to access the string pointed to by pMessage, you can use Marshal
// class to copy its contents to a C# string without unsafe code. You can
// also use the new function Marshal.PtrToStringUTF8 since .NET Core 1.1.
string message = Marshal.PtrToStringAnsi(pMessage, length);
// The rest of the function is up to you to implement, however a debug output
// is always useful.
Console.WriteLine("[{0} source={1} type={2} id={3}] {4}", severity, source, type, id, message);
// Potentially, you may want to throw from the function for certain severity
// messages.
if (type == DebugType.DebugTypeError)
{
throw new Exception(message);
}
}
Note
Neither the access modifier or whether the method is an instance method or a static method matters.
Warning
This function is called from native code, which means there are stack frames associated with them. If your debugger does not support mixed-mode debugging it may affect your user experience if an exception is thrown or occurs within the callback.
Then you should create a delegate which encapsulates the function you just implmented.
private static GLDebugProc DebugMessageDelegate = OnDebugMessage;
Note
Creating this reference is critical. A delegate is managed by the .NET garbage collector, however, this delegate will be passed to a native function as a pointer. The garbage collector cannot track outside references, and if there are no references to the delegate, the pointer passed to the native function becomes "dangling" (in simpler terms, invalid). This causes an access violation whenever OpenGL attempts to call you back.
And as before, access modifiers and instance/static do not matter as long as it suits your code.
Finally you can provide OpenGL your delegate as your debug callback. You can now enable debug output, and optionally enable synchronous output.
GL.DebugMessageCallback(DebugMessageDelegate, IntPtr.Zero);
GL.Enable(EnableCap.DebugOutput);
// Optionally
GL.Enable(EnableCap.DebugOutputSynchronous)
Tip
Using synchronous output may decrease your performance significantly as OpenGL cannot defer command execution to other threads and forces validation of parameters immediately. However, this will allow you to easily break in the callback function to analyze the situtaion, such as viewing the stack trace and finding the culprit code. Otherwise the graphics driver is allowed to call the function from any thread concurrently. Be careful of the usual pitfalls of multithreading when disabled.
Note
The second paramter of DebugMessageCallback*
determines the value of the
pUserParam
parameter in the callback. This parameter is designed for C users
which has no concept delegates, but only function pointers. If you are very
interested in using this parameter (instead of capturing objects via the
delegate as recommended) you can use any C# pointer, or a pointer to a C#
GCHandle.
Common pointer gotchas apply.
References
- This page is based on the gist of Vassalware on the topic.
- AMD_debug_output
- ARB_debug_output
- KHR_debug
- System.Runtime.InteropServices.Marshal
- System.Runtime.InteropServices.GCHandle