Россия и Беларусь начали и продолжают войну против народа Украины!

Урок №44. Отладка в OpenGL

  Дмитрий Бушуев  | 

  Обновл. 23 Фев 2022  | 

 5350

 ǀ   1 

Графическое программирование — это очень увлекательное занятие. Но в то же время, оно может стать большим источником разочарований в случае, если наш проект рендерится не так, как мы ожидали или, возможно, не рендерится вообще! Учитывая, что большая часть нашей работы связана с манипулированием пикселями, понять причину ошибки, когда что-то работает не так как должно, бывает очень трудно. Отладка визуальных ошибок такого рода отличается от того, к чему мы привыкли при отладке ошибок на CPU. Ведь у нас нет консоли для вывода текста, нет точек останова для GLSL-кода и нет простого способа проверить состояние выполнения программы на GPU.

На этом уроке мы рассмотрим несколько трюков и приемов, которые помогут нам в отладке OpenGL-программы. На самом деле, отладка в OpenGL не так уж сложна, и понимание её методов определенно окупится в долгосрочной перспективе.

Функция glGetError()

В тот момент, когда мы неправильно используем OpenGL (например, настраиваем буфер без предварительной привязки), библиотека заметит это и сгенерирует один или несколько флагов ошибки. Мы можем запросить состояние данных флагов с помощью функции glGetError(). В случае, если при неправильном использовании OpenGL был поднят флаг ошибки, данная функция вернет её код:

Если же ошибок не было, то функция glGetError() вернет код 0, соответствующий флагу GL_NO_ERROR. Коды ошибок, которые может возвращать функция glGetError(), перечислены ниже:

Флаг Код Описание
GL_NO_ERROR 0 С момента последнего вызова glGetError() никаких ошибок не было обнаружено.
GL_INVALID_ENUM 1280 Устанавливается, когда параметр перечисления не является допустимым.
GL_INVALID_VALUE 1281 Устанавливается, когда значение не является допустимым.
GL_INVALID_OPERATION 1282 Устанавливается, когда команда не является допустимой для заданных параметров.
GL_STACK_OVERFLOW 1283 Устанавливается, когда операция добавления данных в стек вызывает переполнение стека.
GL_STACK_UNDERFLOW 1284 Устанавливается, если операция извлечения данных из стека происходит в момент, когда стек пуст.
GL_OUT_OF_MEMORY 1285 Устанавливается, когда операция выделения памяти не может выделить достаточно памяти.
GL_INVALID_FRAMEBUFFER_OPERATION 1286 Устанавливается при чтении или записи во фреймбуфер, который не является завершенным.

В тот момент, когда произошло появление флага ошибки, сообщения о других флагах ошибок поступать не будут. Кроме того, в момент вызова функции glGetError() происходит сброс всех флагов ошибок (или только одного, если речь идет о распределенной системе (см. примечание ниже)). Это означает, что если мы один раз вызываем glGetError() в конце каждого кадра и функция возвращает код ошибки, то мы не сможем определить, была ли она единственной, и в каком конкретно месте нашего кадра произошло данное событие.

Примечание: Обратите внимание, когда OpenGL работает в распределенном режиме, например, как в системах «X Window System», другие коды ошибок все еще могут быть сгенерированы, если это будут разные коды. Последующий вызов glGetError() сбрасывает только один флаг из всех. Поэтому рекомендуется вызывать glGetError() внутри цикла.

Самое замечательное в функции glGetError() то, что она позволяет относительно легко определить место, в котором могла произойти (любая) ошибка, и проверить правильность использования OpenGL. Допустим, вместо ожидаемого изображения мы получаем черный экран, и понятия не имеем, что его вызывает: может это фреймбуфер не настроен должным образом? Или мы забыли связать текстуру? Вставляя по всему коду вызов функции glGetError(), мы быстро найдем первое место возникновения ошибки в OpenGL.

По умолчанию функция glGetError() сообщает только коды ошибок, по которым трудно понять суть ошибки (если, конечно, не выучить их наизусть). Часто имеет смысл написать небольшую вспомогательную функцию, выводящую описание ошибки вместе с тем местом, где была вызвана функция glGetError():

