If the parameters of a function call do not match the set of parameters allowed by OpenGL, or do not interact reasonably with state that is already set in the context, then an OpenGL Error will result. The errors are presented as an error code.
For most OpenGL errors, and for most OpenGL functions, a function that emits an error will have no effect. No OpenGL state will be changed, no rendering will be initiated. It will be as if the function had not been called. There are a few cases where this is not the case.
Catching errors (the hard way)
OpenGL errors are stored in a queue until the error is actually handled.[footnotes 1] Therefore, if you do not regularly test for errors, you will not know necessarily which function call elicited a particular error. As such, error testing should be done regularly if you need to know where an error came from.
To fetch the next error in the queue (and to remove it from the queue), call this function:
If the error queue is empty, it will return GL_NO_ERROR. Otherwise, it will return one of the error enumerators below and remove that error from the queue. So to fetch all of the errors currently in the queue, you would need to loop.
A simple loop to extract the current OpenGL errors:
GLenum err; while((err = glGetError()) != GL_NO_ERROR) { // Process/log the error. }
Note: No OpenGL function call is valid when an OpenGL Context is not made current. Depending on the platform, a call to glGetError without having a context made current may crash or return any value, including indefinitely returning a valid OpenGL error code. So if you see an infinite loop in such a function, this is likely the reason why.
Meaning of errors
The glGetError function returns one of the following error codes, or GL_NO_ERROR if no (more) errors are available. Each error code represents a category of user error.
- GL_INVALID_ENUM, 0x0500
- Given when an enumeration parameter is not a legal enumeration for that function. This is given only for local problems; if the spec allows the enumeration in certain circumstances, where other parameters or state dictate those circumstances, then GL_INVALID_OPERATION is the result instead.
- GL_INVALID_VALUE, 0x0501
- Given when a value parameter is not a legal value for that function. This is only given for local problems; if the spec allows the value in certain circumstances, where other parameters or state dictate those circumstances, then GL_INVALID_OPERATION is the result instead.
- GL_INVALID_OPERATION, 0x0502
- Given when the set of state for a command is not legal for the parameters given to that command. It is also given for commands where combinations of parameters define what the legal parameters are.
- GL_STACK_OVERFLOW, 0x0503
- Given when a stack pushing operation cannot be done because it would overflow the limit of that stack’s size.
- GL_STACK_UNDERFLOW, 0x0504
- Given when a stack popping operation cannot be done because the stack is already at its lowest point.
- GL_OUT_OF_MEMORY, 0x0505
- Given when performing an operation that can allocate memory, and the memory cannot be allocated. The results of OpenGL functions that return this error are undefined; it is allowable for partial execution of an operation to happen in this circumstance.
- GL_INVALID_FRAMEBUFFER_OPERATION, 0x0506
- Given when doing anything that would attempt to read from or write/render to a framebuffer that is not complete.
- GL_CONTEXT_LOST, 0x0507 (with OpenGL 4.5 or ARB_KHR_robustness)
- Given if the OpenGL context has been lost, due to a graphics card reset.
- GL_TABLE_TOO_LARGE1, 0x8031
- Part of the ARB_imaging extension.
1: These error codes are deprecated in 3.0 and removed in 3.1 core and above.
In the OpenGL Reference documentation, most errors are listed explicitly. However, GL_OUT_OF_MEMORY and GL_CONTEXT_LOST could be generated by virtually any OpenGL function. And they can be generated for reasons not directly associated with that particular function call.
Catching errors (the easy way)
The debug output feature provides a simple method for your application to be notified via an application-defined message callback function when an OpenGL error (or other interesting event) occurs within the driver. Simply enable debug output, register a callback, and wait for it to be called with a DEBUG_TYPE_ERROR message.
This method avoids the need to sprinkle expensive and code-obfuscating glGetError() calls around your application to catch and localize the causes of OpenGL errors (and the need to conditionally compile them into debug builds to avoid the performance hit in optimized builds). The feature can even ensure that message callback functions are invoked on the same thread and within the very same call stack as the GL call that triggered the GL error (or performance warning).
A simple example showing how to utilize debug message callbacks (e.g. for detecting OpenGL errors):
void GLAPIENTRY MessageCallback( GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* message, const void* userParam ) { fprintf( stderr, "GL CALLBACK: %s type = 0x%x, severity = 0x%x, message = %sn", ( type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : "" ), type, severity, message ); } // During init, enable debug output glEnable ( GL_DEBUG_OUTPUT ); glDebugMessageCallback( MessageCallback, 0 );
Side effects
Under most circumstances, a function that generates an error will exit without changing any OpenGL state or initiating any OpenGL operations. They will not modify any client memory passed into those functions by the user (ie: pointer arguments). Under such errors, return values are zero or something equally innocuous. However, there are certain circumstances were an error happens and OpenGL state is modified.
Whenever the GL_OUT_OF_MEMORY error is generated, the state of the OpenGL context and/or objects is undefined.
The GL_CONTEXT_LOST error is generated (which requires OpenGL 4.5 or ARB_KHR_robustness) by any commands after the OpenGL context was lost. Those commands have no side effects, with a few special-case exceptions for functions that can block the CPU.
The multi-bind functions functions have unusual error properties. Because they aggregate the ability to bind multiple objects at once, if the binding of one object fails with an error, the others will be bound as normal. Only the erronous objects will fail to bind. Note that there is no way to detect which objects failed to bind (other than querying the context for each binding point).
No error contexts
Core in version | 4.6 | |
---|---|---|
Core since version | 4.6 | |
ARB extension | KHR_no_error |
An OpenGL Context can be created which does not report OpenGL Errors. If the context bit GL_CONTEXT_FLAG_NO_ERROR_BIT is set to true, then the context will not report most errors. It will still report GL_OUT_OF_MEMORY_ERROR where appropriate, but this can be delayed from the point where the error actually happens. No other errors will be reported.
This also means that the implementation will not check for errors either. So if you provide incorrect parameters to a function that would have provoked an error, you will get undefined behavior instead. This includes the possibility of application termination.
Contexts cannot have the no error bit and the robustsness or debug bits.
Notes
- ↑ The specification «OpenGL 4.6 (Core Profile) — October 22, 2019», chapter «2.3.1 Errors» doesn’t talk about a error queue, it talks about several flag-code pairs.
Графическое программирование — не только источник веселья, но еще и фрустрации, когда что-либо не отображается так, как задумывалось, или вообще на экране ничего нет. Видя, что большая часть того, что мы делаем, связана с манипулированием пикселями, может быть трудно выяснить причину ошибки, когда что-то работает не так, как полагается. Отладка такого вида ошибок сложнее, чем отладка ошибок на CPU. У нас нет консоли, в которую мы могли бы вывести текст, мы не можем поставить точку останова в шейдере и мы не можем просто взять и проверить состояние программы на GPU.
В этом уроке мы познакомимся с некоторыми методами и приемами отладки вашей OpenGL-программы. Отладка в OpenGL не так сложна, и изучение некоторых приемов обязательно окупится.
Содержание
Часть 2. Базовое освещение
Часть 3. Загрузка 3D-моделей
Часть 4. Продвинутые возможности OpenGL
Часть 5. Продвинутое освещение
glGetError()
Когда вы некорректно используете OpenGL (к примеру, когда настраиваете буфер, забыв его связать (to bind)), OpenGL заметит и создаст один или несколько пользовательских флагов ошибок за кулисами. Мы можем эти ошибки отследить, вызывая функцию glGetError()
, которая просто проверяет выставленные флаги ошибок и возвращает значение ошибки, если случились ошибки.
GLenum glGetError();
Эта функция возвращает флаг ошибки или вообще никакую ошибку. Список возвращаемых значений:
Внутри документации к функциям OpenGL вы можете найти коды ошибок, которые генерируются функциями, некорректно используемыми. К примеру, если вы посмотрите на документацию к функции glBindTexture()
, то вы сможете найти коды ошибок, генерируемые этой функцией, в разделе «Ошибки» (Errors).
Когда флаг ошибки установлен, никаких других флагов ошибки сгенерировано не будет. Более того, когда glGetError
вызывается, функция стирает все флаги ошибок (или только один на распределенной системе, см. ниже). Это значит, что если вы вызываете glGetError
один раз после каждого кадра и получаете ошибку, это не значит, что это — единственная ошибка и еще вы не знаете, где произошла эта ошибка.
Заметьте, что когда OpenGL работает распределенно, как это часто бывает на системах с X11, другие ошибки могут генерироваться, пока у них различные коды. Вызов
glGetError
тогда просто сбрасывает только один из флагов кодов ошибки вместо всех. Из-за этого и рекомендуют вызывать эту функцию в цикле.
glBindTexture(GL_TEXTURE_2D, tex);
std::cout << glGetError() << std::endl; // вернет 0 (нет ошибки)
glTexImage2D(GL_TEXTURE_3D, 0, GL_RGB, 512, 512, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
std::cout << glGetError() << std::endl; // вернет 1280 (неверное перечисление)
glGenTextures(-5, textures);
std::cout << glGetError() << std::endl; // вернет 1281 (неверное значение
std::cout << glGetError() << std::endl; // вернет 0 (нет ошибки)
Отличительной особенностью glGetError
является то, что она позволяет относительно легко определить, где может быть любая ошибка, и проверить правильность использования OpenGL. Скажем, что у вас ничего не отрисовывается, и вы не знаете, в чем причина: неправильно установленный кадровый буфер? Забыл установить текстуру? Вызывая glGetError
везде, вы сможете быстро понять, где возникает первая ошибка.
По умолчанию, glGetError
сообщает только номер ошибки, который нелегко понять, пока вы не заучиваете номера кодов. Часто имеет смысл написать небольшую функцию, помогающую напечатать строку с ошибкой вместе с местом, откуда вызывается функция.
GLenum glCheckError_(const char *file, int line)
{
GLenum errorCode;
while ((errorCode = glGetError()) != GL_NO_ERROR)
{
std::string error;
switch (errorCode)
{
case GL_INVALID_ENUM: error = "INVALID_ENUM"; break;
case GL_INVALID_VALUE: error = "INVALID_VALUE"; break;
case GL_INVALID_OPERATION: error = "INVALID_OPERATION"; break;
case GL_STACK_OVERFLOW: error = "STACK_OVERFLOW"; break;
case GL_STACK_UNDERFLOW: error = "STACK_UNDERFLOW"; break;
case GL_OUT_OF_MEMORY: error = "OUT_OF_MEMORY"; break;
case GL_INVALID_FRAMEBUFFER_OPERATION: error = "INVALID_FRAMEBUFFER_OPERATION"; break;
}
std::cout << error << " | " << file << " (" << line << ")" << std::endl;
}
return errorCode;
}
#define glCheckError() glCheckError_(__FILE__, __LINE__)
Если вы решите сделать больше вызовов glCheckError
, будет полезно знать в каком месте произошла ошибка.
glBindBuffer(GL_VERTEX_ARRAY, vbo);
glCheckError();
Вывод:
Осталась одна важная вещь: в GLEW есть давний баг: glewInit()
всегда выставляет флаг GL_INVALID_ENUM
. Чтобы это исправить, просто вызывайте glGetError
после glewInit
чтобы сбросить флаг:
glewInit();
glGetError();
glGetError
не сильно помогает, поскольку возвращаемая информация относительно проста, но часто помогает отловить опечатки или отловить место возникновения ошибки. Это простой, но эффективный инструмент для отладки.
Отладочный вывод
Инструмент менее известный, но полезнее, чем glCheckError
— расширение OpenGL «debug output» (Отладочный вывод), вошедшее в OpenGL 4.3 Core Profile. С этим расширением OpenGL отошлет сообщение об ошибке пользователю с деталями ошибки. Это расширение не только выдает больше информации, но и позволяет отловить ошибки там, где они возникают, используя отладчик.
Отладочный вывод входит в OpenGL начиная с версии 4.3, что означает, что вы найдете эту функциональность на любой машине, поддерживающей OpenGL 4.3 и выше. Если такая версия недоступна, то можно проверить расширения
ARB_debug_output
иAMD_debug_output
. Также есть непроверенная информация о том, что отладочный вывод не поддерживается на OS X (автор оригинала и переводчик не тестировали, прошу сообщать автору оригинала или мне в личные сообщения через механизм исправления ошибок, если найдете подтверждение или опровержение данного факта; UPD: Jeka178RUS проверил этот факт: из коробки отладочный вывод не работает, через расширения он не проверял).
Чтобы начать использовать отладочный вывод, нам надо у OpenGL запросить отладочный контекст во время инициализационного процесса. Этот процесс отличается на разных оконных системах, но здесь мы обсудим только GLFW, но в конце статьи в разделе «Дополнительные материалы» вы можете найти информацию насчет других оконных систем.
Отладочный вывод в GLFW
Запросить отладочный контекст в GLFW на удивление просто: нужно всего лишь дать подсказку GLFW, что мы хотим контекст с поддержкой отладочного вывода. Нам надо сделать это до вызова glfwCreateWindow
:
glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GL_TRUE);
Как только мы проинициализировали GLFW, у нас должен появиться отладочный контекст, если мы используем OpenGL 4.3 или выше, иначе нам надо попытать удачу и надеяться на то, что система все еще может создать отладочный контекст. В случае неудачи нам надо запросить отладочный вывод через механизм расширений OpenGL.
Отладочный контекст OpenGL бывает медленнее, чем обычный, так что во время работ над оптимизациями или перед релизом следует убрать или закомментировать эту строчку.
Чтобы проверить результат инициализации отладочного контекста, достаточно выполнить следующий код:
GLint flags; glGetIntegerv(GL_CONTEXT_FLAGS, &flags);
if (flags & GL_CONTEXT_FLAG_DEBUG_BIT)
{
// успешно
}
else
{
// не получилось
}
Как работает отладочный вывод? Мы передаем callback-функцию обработчик сообщений в OpenGL (похоже на callback’и в GLFW) и в этой функции мы можем обрабатывать данные OpenGL как нам угодно, в нашем случае — отправка полезных сообщений об ошибках на консоль. Прототип этой функции:
void APIENTRY glDebugOutput(GLenum source, GLenum type, GLuint id, GLenum severity,
GLsizei length, const GLchar *message, void *userParam);
Заметьте, что на некоторых операционных системах тип последнего параметра может быть const void*
.
Учитывая большой набор данных, которыми мы располагаем, мы можем создать полезный инструмент печати ошибок, как показано ниже:
void APIENTRY glDebugOutput(GLenum source,
GLenum type,
GLuint id,
GLenum severity,
GLsizei length,
const GLchar *message,
void *userParam)
{
// ignore non-significant error/warning codes
if(id == 131169 || id == 131185 || id == 131218 || id == 131204) return;
std::cout << "---------------" << std::endl;
std::cout << "Debug message (" << id << "): " << message << std::endl;
switch (source)
{
case GL_DEBUG_SOURCE_API: std::cout << "Source: API"; break;
case GL_DEBUG_SOURCE_WINDOW_SYSTEM: std::cout << "Source: Window System"; break;
case GL_DEBUG_SOURCE_SHADER_COMPILER: std::cout << "Source: Shader Compiler"; break;
case GL_DEBUG_SOURCE_THIRD_PARTY: std::cout << "Source: Third Party"; break;
case GL_DEBUG_SOURCE_APPLICATION: std::cout << "Source: Application"; break;
case GL_DEBUG_SOURCE_OTHER: std::cout << "Source: Other"; break;
} std::cout << std::endl;
switch (type)
{
case GL_DEBUG_TYPE_ERROR: std::cout << "Type: Error"; break;
case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR: std::cout << "Type: Deprecated Behaviour"; break;
case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR: std::cout << "Type: Undefined Behaviour"; break;
case GL_DEBUG_TYPE_PORTABILITY: std::cout << "Type: Portability"; break;
case GL_DEBUG_TYPE_PERFORMANCE: std::cout << "Type: Performance"; break;
case GL_DEBUG_TYPE_MARKER: std::cout << "Type: Marker"; break;
case GL_DEBUG_TYPE_PUSH_GROUP: std::cout << "Type: Push Group"; break;
case GL_DEBUG_TYPE_POP_GROUP: std::cout << "Type: Pop Group"; break;
case GL_DEBUG_TYPE_OTHER: std::cout << "Type: Other"; break;
} std::cout << std::endl;
switch (severity)
{
case GL_DEBUG_SEVERITY_HIGH: std::cout << "Severity: high"; break;
case GL_DEBUG_SEVERITY_MEDIUM: std::cout << "Severity: medium"; break;
case GL_DEBUG_SEVERITY_LOW: std::cout << "Severity: low"; break;
case GL_DEBUG_SEVERITY_NOTIFICATION: std::cout << "Severity: notification"; break;
} std::cout << std::endl;
std::cout << std::endl;
}
Когда расширение определяет ошибку OpenGL, оно вызовет эту функцию и мы сможем печатать огромное количество информации об ошибке. Заметьте, мы проигнорировали некоторые ошибки, так как они бесполезны (к примеру, 131185 в драйверах NVidia говорит о том, что буфер успешно создан).
Теперь, когда у нас есть нужный callback, самое время инициализировать отладочный вывод:
if (flags & GL_CONTEXT_FLAG_DEBUG_BIT)
{
glEnable(GL_DEBUG_OUTPUT);
glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
glDebugMessageCallback(glDebugOutput, nullptr);
glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, nullptr, GL_TRUE);
}
Так мы сообщаем OpenGL, что хотим включить отладочный вывод. Вызов glEnable(GL_DEBUG_SYNCRHONOUS)
говорит OpenGL, что мы хотим сообщение об ошибке в тот момент, когда только она произошла.
Фильтрация отладочного вывода
С функцией glDebugMessageControl
вы можете выбрать типы ошибок, которые хотите получать. В нашем случае мы получаем все виды ошибок. Если бы мы хотели только ошибки OpenGL API, типа Error и уровня значимости High, мы бы написали следующий код:
glDebugMessageControl(GL_DEBUG_SOURCE_API,
GL_DEBUG_TYPE_ERROR,
GL_DEBUG_SEVERITY_HIGH,
0, nullptr, GL_TRUE);
С такой конфигурацией и отладочным контекстом каждая неверная команда OpenGL будет отправлять много полезной информации:
Находим источник ошибки через стек вызовов
Еще один трюк с отладочным выводом заключается в том, что вы можете относительно просто установить точное место возникновения ошибки в вашем коде. Устанавливая точку останова в функции DebugOutput
на нужном типе ошибки (или в начале функции если вы хотите отловить все ошибки) отладчик отловит ошибку и вы сможете переместиться по стеку вызовов, чтобы узнать, где произошла ошибка:
Это требует некоторого ручного вмешательства, но если вы примерно знаете, что ищете, невероятно полезно быстро определить, какой вызов вызывает ошибку.
Свои ошибки
Наряду с чтением ошибок, мы можем их посылать в систему отладочного вывода с помощью glDebugMessageInsert
:
glDebugMessageInsert(GL_DEBUG_SOURCE_APPLICATION, GL_DEBUG_TYPE_ERROR, 0,
GL_DEBUG_SEVERITY_MEDIUM, -1, "error message here");
Это очень полезно, если вы подключаетесь к другому приложению или к коду OpenGL, который использует отладочный контекст. Другие разработчики смогут быстро выяснить любую сообщенную ошибку, которая происходит в вашем пользовательском коде OpenGL.
В общем, отладочный вывод (если доступен) очень полезен для быстрого отлова ошибок и определенно стоит потраченных усилий на настройку, так как экономит значительное время разработки. Вы можете найти копию исходного кода здесь с использованием glGetError
и отладочного вывода. Есть ошибки, попробуйте их исправить.
Отладочный вывод шейдера
Когда дело доходит до GLSL, у нас нет доступа к функции типа glGetError
или возможности пройтись по коду по шагам в отладчике. Когда вы встречаетесь с черным экраном или совершенно неправильным отображением, бывает очень сложно понять, что происходит, если проблема в шейдере. Да, ошибки компиляции сообщают о синтаксических ошибках, но отлов семантических ошибок — та еще песня.
Один из часто используемых приемов для выяснения того, что не так с шейдером, состоит в том, чтобы отправить все соответствующие переменные в шейдерной программе непосредственно в выходной канал фрагментного шейдера. Выводя шейдерные переменные напрямую в выходной канал с цветом мы можем узнать интересную информацию проверяя картинку на выходе. К примеру, нам надо узнать, правильные ли нормали у модели. Мы можем отправить их (трансформированными или нет) из вершинного в фрагментный шейдер, где мы выведем нормали как-то так:
(прим. пер: почему нет подсветки синтаксиса GLSL?)
#version 330 core
out vec4 FragColor;
in vec3 Normal;
[...]
void main()
{
[...]
FragColor.rgb = Normal;
FragColor.a = 1.0f;
}
Выводя нецветовую переменную в выходной канал с цветом как сейчас, мы можем быстро проверить значение переменной. Если, к примеру, результатом стал черный экран, то ясно, что нормали неправильно переданы в шейдеры, а когда они отображаются, сравнительно легко проверить их на правильность:
Из визуальных результатов мы можем видеть, что нормали верны, так как правая сторона костюма преимущественно красная (что говорит, что нормали примерно показывают в направлении полощительной оси x) и также передняя сторона костюма окрашена в направлении положительной оси z (в синий цвет).
Этот подход можно расширить на любую переменную, которую вы хотите протестировать. Каждый раз, когда вы застряли и предполагаете, что ошибка в шейдерах, попробуйте отрисовывать несколько переменных или промежуточных результатов и выяснить, в какой части алгоритма есть ошибка.
OpenGL GLSL reference compiler
В каждом видеодрайвере свои причуды. К примеру, драйвера NVIDIA немного смягчают требования спецификации, а драйвера AMD лучше соответствую спецификациям (что лучше, как мне кажется). Проблема в том, что шейдеры работающие на одной машине, могут не заработать на другой из-за отличий в драйверах.
За несколько лет опыта вы могли выучить все отличия между различными GPU, но если вы хотите быть уверены в том, что ваши шейдеры будут работать везде, то вы можете сверить ваш код с официальной спецификацией с помощью GLSL reference compiler. Вы можете скачать так называемый GLSL lang validator тут (исходник).
С этой программой вы можете проверить свои шейдеры, передавая их как 1-й аргумент к программе. Помните, что программа определяет тип шейдера по расширению:
.vert
: вершинный шейдер.frag
: фрагментный шейдер.geom
: геометрический шейдер.tesc
: тесселяционный контролирующий шейдер.tese
: тесселяционный вычислительный шейдер.comp
: вычислительный шейдер
Запустить программу легко:
glslangValidator shader.vert
Заметьте, что если нет ошибок, то программа ничего не выведет. На сломанном вершинном шейдере вывод будет похож на:
Программа не покажет различий между компиляторами GLSL от AMD, NVidia или Intel, и даже не может сообщить обо всех багах в шейдере, но он хотя бы проверяет шейдеры на соответствие стандартам.
Вывод буфера кадра
Еще один метод для вашего инструментария — отобразить содержимое кадрового буфера в определенной части экрана. Скорее всего, вы часто используете кадровые буферы и, поскольку вся магия происходит за кадром, бывает трудно определить, что происходит. Вывод содержимого кадрового буфера — полезный прием, чтобы проверить правильность вещей.
Заметьте, что содержимое кадрового буфера, как тут объясняется, работает с текстурами, а не с объектами буферов отрисовки
Используя простой шейдер, который отрисовывает одну текстуру, мы можем написать небольшую функцию, быстро отрисовывающую любую текстуру в правом верхнем углу экрана:
// vertex shader
#version 330 core
layout (location = 0) in vec2 position;
layout (location = 1) in vec2 texCoords;
out vec2 TexCoords;
void main()
{
gl_Position = vec4(position, 0.0f, 1.0f);
TexCoords = texCoords;
}
//fragment shader
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D fboAttachment;
void main()
{
FragColor = texture(fboAttachment, TexCoords);
}
//main.cpp
void DisplayFramebufferTexture(GLuint textureID)
{
if(!notInitialized)
{
// initialize shader and vao w/ NDC vertex coordinates at top-right of the screen
[...]
}
glActiveTexture(GL_TEXTURE0);
glUseProgram(shaderDisplayFBOOutput);
glBindTexture(GL_TEXTURE_2D, textureID);
glBindVertexArray(vaoDebugTexturedRect);
glDrawArrays(GL_TRIANGLES, 0, 6);
glBindVertexArray(0);
glUseProgram(0);
}
int main()
{
[...]
while (!glfwWindowShouldClose(window))
{
[...]
DisplayFramebufferTexture(fboAttachment0);
glfwSwapBuffers(window);
}
}
Это даст вам небольшое окошко в углу экрана для отладочного вывода кадрового буфера. Полезно, к примеру, когда пытаешься проверить корректность нормалей:
Вы также можете расширить эту функцию так, чтобы она отрисовывала больше 1 текстуры. Это быстрый путь получить непрерывную отдачу от чего угодно в кадровых буферах.
Внешние программы-отладчики
Когда ничего не помогает, есть еще один прием: воспользоваться сторонними программами. Они встраиваются в драйвера OpenGL и могут перехватывать все вызовы OpenGL, чтобы дать вам очень много интересных данных о вашем приложении. Эти приложения могут профилировать использование функций OpenGL, искать узкие места, наблюдать за кадровыми буферами, текстурами и памятью. Во время работы над (большим) кодом, эти инструменты могут стать бесценными.
Я перечислил несколько популярных инструментов. Попробуйте каждый и выберите тот, который лучше всего вам подходит.
RenderDoc
RenderDoc — хороший (полностью опенсорсный) отдельный отладочный инструмент. Чтобы начать захват, выберите исполняемый файл и рабочую папку (working directory). Ваше приложение работает как обычно, и когда вы хотите понаблюдать за отдельным кадром, вы позволяете RenderDoc снять несколько кадров вашего приложения. Среди захваченных кадров вы можете просмотреть состояние конвейера, все команды OpenGL, хранилище буферов и используемые текстуры.
CodeXL
CodeXL — инструмент отладки GPU, работает как отдельное приложение и плагин к Visual Studio. CodeXL Дает много информации и отлично подходит для профилирования графических приложений. CodeXL также работает на видеокартах от NVidia и Intel, но без поддержки отладки OpenCL.
Я не так много использовал CodeXL, поскольку RenderDoc мне показался проще, но я включил CodeXL в этот список, потому что он выглядит довольно надежным инструментом и в основном разработан одним из крупных производителей графических процессоров.
NVIDIA Nsight
Nsight — популярный инструмент отладки GPU от NUIDIA. Является не только плагином к Visual Studio и Eclipse, но еще и отдельное приложение. Плагин Nsight — очень полезная вещь для графических разработчиков, поскольку собирает много статистик в реальном времени относительно использования GPU и покадрового состояния GPU.
В тот момент, когда вы запускаете свое приложение через Visual Studio или Eclipse с помощью команд отладки или профилирования Nsight, он запустится сам внутри приложения. Хорошая вещь в Nsight: рендер ГИП-системы (GUI, графический интерфейс пользователя) поверх запускаемого приложения, которую можно использовать для собирания информации всех видов о вашем приложении в реальном времени или покадровом анализе.
Nsight — очень полезный инструмент, который, по моему мнению, превосходит вышеперечисленные инструменты, но имеет один серьезный недостаток: работает только на видеокартах от NVIDIA. Если вы работаете на видеокартах от NVIDIA и используете Visual Studio — определенно стоит попробовать Nsight.
Я уверен, что есть еще инструменты для отладки графических приложений (к примеру, VOGL и APItrace), но я считаю, что этот список уже предоставил вам достаточно инструментов для экспериментов. Я не эксперт в вышеупомянутых инструментах, так что если есть ошибки, то пишите мне (переводчику) в личные сообщения и в комментарии к оригинальной статье (если конечно же, там еще осталась эта ошибка).
Дополнительные материалы
- Почему я вижу черный экран? — список возможных случаев появления черного экрана вместо нужной картинки от Reto Koradi.
- Отладочный вывод — обширный список методов настройки отладочного контекста в разных оконных менеджерах от Vallentin Source.
P.S.: У нас есть телеграм-конфа для координации переводов. Если есть серьезное желание помогать с переводом, то милости просим!
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Альтернативное голосование
Проголосовали 30 пользователей.
Воздержались 8 пользователей.
Many of the features of the OpenGL API are very useful and powerful. But, it’s quite possible that OpenGL programs may contain errors. So, it becomes important to learn error handling in OpenGL programs. The OpenGL and GLU libraries have a simple method of recording errors. When an OpenGL program encounters an error in a call to a base library routine or a GLU routine, it records an error code internally, and the routine which caused the error is ignored. Although, OpenGL records only a single error code at any given time. OpenGL uses its own methods to detect errors. Once an error occurs, no other error code will be recorded until the program explicitly queries the OpenGL error state.
Syntax:
GLenum code;
code = glGetError ();
This function call returns the current error code and clears the internal error flag:
- If the returned value is equal to the GLNOERROR OpenGL symbolic constant, everything is fine.
- If there is any other return value, then it indicates that a problem has occurred.
The base OpenGL library provides a definition for a number of symbolic constants which represent different error conditions. The GLU library also defines a number of error codes, but most of them have almost meaningless names such as GLUNURBSERROR1, GLUNURBSERROR2, and so on.
The GLU library contains a function that returns a descriptive string for each of the GLU and GL errors. To use it, first retrieve the current error code and then pass it as a parameter to this function. The return value can be printed out using the C standard library functions like fprintf() function.
Below is the code snippet to implement the above approach:
C
#include <stdio.h>
GLenum code;
const
GLubyte* string;
code = glGetError();
string = gluErrorString(code);
fprintf
(stderr,
"OpenGL error: %sn"
, string);
Explanation: The value returned by gluErrorString points to a string located inside the GLU library. Since it is not a dynamically allocated string, so it must not be explicitly deallocated by the program. Additionally, it mustn’t be modified by the program (therefore, the const modifier on the declaration of string). It is quite easy to encapsulate these function calls into a general error-reporting function in the program.
The function given below will retrieve the current error code, print the descriptive error string, and return the code to the calling routine:
C
#include <stdio.h>
GLenum errorCheck()
{
GLenum code;
const
GLubyte* string;
code = glGetError();
if
(code != GL_NO_ERROR) {
string = gluErrorString(code);
fprintf
(stderr,
"OpenGL error: %sn"
, string);
}
return
code;
}
Explanation: By default, glGetError only prints error numbers, which isn’t easy to understand unless error codes have been memorized already. Thus, it often makes sense to write a small helper function to easily print out the error strings together with where the error check function was called:
GLenum glCheckError_(const char *file, int line)
{
GLenum errorCode;
while ((errorCode = glGetError()) != GL_NO_ERROR)
{
std::string error;
switch (errorCode)
{
case GL_INVALID_ENUM: error = “INVALID_ENUM”; break;
case GL_INVALID_VALUE: error = “INVALID_VALUE”; break;
case GL_INVALID_OPERATION: error = “INVALID_OPERATION”; break;
case GL_STACK_OVERFLOW: error = “STACK_OVERFLOW”; break;
case GL_STACK_UNDERFLOW: error = “STACK_UNDERFLOW”; break;
case GL_OUT_OF_MEMORY: error = “OUT_OF_MEMORY”; break;
case GL_INVALID_FRAMEBUFFER_OPERATION: error = “INVALID_FRAMEBUFFER_OPERATION”; break;
}
std::cout << error << ” | ” << file << ” (” << line << “)” << std::endl;
}
return errorCode;
}
#define glCheckError() glCheckError_(__FILE__, __LINE__)
It’s helpful to more precisely know which glCheckError call returned the error if any of these glCheckError calls are grouped in our database.
glBindBuffer(GL_VERTEX_ARRAY, vbo);
glCheckError();
Output Example:
You’re following a null pointer.
#0 0x00000320 in ?? ()
#1 0x004020cb in StateManager::Loop (this=0x6bfeec) at src/StateManager.cpp:81
The first line, #0, the address is a very low number. This commonly happens when the compiler is looking at a null pointer (typically address 0) plus the offset of a data member. In this case, it is probably looking at 0 + 0x320, meaning it is looking for a function or data member 0x320 bytes in to a null object. (The value 0x320 == 800, meaning the address is 800 bytes in, which is fairly large for a non-resource object in C++. You might consider trimming your object size.)
Fortunately you’ve got another line in your dump. When the first line is a very low number like that generally you look at the second line, #1, and nearly always this will be a line of code that calls into a pointer. In your case, it is src/StateManager.cpp:81, which reads: (*it)->Event (); Jackpot, you’ve got a few of them. You dereference an iterator, then you follow it as a pointer.
So now to see what the iterator points to and what it contains. Look up one statement before for the definition, and this is the first line any of those items were used, which reinforces that one of the values are bad. The line that initialized it was: auto it = m_states.begin() + m_current;
So that gives a few possible errors.
For one thing, you don’t check what happens if m_states is empty. Even if m_current is 0 you invoke undefined behavior. If the container is empty, the .begin() function will equal .end() and it is undefined behavior to dereference it. Bug #1. Ensure your container is not empty before using its contents.
It is also undefined behavior to move past the .end() of a container. You can reach .end(), but it is undefined to advance beyond it. Bug #2. Stay in range.
Next, if your m_states contains data but m_current advances to the end or beyond the end, then .begin() gives an iterator to the first one and the addition moves it forward. Depending on which condition the same as above, either you are forbidden from dereferencing that value, or you are forbidden from advancing past the end. Potential bug #3 because you don’t validate either case.
Alternatively, your line of (*it)->Event (); could have a perfectly valid iterator, but the contents of the iterator are a null pointer. It is undefined behavior to follow a null pointer. So potential bug #4 is that you have stored a null value. If null is a proper condition for your code then you need to test for null before following your pointer. If null should never be allowed then you’ve put bad data into your container and you need to hunt that down.
Drop a breakpoint on line 79 and inspect the values to see which of those conditions you triggered. Whichever way the problem lies, you should modify your code to address and prevent all four possibilities.
Graphics programming can be a lot of fun, but it can also be a large source of frustration whenever something isn’t rendering just right, or perhaps not even rendering at all! Seeing as most of what we do involves manipulating pixels, it can be difficult to figure out the cause of error whenever something doesn’t work the way it’s supposed to. Debugging these kinds of visual errors is different than what you’re used to when debugging errors on the CPU. We have no console to output text to, no breakpoints to set on GLSL code, and no way of easily checking the state of GPU execution.
In this chapter we’ll look into several techniques and tricks of debugging your OpenGL program. Debugging in OpenGL is not too difficult to do and getting a grasp of its techniques definitely pays out in the long run.
glGetError()
The moment you incorrectly use OpenGL (like configuring a buffer without first binding any) it will take notice and generate one or more user error flags behind the scenes. We can query these error flags using a function named glGetError that checks the error flag(s) set and returns an error value if OpenGL got misused:
GLenum glGetError();
The moment glGetError is called, it returns either an error flag or no error at all. The error codes that glGetError can return are listed below:
Flag | Code | Description |
---|---|---|
GL_NO_ERROR | 0 | No user error reported since the last call to glGetError. |
GL_INVALID_ENUM | 1280 | Set when an enumeration parameter is not legal. |
GL_INVALID_VALUE | 1281 | Set when a value parameter is not legal. |
GL_INVALID_OPERATION | 1282 | Set when the state for a command is not legal for its given parameters. |
GL_STACK_OVERFLOW | 1283 | Set when a stack pushing operation causes a stack overflow. |
GL_STACK_UNDERFLOW | 1284 | Set when a stack popping operation occurs while the stack is at its lowest point. |
GL_OUT_OF_MEMORY | 1285 | Set when a memory allocation operation cannot allocate (enough) memory. |
GL_INVALID_FRAMEBUFFER_OPERATION | 1286 | Set when reading or writing to a framebuffer that is not complete. |
Within OpenGL’s function documentation you can always find the error codes a function generates the moment it is incorrectly used. For instance, if you take a look at the documentation of glBindTexture function, you can find all the user error codes it could generate under the Errors section.
The moment an error flag is set, no other error flags will be reported. Furthermore, the moment glGetError is called it clears all error flags (or only one if on a distributed system, see note below). This means that if you call glGetError once at the end of each frame and it returns an error, you can’t conclude this was the only error, and the source of the error could’ve been anywhere in the frame.
Note that when OpenGL runs distributedly like frequently found on X11 systems, other user error codes can still be generated as long as they have different error codes. Calling glGetError then only resets one of the error code flags instead of all of them. Because of this, it is recommended to call glGetError inside a loop.
glBindTexture(GL_TEXTURE_2D, tex);
std::cout << glGetError() << std::endl; // returns 0 (no error)
glTexImage2D(GL_TEXTURE_3D, 0, GL_RGB, 512, 512, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
std::cout << glGetError() << std::endl; // returns 1280 (invalid enum)
glGenTextures(-5, textures);
std::cout << glGetError() << std::endl; // returns 1281 (invalid value)
std::cout << glGetError() << std::endl; // returns 0 (no error)
The great thing about glGetError is that it makes it relatively easy to pinpoint where any error may be and to validate the proper use of OpenGL. Let’s say you get a black screen and you have no idea what’s causing it: is the framebuffer not properly set? Did I forget to bind a texture? By calling glGetError all over your codebase, you can quickly catch the first place an OpenGL error starts showing up.
By default glGetError only prints error numbers, which isn’t easy to understand unless you’ve memorized the error codes. It often makes sense to write a small helper function to easily print out the error strings together with where the error check function was called:
GLenum glCheckError_(const char *file, int line)
{
GLenum errorCode;
while ((errorCode = glGetError()) != GL_NO_ERROR)
{
std::string error;
switch (errorCode)
{
case GL_INVALID_ENUM: error = "INVALID_ENUM"; break;
case GL_INVALID_VALUE: error = "INVALID_VALUE"; break;
case GL_INVALID_OPERATION: error = "INVALID_OPERATION"; break;
case GL_STACK_OVERFLOW: error = "STACK_OVERFLOW"; break;
case GL_STACK_UNDERFLOW: error = "STACK_UNDERFLOW"; break;
case GL_OUT_OF_MEMORY: error = "OUT_OF_MEMORY"; break;
case GL_INVALID_FRAMEBUFFER_OPERATION: error = "INVALID_FRAMEBUFFER_OPERATION"; break;
}
std::cout << error << " | " << file << " (" << line << ")" << std::endl;
}
return errorCode;
}
#define glCheckError() glCheckError_(__FILE__, __LINE__)
In case you’re unaware of what the preprocessor directives __FILE__
and __LINE__
are: these variables get replaced during compile time with the respective file and line they were compiled in. If we decide to stick a large number of these glCheckError calls in our codebase it’s helpful to more precisely know which glCheckError call returned the error.
glBindBuffer(GL_VERTEX_ARRAY, vbo);
glCheckError();
This will give us the following output:
glGetError doesn’t help you too much as the information it returns is rather simple, but it does often help you catch typos or quickly pinpoint where in your code things went wrong; a simple but effective tool in your debugging toolkit.
Debug output
A less common, but more useful tool than glCheckError is an OpenGL extension called debug output that became part of core OpenGL since version 4.3. With the debug output extension, OpenGL itself will directly send an error or warning message to the user with a lot more details compared to glCheckError. Not only does it provide more information, it can also help you catch errors exactly where they occur by intelligently using a debugger.
Debug output is core since OpenGL version 4.3, which means you’ll find this functionality on any machine that runs OpenGL 4.3 or higher. If they’re not available, its functionality can be queried from the ARB_debug_output
or AMD_debug_output
extension. Note that OS X does not seem to support debug output functionality (as gathered online).
In order to start using debug output we have to request a debug output context from OpenGL at our initialization process. This process varies based on whatever windowing system you use; here we will discuss setting it up on GLFW, but you can find info on other systems in the additional resources at the end of the chapter.
Debug output in GLFW
Requesting a debug context in GLFW is surprisingly easy as all we have to do is pass a hint to GLFW that we’d like to have a debug output context. We have to do this before we call glfwCreateWindow:
glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, true);
Once we’ve then initialized GLFW, we should have a debug context if we’re using OpenGL version 4.3 or higher. If not, we have to take our chances and hope the system is still able to request a debug context. Otherwise we have to request debug output using its OpenGL extension(s).
Using OpenGL in debug context can be significantly slower compared to a non-debug context, so when working on optimizations or releasing your application you want to remove GLFW’s debug request hint.
To check if we successfully initialized a debug context we can query OpenGL:
int flags; glGetIntegerv(GL_CONTEXT_FLAGS, &flags);
if (flags & GL_CONTEXT_FLAG_DEBUG_BIT)
{
// initialize debug output
}
The way debug output works is that we pass OpenGL an error logging function callback (similar to GLFW’s input callbacks) and in the callback function we are free to process the OpenGL error data as we see fit; in our case we’ll be displaying useful error data to the console. Below is the callback function prototype that OpenGL expects for debug output:
void APIENTRY glDebugOutput(GLenum source, GLenum type, unsigned int id, GLenum severity,
GLsizei length, const char *message, const void *userParam);
Given the large set of data we have at our exposal, we can create a useful error printing tool like below:
void APIENTRY glDebugOutput(GLenum source,
GLenum type,
unsigned int id,
GLenum severity,
GLsizei length,
const char *message,
const void *userParam)
{
// ignore non-significant error/warning codes
if(id == 131169 || id == 131185 || id == 131218 || id == 131204) return;
std::cout << "---------------" << std::endl;
std::cout << "Debug message (" << id << "): " << message << std::endl;
switch (source)
{
case GL_DEBUG_SOURCE_API: std::cout << "Source: API"; break;
case GL_DEBUG_SOURCE_WINDOW_SYSTEM: std::cout << "Source: Window System"; break;
case GL_DEBUG_SOURCE_SHADER_COMPILER: std::cout << "Source: Shader Compiler"; break;
case GL_DEBUG_SOURCE_THIRD_PARTY: std::cout << "Source: Third Party"; break;
case GL_DEBUG_SOURCE_APPLICATION: std::cout << "Source: Application"; break;
case GL_DEBUG_SOURCE_OTHER: std::cout << "Source: Other"; break;
} std::cout << std::endl;
switch (type)
{
case GL_DEBUG_TYPE_ERROR: std::cout << "Type: Error"; break;
case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR: std::cout << "Type: Deprecated Behaviour"; break;
case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR: std::cout << "Type: Undefined Behaviour"; break;
case GL_DEBUG_TYPE_PORTABILITY: std::cout << "Type: Portability"; break;
case GL_DEBUG_TYPE_PERFORMANCE: std::cout << "Type: Performance"; break;
case GL_DEBUG_TYPE_MARKER: std::cout << "Type: Marker"; break;
case GL_DEBUG_TYPE_PUSH_GROUP: std::cout << "Type: Push Group"; break;
case GL_DEBUG_TYPE_POP_GROUP: std::cout << "Type: Pop Group"; break;
case GL_DEBUG_TYPE_OTHER: std::cout << "Type: Other"; break;
} std::cout << std::endl;
switch (severity)
{
case GL_DEBUG_SEVERITY_HIGH: std::cout << "Severity: high"; break;
case GL_DEBUG_SEVERITY_MEDIUM: std::cout << "Severity: medium"; break;
case GL_DEBUG_SEVERITY_LOW: std::cout << "Severity: low"; break;
case GL_DEBUG_SEVERITY_NOTIFICATION: std::cout << "Severity: notification"; break;
} std::cout << std::endl;
std::cout << std::endl;
}
Whenever debug output detects an OpenGL error, it will call this callback function and we’ll be able to print out a large deal of information regarding the OpenGL error. Note that we ignore a few error codes that tend to not really display anything useful (like 131185
in NVidia drivers that tells us a buffer was successfully created).
Now that we have the callback function it’s time to initialize debug output:
if (flags & GL_CONTEXT_FLAG_DEBUG_BIT)
{
glEnable(GL_DEBUG_OUTPUT);
glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
glDebugMessageCallback(glDebugOutput, nullptr);
glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, nullptr, GL_TRUE);
}
Here we tell OpenGL to enable debug output. The glEnable(GL_DEBUG_SYNCRHONOUS)
call tells OpenGL to directly call the callback function the moment an error occurred.
Filter debug output
With glDebugMessageControl you can potentially filter the type(s) of errors you’d like to receive a message from. In our case we decided to not filter on any of the sources, types, or severity rates. If we wanted to only show messages from the OpenGL API, that are errors, and have a high severity, we’d configure it as follows:
glDebugMessageControl(GL_DEBUG_SOURCE_API,
GL_DEBUG_TYPE_ERROR,
GL_DEBUG_SEVERITY_HIGH,
0, nullptr, GL_TRUE);
Given our configuration, and assuming you have a context that supports debug output, every incorrect OpenGL command will now print a large bundle of useful data:
Backtracking the debug error source
Another great trick with debug output is that you can relatively easy figure out the exact line or call an error occurred. By setting a breakpoint in DebugOutput at a specific error type (or at the top of the function if you don’t care), the debugger will catch the error thrown and you can move up the call stack to whatever function caused the message dispatch:
It requires some manual intervention, but if you roughly know what you’re looking for it’s incredibly useful to quickly determine which call causes an error.
Custom error output
Aside from reading messages, we can also push messages to the debug output system with glDebugMessageInsert:
glDebugMessageInsert(GL_DEBUG_SOURCE_APPLICATION, GL_DEBUG_TYPE_ERROR, 0,
GL_DEBUG_SEVERITY_MEDIUM, -1, "error message here");
This is especially useful if you’re hooking into other application or OpenGL code that makes use of a debug output context. Other developers can quickly figure out any reported bug that occurs in your custom OpenGL code.
In summary, debug output (if you can use it) is incredibly useful for quickly catching errors and is well worth the effort in setting up as it saves considerable development time. You can find a source code example here with both glGetError and debug output context configured; see if you can fix all the errors.
Debugging shader output
When it comes to GLSL, we unfortunately don’t have access to a function like glGetError nor the ability to step through the shader code. When you end up with a black screen or the completely wrong visuals, it’s often difficult to figure out if something’s wrong with the shader code. Yes, we have the compilation error reports that report syntax errors, but catching the semantic errors is another beast.
One frequently used trick to figure out what is wrong with a shader is to evaluate all the relevant variables in a shader program by sending them directly to the fragment shader’s output channel. By outputting shader variables directly to the output color channels, we can convey interesting information by inspecting the visual results. For instance, let’s say we want to check if a model has correct normal vectors. We can pass them (either transformed or untransformed) from the vertex shader to the fragment shader where we’d then output the normals as follows:
#version 330 core
out vec4 FragColor;
in vec3 Normal;
[...]
void main()
{
[...]
FragColor.rgb = Normal;
FragColor.a = 1.0f;
}
By outputting a (non-color) variable to the output color channel like this we can quickly inspect if the variable is, as far as you can tell, displaying correct values. If, for instance, the visual result is completely black it is clear the normal vectors aren’t correctly passed to the shaders; and when they are displayed it’s relatively easy to check if they’re (sort of) correct or not:
From the visual results we can see the world-space normal vectors appear to be correct as the right sides of the backpack model is mostly colored red (which would mean the normals roughly point (correctly) towards the positive x axis). Similarly, the front side of the backpack is mostly colored towards the positive z axis (blue).
This approach can easily extend to any type of variable you’d like to test. Whenever you get stuck and suspect there’s something wrong with your shaders, try displaying multiple variables and/or intermediate results to see at which part of the algorithm something’s missing or seemingly incorrect.
OpenGL GLSL reference compiler
Each driver has its own quirks and tidbits; for instance, NVIDIA drivers are more flexible and tend to overlook some restrictions on the specification, while ATI/AMD drivers tend to better enforce the OpenGL specification (which is the better approach in my opinion). The result of this is that shaders on one machine may not work on the other due to driver differences.
With years of experience you’ll eventually get to learn the minor differences between GPU vendors, but if you want to be sure your shader code runs on all kinds of machines you can directly check your shader code against the official specification using OpenGL’s GLSL reference compiler. You can download the so called GLSL lang validator binaries from here or its complete source code from here.
Given the binary GLSL lang validator you can easily check your shader code by passing it as the binary’s first argument. Keep in mind that the GLSL lang validator determines the type of shader by a list of fixed extensions:
.vert
: vertex shader..frag
: fragment shader..geom
: geometry shader..tesc
: tessellation control shader..tese
: tessellation evaluation shader..comp
: compute shader.
Running the GLSL reference compiler is as simple as:
glsllangvalidator shaderFile.vert
Note that if it detects no error, it returns no output. Testing the GLSL reference compiler on a broken vertex shader gives the following output:
It won’t show you the subtle differences between AMD, NVidia, or Intel GLSL compilers, nor will it help you completely bug proof your shaders, but it does at least help you to check your shaders against the direct GLSL specification.
Framebuffer output
Another useful trick for your debugging toolkit is displaying a framebuffer’s content(s) in some pre-defined region of your screen. You’re likely to use framebuffers quite often and, as most of their magic happens behind the scenes, it’s sometimes difficult to figure out what’s going on. Displaying the content(s) of a framebuffer on your screen is a useful trick to quickly see if things look correct.
Note that displaying the contents (attachments) of a framebuffer as explained here only works on texture attachments, not render buffer objects.
Using a simple shader that only displays a texture, we can easily write a small helper function to quickly display any texture at the top-right of the screen:
// vertex shader
#version 330 core
layout (location = 0) in vec2 position;
layout (location = 1) in vec2 texCoords;
out vec2 TexCoords;
void main()
{
gl_Position = vec4(position, 0.0f, 1.0f);
TexCoords = texCoords;
}
// fragment shader
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D fboAttachment;
void main()
{
FragColor = texture(fboAttachment, TexCoords);
}
void DisplayFramebufferTexture(unsigned int textureID)
{
if (!notInitialized)
{
// initialize shader and vao w/ NDC vertex coordinates at top-right of the screen
[...]
}
glActiveTexture(GL_TEXTURE0);
glUseProgram(shaderDisplayFBOOutput);
glBindTexture(GL_TEXTURE_2D, textureID);
glBindVertexArray(vaoDebugTexturedRect);
glDrawArrays(GL_TRIANGLES, 0, 6);
glBindVertexArray(0);
glUseProgram(0);
}
int main()
{
[...]
while (!glfwWindowShouldClose(window))
{
[...]
DisplayFramebufferTexture(fboAttachment0);
glfwSwapBuffers(window);
}
}
This will give you a nice little window at the corner of your screen for debugging framebuffer output. Useful, for example, for determining if the normal vectors of the geometry pass in a deferred renderer look correct:
You can of course extend such a utility function to support rendering more than one texture. This is a quick and dirty way to get continuous feedback from whatever is in your framebuffer(s).
External debugging software
When all else fails there is still the option to use a 3rd party tool to help us in our debugging efforts. Third party applications often inject themselves in the OpenGL drivers and are able to intercept all kinds of OpenGL calls to give you a large array of interesting data. These tools can help you in all kinds of ways like: profiling OpenGL function usage, finding bottlenecks, inspecting buffer memory, and displaying textures and framebuffer attachments. When you’re working on (large) production code, these kinds of tools can become invaluable in your development process.
I’ve listed some of the more popular debugging tools here; try out several of them to see which fits your needs the best.
RenderDoc
RenderDoc is a great (completely open source) standalone debugging tool. To start a capture, you specify the executable you’d like to capture and a working directory. The application then runs as usual, and whenever you want to inspect a particular frame, you let RenderDoc capture one or more frames at the executable’s current state. Within the captured frame(s) you can view the pipeline state, all OpenGL commands, buffer storage, and textures in use.
CodeXL
CodeXL is GPU debugging tool released as both a standalone tool and a Visual Studio plugin. CodeXL gives a good set of information and is great for profiling graphics applications. CodeXL also works on NVidia or Intel cards, but without support for OpenCL debugging.
I personally don’t have much experience with CodeXL since I found RenderDoc easier to use, but I’ve included it anyways as it looks to be a pretty solid tool and developed by one of the larger GPU manufacturers.
NVIDIA Nsight
NVIDIA’s popular Nsight GPU debugging tool is not a standalone tool, but a plugin to either the Visual Studio IDE or the Eclipse IDE (NVIDIA now has a standalone version as well). The Nsight plugin is an incredibly useful tool for graphics developers as it gives a large host of run-time statistics regarding GPU usage and the frame-by-frame GPU state.
The moment you start your application from within Visual Studio (or Eclipse), using Nsight’s debugging or profiling commands, Nsight will run within the application itself. The great thing about Nsight is that it renders an overlay GUI system from within your application that you can use to gather all kinds of interesting information about your application, both at run-time and during frame-by-frame analysis.
Nsight is an incredibly useful tool, but it does come with one major drawback in that it only works on NVIDIA cards. If you are working on NVIDIA cards (and use Visual Studio) it’s definitely worth a shot.
I’m sure there’s plenty of other debugging tools I’ve missed (some that come to mind are Valve’s VOGL and APItrace), but I feel this list should already get you plenty of tools to experiment with.
Additional resources
- Why is your code producing a black window: list of general causes by Reto Koradi of why your screen may not be producing any output.
- Debug Output in OpenGL: an extensive debug output write-up by Vallentin with detailed information on setting up a debug context on multiple windowing systems.
Hi, I’m using C++ to try and create a basic shader program with OpenGL and SDL. So far I’ve been able to create a window and render a multi-colored box on screen. Now I’ve decided to go one step further and add a texture onto that square. So from what I’ve followed and understood, I should have all the necessary code to get a texture to show up, although it keeps breaking at a certain point each time! (It also seems to not be able to run on Nvidia machines)
Below I’ve shown all the code from my Texture class with comments next to the lines that give me run time errors. the GetShaderProgram function gets GLuint m_shaderProgramID from my shader class.
|
|