Как уже упоминалось ранее, EGL - система для получения контекста OpenGL (ES). Используется она преимущественно в "embedded" (если так можно назвать всякие телефоны и прочие raspberry, на которых крутится вполне себе "взрослый" Linux, часто с не менее "взрослым" GNU) и не лишена своих приколов. Далее будет описан опыт работы с EGL на Raspberry Pi, к которой не подключен монитор (то есть работает RPi в headless-режиме).
Вообще говоря, OpenGL (ES) хоть и не говорит ничего про взаимодействие с оконной системой, но предполагает (большую часть времени), что какая-то поверхность для рисования у него есть. И большую часть времени такая поверхность действительно есть - это окно в какой-то оконной системе (ну или на худой конец целый экран, хотя это крайне редко). Собственно, EGL отвечает за связь с оконной системой (или чем-то, что её заменяет), и когда этой оконной системы нет, начинаются всякие проблемы.
Разумеется, в headless-режиме у нас нет ни оконной системы, ни экрана, на который надо что-то выводить, и нам приходится надеяться на какие-то расширения EGL, которые помогут всё-таки создать контекст.
Более старые модели Raspberry Pi поставлялись с проприетарными драйверами от Broadcom. Эти драйверы были весьма ужасны (могли повиснуть при компиляции абсолютно валидного шейдера и отправить всю систему в kernel panic без особых на то причин), но headless у них включался вообще без проблем:
#include <EGL/egl.h>
void create_context()
{
EGLdisplay dpy = eglGetDisplay(EGL_DEFAULT_DISPLAY); // возвращает валидный display
/* далее - eglInitialize, eglChooseConfig, eglCreateContext, eglMakeCurrent */
}
Больше того, проприетарные драйвера ожидают, что в eglGetDisplay передадут EGL_DEFAULT_DISPLAY (ссылка на реализацию khrn_platform_set_display_id
, вызываемую в реализации eglGetDisplay), с другими значениями display они не дружат. Соответственно, во всех примерах используется eglGetDisplay(EGL_DEFAULT_DISPLAY)
, все рады и счастливы.
Вернее, почти все. Потому что в какой-то момент для Raspberry Pi появились открытые драйвера mesa, которые гораздо лучше работали с X11 и даже давали какую-то реализацию OpenGL 2.1 (не ES!), но со старым кодом так просто не работали. Впрочем, все как-то не особо парились, ибо возможность скомпилировать софт десятилетней давности на RPi без изменений для всех перевешивала здравый смысл.
Летом 2019 года Raspberry Pi Foundation выпустила Raspberry Pi 4. Среди нововведений - новый GPU, для которого, внезапно, не было закрытого драйвера! Всем сказали: "Идите в X11 и используйте там Mesa".
Для большинства казуальных пользователей это было действительно здорово, ибо теперь софт десятилетней давности ещё и почти не тормозил. Правда, по умолчанию даже "швабодный" драйвер выключен, и рисуется всё силами хоть и более сильного (по сравнению с предыдущими моделями), но всё ещё недотягивающего до нормального видеочипа CPU.
К слову, примеры кода, использующие GPU, перестали работать на Raspberry Pi 4.
Тем не менее, жить дальше как-то надо, поэтому я начал смотреть, что можно сделать дальше.
Вообще говоря, EGL ничего не говорит про гарантию поддержки EGL_DEFAULT_DISPLAY
. Значительная часть "разумных" реализаций эту константу поддерживает, но с Raspberry Pi 4 + Mesa такой фокус не прокатывает - по крайней мере, без X11 и подключенного монитора.
Вызов eglGetDisplay(EGL_DEFAULT_DISPLAY)
вернёт какой-то указатель, но eglInitialize
выдаст ошибку при передаче этого дисплея. Fail.
(Кстати, из-за этого огромное количество кода, витающего в интернетах, не будет работать - включая примеры с сайта Khronos)
Беглый поиск по ключевым словам "Raspberry Pi 4 OpenGL ES" приводит к обсуждению на форуме Raspberry по поводу использования некого GBM как бэкенда для EGL/OpenGL ES. В качестве примера использования GBM ссылаются на kmscube
.
Но что такое вообще этот GBM?
Статья в Википедии про mesa упоминает термин generic buffer management. Вроде как это прослойка для DRM (или DRI?), который, в свою очередь, является интерфейсом, который предоставляет ядро linux.
В общем, понятно одно: в мире Linux прям очень любят трёхбуквенные сокращения. KMS, DRM, DRI, GBM. Все как-то связаны, но как именно - непонятно.
В сухом остатке получается следующее: kmscube
на Raspberry Pi 4 с подключенным дисплеем (и без X11!) заработал. Без подключенного дисплея - фиг. Поскольку нам интересен именно headless-режим, двигаемся дальше.
Среди расширений, которые есть в Mesa, довольно интересным поначалу показалось EGL_MESA_platform_surfaceless
- судя по описанию, это расширение позволяет создавать контексты, для которых нет "поверхностей", на которых происходит рисование. Это нам и не страшно - всё равно мы не собираемся ничего выводить на экран, поскольку экрана этого у нас нет.
(здесь уместно сказать, что я успел к этому моменту собрать Mesa из исходников - вдруг в репозитории всё уже десять раз протухло? Впрочем, есть подозрение, что это было излишним, и всё, что будет написано далее, прекрасно работает на той версии драйверов, которая есть в Raspbian Buster)
Итак, для использования EGL_MESA_platform_surfaceless
нам нужно использовать eglGetPlatformDisplay
вместо eglGetDisplay
. Не очень-то здорово, но ладно, пишем:
EGLDisplay dpy = eglGetPlatformDisplay(EGL_MESA_platform_surfaceless, EGL_DEFAULT_DISPLAY, NULL);
И - о чудо! У нас теперь есть display handle, работает eglInitialize
и... ну, в общем-то, всё.
Вызов eglChooseConfig
с любым attribList'ом говорит нам, что подходящих конфигураций дисплея - 0. То есть вроде как подключение к дисплею есть, а вроде как и без толку оно.
Среди сообщений, которые выдавались мне при попытке добыть surfaceless-контекст, одно было весьма любопытным: libEGL warning: no hardware driver found
. "Как так - no hardware driver, вот же он, v3d
называется," - недоумевал я. "Ну ладно, может, strace
что-то покажет?"
strace
действительно что-то показал: похоже, что libEGL пытался открывать файлы /dev/dri/card0
, /dev/dri/card1
и /dev/dri/renderD128
. Первые два отправляли libEGL читать что-то из /sys
, последнее же устройство отказалось открываться под предлогом нехватки привелегий. Оказалось, что права на эти устройства следующие:
$ ls -lah /dev/dri/
total 0
drwxr-xr-x 3 root root 120 Oct 20 11:15 .
drwxr-xr-x 17 root root 3.9K Oct 20 11:15 ..
drwxr-xr-x 2 root root 100 Oct 20 11:15 by-path
crw-rw---- 1 root video 226, 0 Oct 20 11:15 card0
crw-rw---- 1 root video 226, 1 Oct 20 11:15 card1
crw-rw---- 1 root render 226, 128 Oct 20 11:15 renderD128
Пользователь pi
уже состоял в группе video
, но для renderD128
нужна группа render
! Ну ладно, usermod -aG render $USER
, sudo reboot
, перезапуск программы - конфигов по-прежнему 0, но предупреждение о нехватке аппаратного драйвера ушло. Прогресс!
Получается, что libEGL что-то для себя находил, когда пытался открыть этот /dev/dri/renderD128
. А что будет, если попробовать руками это устройство открыть? Может, кто-то уже пробовал это сделать до меня?
Беглый поиск показал чей-то блог на igalia.com (название, которое бросилось в глаза после статьи про драйверы в блоге Raspberry Pi). Сама статья была больше про запуск compute-шейдеров в headless-режиме на абстрактной карточке, которая, тем не менее, выдавала устройство /dev/dri/renderD128
. Может, и на Raspberry Pi 4 это прокатит?
Дело за малым: пишем:
int32_t fd = open("/dev/dri/renderD128", O_RDWR);
struct gbm_device *gbm = gbm_create_device(fd);
EGLDisplay dpy = eglGetPlatformDisplay(EGL_PLATFORM_GBM_MESA, gbm, NULL);
EGLint major, minor;
eglInitialize(dpy, &major, &minor);
eglChooseConfig(display, attribList, config, MAX_NUM_CONFIG, &num_config);
/*...*/
В attribList
кладём только {EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, EGL_NONE}
. Смотрим в num_config - аж 32 конфига нам подходят! Создаём контекст с первым попавшимся, смотрим в строки OpenGL ES, видим:
Vendor: Broadcom
Version: OpenGL ES 3.1 Mesa 19.3.0-devel (git-3f8f52b241)
Renderer: V3D 4.2
Shading language version: OpenGL ES GLSL ES 3.10
Extensions: GL_EXT_blend_minmax GL_EXT_multi_draw_arrays GL_EXT_texture_format_BGRA8888 GL_OES_compressed_ETC1_RGB8_texture GL_OES_depth24 GL_OES_element_index_uint GL_OES_fbo_render_mipmap GL_OES_mapbuffer GL_OES_rgb8_rgba8 GL_OES_standard_derivatives GL_OES_stencil8 GL_OES_texture_3D GL_OES_texture_float GL_OES_texture_half_float GL_OES_texture_half_float_linear GL_OES_texture_npot GL_OES_vertex_half_float GL_EXT_texture_sRGB_decode GL_OES_EGL_image GL_OES_depth_texture GL_OES_packed_depth_stencil GL_EXT_texture_type_2_10_10_10_REV GL_OES_get_program_binary GL_APPLE_texture_max_level GL_EXT_discard_framebuffer GL_EXT_read_format_bgra GL_EXT_frag_depth GL_NV_fbo_color_attachments GL_OES_EGL_image_external GL_OES_EGL_sync GL_OES_vertex_array_object GL_EXT_occlusion_query_boolean GL_EXT_texture_rg GL_EXT_unpack_subimage GL_NV_draw_buffers GL_NV_read_buffer GL_NV_read_depth GL_NV_read_depth_stencil GL_NV_read_stencil GL_EXT_draw_buffers GL_EXT_map_buffer_range GL_KHR_debug GL_KHR_texture_compression_astc_ldr GL_OES_depth_texture_cube_map GL_OES_required_internalformat GL_OES_surfaceless_context GL_EXT_color_buffer_float GL_EXT_sRGB_write_control GL_EXT_separate_shader_objects GL_EXT_shader_implicit_conversions GL_EXT_shader_integer_mix GL_EXT_base_instance GL_EXT_compressed_ETC1_RGB8_sub_texture GL_EXT_draw_elements_base_vertex GL_EXT_primitive_bounding_box GL_EXT_shader_io_blocks GL_EXT_texture_border_clamp GL_EXT_texture_norm16 GL_KHR_context_flush_control GL_NV_image_formats GL_OES_draw_elements_base_vertex GL_OES_primitive_bounding_box GL_OES_shader_io_blocks GL_OES_texture_border_clamp GL_OES_texture_stencil8 GL_OES_texture_storage_multisample_2d_array GL_EXT_buffer_storage GL_EXT_float_blend GL_KHR_no_error GL_KHR_texture_compression_astc_sliced_3d GL_OES_EGL_image_external_essl3 GL_OES_shader_image_atomic GL_MESA_shader_integer_functions GL_KHR_parallel_shader_compile GL_MESA_framebuffer_flip_y GL_EXT_texture_query_lod
Самое главное: Renderer: V3D 4.2
! У нас есть аппаратное ускорение! Ещё и ES 3.1
, можно вообще compute shader'ы заводить (сомневаюсь, что они заработают, но сам факт очень приятен).
(с более старым драйвером вывод немного иной, представлен ниже)
Vendor: Broadcom
Version: OpenGL ES 3.0 Mesa 19.2.0-rc1
Renderer: V3D 4.2
Shading language version: OpenGL ES GLSL ES 3.00
Extensions: GL_EXT_blend_minmax GL_EXT_multi_draw_arrays GL_EXT_texture_format_BGRA8888 GL_OES_compressed_ETC1_RGB8_texture GL_OES_depth24 GL_OES_element_index_uint GL_OES_fbo_render_mipmap GL_OES_mapbuffer GL_OES_rgb8_rgba8 GL_OES_standard_derivatives GL_OES_stencil8 GL_OES_texture_3D GL_OES_texture_float GL_OES_texture_half_float GL_OES_texture_half_float_linear GL_OES_texture_npot GL_OES_vertex_half_float GL_EXT_texture_sRGB_decode GL_OES_EGL_image GL_OES_depth_texture GL_OES_packed_depth_stencil GL_EXT_texture_type_2_10_10_10_REV GL_OES_get_program_binary GL_APPLE_texture_max_level GL_EXT_discard_framebuffer GL_EXT_read_format_bgra GL_EXT_frag_depth GL_NV_fbo_color_attachments GL_OES_EGL_image_external GL_OES_EGL_sync GL_OES_vertex_array_object GL_EXT_occlusion_query_boolean GL_EXT_texture_rg GL_EXT_unpack_subimage GL_NV_draw_buffers GL_NV_read_buffer GL_NV_read_depth GL_NV_read_depth_stencil GL_NV_read_stencil GL_EXT_draw_buffers GL_EXT_map_buffer_range GL_KHR_debug GL_KHR_texture_compression_astc_ldr GL_OES_depth_texture_cube_map GL_OES_required_internalformat GL_OES_surfaceless_context GL_EXT_color_buffer_float GL_EXT_sRGB_write_control GL_EXT_separate_shader_objects GL_EXT_shader_integer_mix GL_EXT_base_instance GL_EXT_compressed_ETC1_RGB8_sub_texture GL_EXT_draw_elements_base_vertex GL_EXT_texture_border_clamp GL_KHR_context_flush_control GL_OES_draw_elements_base_vertex GL_OES_texture_border_clamp GL_OES_texture_stencil8 GL_EXT_float_blend GL_KHR_no_error GL_KHR_texture_compression_astc_sliced_3d GL_OES_EGL_image_external_essl3 GL_MESA_shader_integer_functions GL_KHR_parallel_shader_compile GL_EXT_texture_query_lod
Пример кода, который позволил мне это всё получить, лежит в моих гистах. Пока что это просто создание EGL-контекста, никаких операций с OpenGL, но это уже прогресс.
Перед запуском надо включить драйвер GL (Fake KMS)
в raspi-config
, иначе чуда не произойдёт. Магия происходит в функции do_gldriver()
и состоит в добавлении dtoverlay=vc4-fkms-v3d
в секцию [all]
файла /boot/config.txt
.
Для успешной сборки нужно линковать библиотеки libEGL
, libGLESv2
, libgbm
. ldd
на бинарнике выдаёт:
$ ldd ./egl_test_gbm
linux-vdso.so.1 (0xbef7f000)
/usr/lib/arm-linux-gnueabihf/libarmmem-${PLATFORM}.so => /usr/lib/arm-linux-gnueabihf/libarmmem-v7l.so (0xb6f81000)
libGLESv2.so.2 => /usr/local/lib/arm-linux-gnueabihf/libGLESv2.so.2 (0xb6f67000)
libEGL.so.1 => /usr/local/lib/arm-linux-gnueabihf/libEGL.so.1 (0xb6f23000)
libgbm.so.1 => /usr/local/lib/arm-linux-gnueabihf/libgbm.so.1 (0xb6f06000)
libstdc++.so.6 => /usr/lib/arm-linux-gnueabihf/libstdc++.so.6 (0xb6d98000)
libm.so.6 => /lib/arm-linux-gnueabihf/libm.so.6 (0xb6d16000)
libgcc_s.so.1 => /lib/arm-linux-gnueabihf/libgcc_s.so.1 (0xb6ce9000)
libc.so.6 => /lib/arm-linux-gnueabihf/libc.so.6 (0xb6b9b000)
libglapi.so.0 => /usr/local/lib/arm-linux-gnueabihf/libglapi.so.0 (0xb6b5c000)
libexpat.so.1 => /lib/arm-linux-gnueabihf/libexpat.so.1 (0xb6b1b000)
libX11-xcb.so.1 => /usr/lib/arm-linux-gnueabihf/libX11-xcb.so.1 (0xb6b09000)
libxcb.so.1 => /usr/lib/arm-linux-gnueabihf/libxcb.so.1 (0xb6ada000)
libxcb-dri2.so.0 => /usr/lib/arm-linux-gnueabihf/libxcb-dri2.so.0 (0xb6ac6000)
libxcb-xfixes.so.0 => /usr/lib/arm-linux-gnueabihf/libxcb-xfixes.so.0 (0xb6aaf000)
libdrm.so.2 => /usr/lib/arm-linux-gnueabihf/libdrm.so.2 (0xb6a90000)
libwayland-client.so.0 => /usr/lib/arm-linux-gnueabihf/libwayland-client.so.0 (0xb6a76000)
libwayland-server.so.0 => /usr/lib/arm-linux-gnueabihf/libwayland-server.so.0 (0xb6a59000)
libdl.so.2 => /lib/arm-linux-gnueabihf/libdl.so.2 (0xb6a46000)
libxcb-dri3.so.0 => /usr/lib/arm-linux-gnueabihf/libxcb-dri3.so.0 (0xb6a32000)
libxcb-present.so.0 => /usr/lib/arm-linux-gnueabihf/libxcb-present.so.0 (0xb6a1f000)
libxcb-sync.so.1 => /usr/lib/arm-linux-gnueabihf/libxcb-sync.so.1 (0xb6a09000)
libxshmfence.so.1 => /usr/lib/arm-linux-gnueabihf/libxshmfence.so.1 (0xb69f7000)
libpthread.so.0 => /lib/arm-linux-gnueabihf/libpthread.so.0 (0xb69cd000)
/lib/ld-linux-armhf.so.3 (0xb6f96000)
libXau.so.6 => /usr/lib/arm-linux-gnueabihf/libXau.so.6 (0xb69ba000)
libXdmcp.so.6 => /usr/lib/arm-linux-gnueabihf/libXdmcp.so.6 (0xb69a5000)
libffi.so.6 => /usr/lib/arm-linux-gnueabihf/libffi.so.6 (0xb698d000)
librt.so.1 => /lib/arm-linux-gnueabihf/librt.so.1 (0xb6976000)
libbsd.so.0 => /usr/lib/arm-linux-gnueabihf/libbsd.so.0 (0xb694e000)
(со старыми библиотеками - ниже)
$ LD_LIBRARY_PATH=/usr/lib/arm-linux-gnueabihf ldd ./egl_test_gbm
linux-vdso.so.1 (0xbefbe000)
/usr/lib/arm-linux-gnueabihf/libarmmem-${PLATFORM}.so => /usr/lib/arm-linux-gnueabihf/libarmmem-v7l.so (0xb6ee2000)
libGLESv2.so.2 => /usr/lib/arm-linux-gnueabihf/libGLESv2.so.2 (0xb6ec2000)
libEGL.so.1 => /usr/lib/arm-linux-gnueabihf/libEGL.so.1 (0xb6ea3000)
libgbm.so.1 => /usr/lib/arm-linux-gnueabihf/libgbm.so.1 (0xb6e87000)
libstdc++.so.6 => /usr/lib/arm-linux-gnueabihf/libstdc++.so.6 (0xb6d40000)
libm.so.6 => /lib/arm-linux-gnueabihf/libm.so.6 (0xb6c97000)
libgcc_s.so.1 => /lib/arm-linux-gnueabihf/libgcc_s.so.1 (0xb6c6a000)
libc.so.6 => /lib/arm-linux-gnueabihf/libc.so.6 (0xb6b1c000)
libGLdispatch.so.0 => /usr/lib/arm-linux-gnueabihf/libGLdispatch.so.0 (0xb6a8b000)
/lib/ld-linux-armhf.so.3 (0xb6ef7000)
libdl.so.2 => /lib/arm-linux-gnueabihf/libdl.so.2 (0xb6a78000)
libpthread.so.0 => /lib/arm-linux-gnueabihf/libpthread.so.0 (0xb6a4e000)
libdrm.so.2 => /usr/lib/arm-linux-gnueabihf/libdrm.so.2 (0xb6a2f000)
libwayland-server.so.0 => /usr/lib/arm-linux-gnueabihf/libwayland-server.so.0 (0xb6a12000)
libexpat.so.1 => /lib/arm-linux-gnueabihf/libexpat.so.1 (0xb69d1000)
libffi.so.6 => /usr/lib/arm-linux-gnueabihf/libffi.so.6 (0xb69b9000)
librt.so.1 => /lib/arm-linux-gnueabihf/librt.so.1 (0xb69a2000)