Skip to content

Latest commit

 

History

History
64 lines (38 loc) · 10.9 KB

kb009_ros_opengl.md

File metadata and controls

64 lines (38 loc) · 10.9 KB

ROS и OpenGL

Здесь будут описаны всякие приколы, связанные с использованием OpenGL (ES) в ROS и не только на различных платформах.

Приколы с EGL

OpenGL для своего функционирования требует наличия контекста. Контекст - некоторая thread-local (это важно!) сущность, к которой привязаны выделенные на данный момент ресурсы (текстуры, массивы вершин и индексов, шейдерные программы и т.д.). Ни одна функция OpenGL не будет работать без контекста; больше того, "десктопный" OpenGL вообще требует наличия контекста для получения адресов функций этого самого OpenGL! Разумеется, попытка вызвать OpenGL-функцию из потока, в котором нет контекста, в лучшем случае приведёт к аварийному завершению программы.

Раньше получение контекста сильно зависело от платформы: на Windows приходилось использовать WGL, на Linux - GLX (привязано к X11), на маке - фиг его знает (в разное время было по-разному, а скоро будет вообще никак нельзя). Затем появились мобильные устройства, и для них Khronos Group разработал стандарт EGL - и получилось достаточно удачно, чтобы EGL перекочевал на десктопы и стал вытеснять platform-specific методы. Разумеется, Apple не смогла не сделать всё по-своему и не разработать EAGL, но на то она и Apple.

Уточню сразу, нас более всего будет интересовать offscreen rendering - то есть ситуация, когда на экран ничего не выводится. Для работы с OpenGL в оконном режиме есть SDL2, и я буду использовать SDL2, пока могу.

Процесс получения контекста в EGL выглядит следующим образом:

  1. Получаем "дисплей" (что бы это ни значило) с помощью eglGetDisplay;
  2. Подключаемся к этому "дисплею" через eglInitialize (и попутно узнаём нашу версию EGL);
  3. Говорим, какие параметры контекста (количество бит цветовых каналов, буфера глубины, поддерживаемые стандарты OpenGL и прочее) нас интересуют функции eglChooseConfig. В идеале она должна предложить список конфигураций, отсортированный по соответствию нашим требованиям;
  4. Говорим, какой API мы будем использовать с помощью eglBindAPI;
  5. Создаём контекст с нужной конфигурацией (из п.3) с помощью eglCreateContext;
  6. (Опционально?) создаём поверхности для рисования/отображения с помощью eglCreate*Surface (вариантов тут много, нас интересует offscreen rendering, так что мы можем взять eglCreatePbufferSurface);
  7. Делаем контекст "текущим" для нашего дисплея с помощью eglMakeCurrent.

Выглядит просто, но на практике есть ряд подводных камней.

Raspberry Pi и EGL

Проблемы начинаются с первого же пункта. В eglGetDisplay можно передать константу EGL_DEFAULT_DISPLAY, по которой драйвер должен выбрать какой-то дисплей. В случае с Raspberry Pi такой фокус не проходит - надо создавать native display через dispmanx (для RPi 3 и ниже) либо через KMS DRM (для RPi 4 и выше, а также при использовании "открытых" драйверов на более старых платах). В последнем случае можно пытаться использовать EGL GBM, смотреть можно в сторону вот этого форумного тредика.

Приколы с Jetson Nano

Это странно, но если в параметрах к eglChooseConfig не указать, что нам нужна конфигурация, поддерживающая Pbuffer (в массиве attribList должны подряд идти записи EGL_SURFACE_TYPE, EGL_PBUFFER_BIT), то окажется, что подходящих конфигураций попросту нет. Ничего страшного, указываем, радуемся жизни.

ES, да не тот

Так уж получилось, что OpenGL ES 1.x прям совсем никак не совместим с OpenGL ES 2.0+. Поэтому при выборе конфигурации и создании контекста надо указывать версию явно:

  • при вызове eglChooseConfig указать, что у Renderable type должен быть ES2 (или даже ES3!) бит - в массиве attribList должны подряд идти записи EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT;
  • при вызове eglCreateContext надо указать, какую же версию мы желаем - там тоже есть массив аттрибутов, и там можно написать EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 0 - тогда нам попытаются отдать контекст версии 3.0 или выше.

Кстати, при запросе контекста версии 2.0 драйвер вправе дать нам контекст более высокой версии, но рассчитывать на это не стоит. Если уж нужен ES3, то и просить надо ES3.

Pbuffer или не Pbuffer?

Вообще говоря, примерно все инструкции по использованию Offscreen OpenGL в той или иной мере забивают на использование Pbuffer'а, и вместо этого используют Framebuffer Object из OpenGL. Читать из FBO довольно просто (glReadPixels), лишние ресурсы не выделяются. В eglMakeCurrent вместо draw и read поверхностей можно передать EGL_NO_SURFACE и просто в framebuffer по умолчанию ничего не рисовать (поскольку не во что).

OpenGL или GLES?

Для своего кода я предпочитаю использовать OpenGL ES. Почему? Ну, например:

  • GLES поддерживается на большем количестве чипов, чем десктопный OpenGL. Это, скорее, вопрос к тем, кто пишет драйверы, но тем не менее;
  • GLES и EGL прекрасно линкуются во время сборки - то есть не надо писать и использовать загрузчики, как в OpenGL. Исключение - расширения GLES, но по большей части без них можно обойтись;
  • GLES замечательно поддерживается и на десктопе - причём как нативно (если драйвера нормальные), так и через ANGLE;
  • GLES не содержит deprecated функции - поскольку пока не успел ими обрасти (впрочем, некоторые считают, что GLES, равно как и OpenGL, уже deprecated);
  • наконец, GLES ещё и в вебе используется - и если вдруг придётся переносить туда свой код, то GLES достаточно будет заменить на WebGL.

Приколы с ROS

Вообще, ROS, видимо, не очень заточен под использование "сырого" OpenGL. Инструменты, которые используют аппаратное ускорение в таком виде, скорее являются оконными и попутно используют какой-нибудь движок типа OGRE. Если уж надо раскрывать потенциал видеокарты, то делается это через какой-нибудь OpenCL, опять же, обёрнутый в OpenCV. Видимо, не в последнюю очередь за это можно быть благодарным сложности OpenGL (ну серьёзно, путь до первого треугольника - сотни строк кода и часы отладки!) и threading-модели ROS (я не зря писал, что контекст - это thread-local штука!).

Nodelet и потоки

В документации к nodelet'ам написано, что метод onInit не должен занимать много времени. Не знаю, насколько я ошибаюсь, но, похоже, что callback'и будут вызываться не в том же thread'е, что и onInit: по крайней мере, в моём случае callback таймера вызывался не там же, где и onInit. Это стоило мне нескольких часов отладки и попыток понять, почему же у меня ничего не рисуется: я создавал контекст в onInit, использовал его в callback'е и наблюдал отсутствие чего-либо нарисованного. Оказалось, что в callback'е контекста не существовало, а все функции OpenGL просто по-тихому не выполнялись.

Вывод простой: имеет смысл посмотреть на текущий EGL-контекст с помощью eglGetCurrentContext, и если контекста нет - создать. Такие вот дела.