Директивы препроцессора __FILE__ и __LINE__ во время компиляции заменяются соответствующим файлом и номером строки, в которых они были скомпилированы. Чем больше вызовов glCheckError() мы вставим в наш код, тем более точно сможем определить, какой вызов glCheckError() вернул ошибку.

Это даст нам следующий результат:

Функция glGetError() не решит за нас все проблемы, поскольку возвращаемая ею информация об ошибке довольно скудна. Но она поможет нам поймать опечатки или быстро определить, где в нашем коде что-то пошло не так.

Вывод отладочной информации (debug output)


Менее распространенный, но более полезный инструмент, чем функция glCheckError() — это OpenGL-расширение, именуемое debug output (или «вывод отладочной информации»), которое стало частью ядра OpenGL, начиная с версии OpenGL 4.3. При использовании debug output OpenGL сам напрямую отправит пользователю сообщение (или предупреждение) с гораздо большим объемом детальной информации о вызвавшей данное событие ошибке по сравнению с выводом функции glCheckError(). Оно не только предоставляет дополнительную информацию, но и при грамотном использовании отладчика поможет выловить ошибки именно в том месте, в котором они и возникают.

Примечание: Модуль debug output добавлен в ядро OpenGL, начиная с версии OpenGL 4.3. Если же по каким-то причинам данный модуль у вас недоступен, то вместо него можно воспользоваться расширениями ARB_debug_output или AMD_debug_output. Обратите внимание, что операционная система macOS (похоже) не поддерживает функциональность вывода отладочной информации.

Чтобы начать использовать debug output, необходимо в процессе инициализации запросить у OpenGL контекст отладочного вывода. Этот процесс зависит от того, какую оконную систему вы используете; здесь мы обсудим настройку под GLFW, но вы можете найти информацию о других системах в конце данного урока.

Вывод отладочной информации в GLFW

Запрос контекста отладки в GLFW удивительно прост, так как всё, что нам нужно сделать, — это передать подсказку для GLFW, что мы хотели бы иметь контекст вывода отладочной информации. Эти действия должны выполняться прежде, чем будет вызвана функция glfwCreateWindow():

Если мы используем OpenGL версии 4.3 или выше, то, как только инициализируем GLFW, у нас должен появиться контекст отладки. В противном случае мы должны будем запросить отладочный вывод, используя расширение(я) OpenGL.

Примечание: Работа OpenGL в контексте отладки может быть значительно медленнее, по сравнению с обычным режимом, поэтому при работе над оптимизацией или выпуском релизной версии приложения вам необходимо будет удалить подсказку GLFW о запросе отладки.

Чтобы проверить, успешно ли мы инициализировали контекст отладки, мы можем запросить у OpenGL статус данной операции:

Принцип работы debug output заключается в том, что мы передаем OpenGL callback-функцию регистрации ошибок (аналогично callback в GLFW) и уже в callback-функции мы можем обрабатывать данные об ошибках OpenGL; в нашем случае мы будем выводить информацию в консоль. Ниже приведен прототип callback-функции, которую необходимо передать OpenGL для вывода отладочной информации:

Учитывая имеющийся у нас довольно большой набор данных, можно, как показано ниже, создать полезный инструмент вывода информации об ошибках:

Всякий раз, когда debug output обнаруживает ошибку OpenGL, он вызывает callback-функцию, и мы получаем подробную информацию об этой ошибке. Обратите внимание, что мы игнорируем несколько кодов ошибок, которые, как правило, не отображают ничего полезного (например, код 131185 выдается драйверами NVIDIA в случае, когда буфер был успешно создан).

Теперь, когда у нас есть callback-функция, пришло время инициализировать debug output:

Здесь мы сообщаем OpenGL включить вывод отладочной информации. Вызов функции glEnable(GL_DEBUG_SYNCHRONOUS) указывает OpenGL вызывать callback-функцию непосредственно в момент возникновения ошибки.

