Skip to content

Instantly share code, notes, and snippets.

@Vassalware
Last active October 31, 2023 06:07
Show Gist options
  • Save Vassalware/d47ff5e60580caf2cbbf0f31aa20af5d to your computer and use it in GitHub Desktop.
Save Vassalware/d47ff5e60580caf2cbbf0f31aa20af5d to your computer and use it in GitHub Desktop.

Guide to using OpenGL's GL.DebugMessageCallback()

When learning OpenGL, one of the most common sources of bugs you'll encounter is from performing OpenGL API calls with invalid arguments, performing them in the wrong order, or performing calls that assume some OpenGL state that differs from the actual state.

With most well-written C# APIs, you'd get an exception thrown the moment you do something you shouldn't. By default, this doesn't happen with OpenGL, but there are still ways to tell if there are errors:

The easy (but tedious) way

The common way to check for errors is to use GL.GetError() to check for errors, often with a helper method such as the following:

/// <summary>
/// Checks to see if the last OpenGL API call resulted in an error,
/// and throws an <see cref="Exception"/> if an error was found.
/// </summary>
[Conditional("DEBUG")]
[DebuggerStepThrough]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void CheckLastError()
{
    ErrorCode errorCode = GL.GetError();

    if (errorCode != ErrorCode.NoError)
    {
        throw new Exception(errorCode.ToString());
    }
}

While this may be the simplest way to tell if there are OpenGL errors, you'd need to call this method after every OpenGL api call to get an accurate understanding of which OpenGL API call caused the error. If you call it after five API calls in a row, you wouldn't be able to tell which one caused the error. While calling this method often won't have much of an effect on performance (no noticable change in framerate for me, at least), and none at all in Release mode (because of the Conditional attribute), it's tedious and easy to forget (not to mention the time you might sink into debugging the wrong call if you forget to put it after every one.)

These problems are solved in OpenGL 4.3 (or earlier versions with the KHR_Debug extension) using the GL.DebugMessageCallback function.

The better way

This function allows us to write a custom method that will be called with "messages" from OpenGL. Not only will it be called to notify us of any errors, but it can also have useful debug messages with notifications about possible performance issues, or when a specified UsageHint doesn't properly match up with how the object is being used, and much more. In addition to this, we can set it to be called synchronously so that we can set a breakpoint (or throw an exception, as seen below) and use the debugger to follow the callstack from our method, over OpenGL, and back up to our code to see which of our OpenGL API calls are spawning these messages.

Let's start by creating the method that we'll tell OpenGL to use as the debug message callback:

private static void DebugCallback(DebugSource source,
                                  DebugType type,
                                  int id,
                                  DebugSeverity severity,
                                  int length,
                                  IntPtr message,
                                  IntPtr userParam)
{
    string messageString = Marshal.PtrToStringAnsi(message, length);

    Console.WriteLine($"{severity} {type} | {messageString}");

    if (type == DebugType.DebugTypeError)
    {
        throw new Exception(messageString);
    }
}

Within this method, we create a C# string from the pointer and length passed to us and write it out to the console, as well as throw an exception if the message is telling us about an error. When that happens, you can follow the callstack up to the API call in your code that caused the error. Depending on your environment, you may also be able to decorate this method with the [DebuggerStepThrough] attribute and get your IDE to break on the API call in the first place, making for a much better debugging workflow.

We'll use GL.DebugMessageCallback to tell OpenGL about our method, but we can't just pass the method group itself. We'll store the method as a delegate in a static field like so:

private static DebugProc _debugProcCallback = DebugCallback;

Because we're going to be sending a pointer to managed memory to OpenGL (an unmanaged library) we're going to want to pin the delegate so the GC can't move it around. We'll create a static variable to keep the handle around:

private static GCHandle _debugProcCallbackHandle;

Then, we'll pin the delegate and save the handle, as well as tell OpenGL about our method, and enable debug output.

_debugProcCallbackHandle = GCHandle.Alloc(_debugProcCallback);

GL.DebugMessageCallback(_debugProcCallback, IntPtr.Zero);
GL.Enable(EnableCap.DebugOutput);
GL.Enable(EnableCap.DebugOutputSynchronous);

And that's it! Here's an example of some of the messages I got after adding this:

Notif Other | Buffer detailed info: Buffer object 1 (bound to NONE, usage hint is GL_STATIC_DRAW) will use VIDEO memory as the source for buffer object operations.
Notif Other | Buffer detailed info: Buffer object 2 (bound to NONE, usage hint is GL_STATIC_DRAW) will use VIDEO memory as the source for buffer object operations.
Medium Performance | Program/shader state performance warning: Vertex shader in program 5 is being recompiled based on GL state.

One thing you'll notice is that these object names, such as "Buffer object 1", aren't very helpful. We can fix this by setting object labels for our objects, using the GL.ObjectLabel method. Here's an example for a Buffer object.

string label = "ImGui VBO";
GL.ObjectLabel(ObjectLabelIdentifier.Buffer, Handle, label.Length, label);

Where Handle is the integer OpenGL "name" for the buffer object. Naturally, the first parameter of this method depends on the type of the OpenGL object you're setting the label for.

Now we get much better messages!

Notif Other | Buffer detailed info: Buffer object ImGui VBO (bound to NONE, usage hint is GL_DYNAMIC_DRAW) will use VIDEO memory as the source for buffer object operations.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment