Show / Hide Table of Contents

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.

  • OpenTK 3.0
  • OpenTK 4.0
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.
using OpenTK;
using OpenTK.Windowing.Desktop;

// ...

GameWindow window = new GameWindow(
    GameWindowSettings.Default,
    new NativeWindowSettings() {
        Flags = ContextFlags.Debug
    });

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.

  • Base
  • KHR_debug
  • ARB_debug_output
private static GLDebugProc DebugMessageDelegate = OnDebugMessage;
private static GLDebugProcKHR DebugMessageDelegate = OnDebugMessage;
private static GLDebugProcARB 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.

  • Base
  • KHR_debug
  • ARB_debug_output
GL.DebugMessageCallback(DebugMessageDelegate, IntPtr.Zero);
GL.Enable(EnableCap.DebugOutput);

// Optionally
GL.Enable(EnableCap.DebugOutputSynchronous)
GL.Khr.DebugMessageCallback(DebugMessageDelegate, IntPtr.Zero);
GL.Enable(EnableCap.DebugOutput);

// Optionally
GL.Enable(EnableCap.DebugOutputSynchronous)
GL.Arb.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
In this article
Back to top Generated by DocFX