Фильтр вывода отладочной информации

С помощью функции glDebugMessageControl() у нас появляется возможность отфильтровать тип(ы) ошибок, по которым мы хотели бы получать сообщения. В нашем случае мы решили не фильтровать ни по одному из источников, типов или уровней серьезности. Если бы мы хотели показывать только сообщения из OpenGL API, которые являются ошибками и имеют высокую степень серьезности, то мы бы настроили вызов функции glDebugMessageControl() следующим образом:

Теперь, учитывая нашу конфигурацию и предполагая, что у нас есть контекст, поддерживающий вывод отладочной информации, каждая OpenGL-команда, выдавшая ошибку, будет подробно информировать о ней:


Отслеживание источника ошибки

Еще один отличный трюк с debug output заключается в том, что мы можем довольно легко вычислить точную строку (или вызов), в которой произошла ошибка. Устанавливаем точку останова на нужном типе ошибки внутри функции glDebugOutput() (или на начало самой функции, если это неважно), отладчик поймает возникшую ошибку, и мы сможем перейти вверх по стеку вызовов к любой функции, вызвавшей отправку сообщения об ошибке:

Это требует некоторого ручного вмешательства, но если примерно известно, что искать, то можно очень быстро определить, какой вызов приводит к ошибке.

Настраиваемый вывод информации об ошибках

Помимо чтения сообщений, мы также можем отправлять сообщения в систему вывода отладочной информации. Для этого используется функция glDebugMessageInsert():

Это особенно полезно, если мы подключаемся к другому приложению или OpenGL-коду, который использует контекст вывода отладки. Другие разработчики могут быстро вычислить любую появившуюся ошибку, которая возникла в нашем пользовательском OpenGL-коде.

Таким образом, debug output (если мы можем его использовать) невероятно полезен для быстрого отлавливания ошибок и вполне стоит усилий, затрачиваемых на его настройку, поскольку он экономит значительное время разработки.

В следующем исходнике вы сможете найти пример исходного кода с использованием функции glGetError() и контекста вывода отладочной информации; посмотрите, сможете ли вы исправить все ошибки.

  GitHub / Урок №44. Отладка в OpenGL — Исходный код

Важное примечание: При попытке скомпилировать вышеприведенный исходной код, у вас могут появиться ошибки типа «Неизвестный идентификатор GL_STACK_OVERFLOW» и т.п. Чтобы решить данную проблему, необходимо пересобрать библиотеку GLAD с поддержкой более свежей версии OpenGL, например, 4.5 (напомню, сейчас мы работаем с GLAD, собранной под OpenGL 3.3). Вы можете сделать это самостоятельно, руководствуясь следующими параметрами:

   LanguageC/C++ Debug;

   API glVersion 4.5;

   ProfileCore;

   ExtensionsGL_KHR_debug;

   Options — отметить пункт "Generate a loader".

Либо просто скопировать уже готовые заголовочные файлы (на всякий случай я добавил библиотеку glwf3.lib) и файл glad.c из соответствующих папок репозитория в свои локальные папки подключаемых заголовочных файлов и файлов библиотек (с заменой содержимого):

и

Отладочная информация шейдеров

Когда речь заходит о GLSL, то у нас, к сожалению, нет доступа к такой функции, как glGetError(), как и нет возможности пошагово выполнять код шейдера. Когда вместо ожидаемого результата работы нашей программы мы получаем черный экран или совершенно неправильные визуальные эффекты, то часто бывает трудно понять, что происходит, если в коде шейдера есть ошибка. Да, у нас есть отчеты компиляции, которые сообщают о синтаксических ошибках, но выловить семантические — то еще дело.

Один из часто используемых трюков при выяснении, что не так с шейдером, заключается в том, чтобы оценить все соответствующие переменные шейдерной программы, отправив их непосредственно в выходной канал фрагментного шейдера. Выводя шейдерные переменные непосредственно в выходные цветовые каналы, мы можем передавать интересную информацию, проверяя визуальные результаты. Например, предположим, что мы хотим проверить, имеет ли модель правильные нормальные векторы. Мы можем передать их (трансформированные или нетрансформированные) из вершинного шейдера во фрагментный шейдер, где затем отобразим нормали следующим образом:

Выводя переменную (нецветовую) в выходной цветовой канал подобным образом, мы можем быстро проверить, содержит ли переменная правильные значения. Если, например, визуальным результатом является полностью черный экран, то становится ясно, что нормальные векторы передаются шейдерам некорректно; а когда они визуализированы, то относительно легко проверить, являются ли они (своего рода) корректными или нет:

На вышеприведенной картинке мы видим, что нормальные векторы мирового пространства являются корректными, поскольку боковые стороны модели рюкзака окрашены в красный цвет (это означает, что нормали указываются в (верном) положительном направлении оси x). Аналогично, передняя сторона рюкзака окрашена синим цветом, соответствующим направлению нормалей в сторону положительной оси z.

Этот подход можно легко распространить на любой тип переменной, которую вы хотите протестировать. Всякий раз, когда вы подозреваете, что с вашими шейдерами что-то не так, попробуйте отобразить несколько переменных и/или промежуточных результатов, чтобы увидеть, в какой части алгоритма что-то отсутствует или кажется неправильным.

Эталонный OpenGL-компилятор GLSL


Каждый драйвер имеет свои особенности; например, драйверы NVIDIA более гибки и склонны игнорировать некоторые ограничения спецификации, в то время как драйверы ATI/AMD имеют тенденцию лучше применять спецификацию OpenGL (что, на мой взгляд, является наиболее правильным подходом). В результате шейдеры, отлично работающие на одном компьютере, могут не работать на другом из-за различий в драйверах.

Со временем, приобретя некоторый опыт в графическом программировании, вы в конечном итоге узнаете о незначительных различиях между поставщиками графических процессоров, но если вы хотите быть уверены, что ваш шейдерный код работает на всех типах компьютеров, то вы можете напрямую проверить его на соответствие официальной спецификации с помощью эталонного OpenGL-компилятора GLSL. Вы можете скачать так называемые двоичные файлы GLSL lang validator отсюда или же полный исходный код отсюда.

Используя GLSL lang validator, вы можете легко проверить свой шейдерный код, передав его в качестве первого аргумента файлу валидатора. Имейте в виду, что GLSL lang validator определяет тип шейдера по заданному списку расширений:

   .vert — вершинный шейдер;

   .frag — фрагментный шейдер;

   .geom — геометрический шейдер;

   .tesc — шейдер управления тесселяцией;

   .tese — шейдер вычисления тесселяции;

   .comp — вычислительный шейдер.

Запуск эталонного компилятора GLSL выглядит следующим образом:

Обратите внимание, что если он не обнаруживает ошибки, то и не возвращает никаких выходных данных. Тестирование эталонного компилятора GLSL на «сломанном» вершинном шейдере дает следующие выходные данные:

Он не покажет вам различий между компиляторами AMD, NVIDIA или Intel GLSL, а также не поможет вам полностью защитить ваши шейдеры от ошибок, но он, по крайней мере, поможет вам проверить ваши шейдеры на прямое соответствие спецификации GLSL.

Вывод фреймбуфера

Еще один полезный трюк для вашего инструментария отладки — это отображение содержимого фреймбуфера(ов) в некоторой предопределенной области экрана. Вы, вероятно, довольно часто будете использовать фреймбуферы, и, поскольку большая часть их магии выполняется «за кулисами», то иногда трудно понять, что происходит на самом деле. Отображение содержимого фреймбуфера на экране — это полезный трюк, дающий возможность быстро увидеть, правильно ли всё выглядит.

Примечание: Обратите внимание, что отображение содержимого (прикрепленных объектов) фреймбуфера, как здесь описывается, работает только с объектами текстур, но не с объектами рендербуфера.

Используя простой шейдер, который отображает только текстуру, мы можем с легкостью написать небольшую вспомогательную функцию для быстрого отображения любой текстуры в правом верхнем углу экрана:

и

Это даст вам милое маленькое окошко в углу экрана, в котором вы будете видеть вывод фреймбуфера. Это полезно, например, для определения того, правильно ли выглядят нормальные векторы геометрического этапа при отложенном рендеринге:

Конечно, вы можете расширить такую вспомогательную функцию для поддержки рендеринга более чем одной текстуры. Это быстрый и малозатратный способ получить обратную связь от всего, что находится в вашем фреймбуфере в режиме реального времени.

Сторонние инструменты отладки


Вы также можете использовать стороннее программное обеспечение. Сторонние программы часто уже внедрены в OpenGL-драйверы и способны перехватывать все виды вызовов OpenGL, чтобы предоставить вам наиболее полную информацию обо всем, что может понадобиться. Эти инструменты могут помочь вам в профилировании использования OpenGL-функций, поиске узких мест, проверке буферной памяти и отображении текстур и вложений фреймбуфера. Когда вы работаете над (большим) кодом, эти инструменты могут стать неоценимыми помощниками в процессе разработки.

Ниже перечислены некоторые из наиболее популярных инструментов отладки; попробуйте несколько из них, чтобы увидеть, что лучше всего соответствует вашим потребностям.

RenderDoc

RenderDoc — это отличный автономный инструмент отладки. Чтобы начать захват, вы указываете исполняемый файл, который хотите захватить, и рабочий каталог. Затем приложение запускается как обычно, и всякий раз, когда вы хотите проверить определенный кадр, вы позволяете RenderDoc захватить один или несколько кадров в текущем состоянии исполняемого файла. В захваченных кадрах можно просмотреть состояние конвейера, все команды OpenGL, буферное хранилище и используемые текстуры.


CodeXL

CodeXL — это инструмент отладки GPU, выпущенный как автономный инструмент, так и в качестве плагина к Visual Studio. CodeXL дает хороший набор информации и отлично подходит для профилирования графических приложений. Программа также работает на картах NVIDIA или Intel, но без поддержки отладки OpenGL.

Лично у меня нет большого опыта работы с CodeXL, так как я нахожу RenderDoc более простым в использовании, но я все равно включил его в этот список, поскольку он выглядит довольно солидным инструментом и разработан одним из крупнейших производителей графических процессоров.

NVIDIA Nsight

Популярный инструмент отладки GPU NVIDIA Nsight — это плагин к Visual Studio IDE или Eclipse IDE (теперь у NVIDIA также есть автономная версия). NVIDIA Nsight — это невероятно полезный инструмент для графических разработчиков, поскольку собирает объемную статистику в режиме реального времени использования графического процессора и о его состоянии кадр за кадром.

В то время, когда вы запускаете приложение из Visual Studio (или Eclipse) с помощью команд отладки или профилирования, NVIDIA Nsight запускается в самом приложении. Самое замечательное в NVIDIA Nsight то, что он визуализирует оверлей из вашего приложения, который вы можете использовать для сбора всех видов интересующей вас информации о приложении как во время выполнения, так и во время покадрового анализа.

NVIDIA Nsight является невероятно полезным инструментом, но у него есть один существенный недостаток, заключающийся в том, что он работает только на картах NVIDIA.

Я уверен, что еще есть много других инструментов отладки, которые я пропустил (например, VOGL Valve или APItrace), но я думаю, что этот список уже должен дать вам много инструментов для экспериментов.

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (8 оценок, среднее: 5,00 из 5)
Загрузка...

Комментариев: 1

  1. Демьян:

    Отлично! Сам считаю, что такую информацию нужно давать конечно ближе к началу. Ничего нет понятней для понимания работы программы, чем отладочная инфа или кэтчи ошибок в коде. Материал супер, понимаю что автор оставил его на вкусное в оригинале, но впринципе можно дать ссылку на этот урок в самое начало, где мы уже понимаем что мы делаем когда байндим и заливаем инфу в шейдеры, чтобы быстрее вылавливать проблемы.

Добавить комментарий

Ваш E-mail не будет опубликован. Обязательные поля помечены *