Pull to refresh

Перезагрузка текстур OpenGLESv2 через DMABUF

Reading time9 min
Views3.5K

В этой статье я хочу рассказать, как просто можно обновлять текстуры OpenGLES через DMABUF. Поискал по Хабру и к своему удивлению не обнаружил ни одной статьи на эту тему. В Хабр Q&A тоже ничего такого не нашел. И это для меня немного странно. Технология появилась довольно давно, хотя информации о ней действительно в сети не много, вся она расплывчатая и противоречивая.

Я всю эту информацию собирал по крупицам из разных источников, прежде чем смог написать вот такой видео плеер, как на демке выше. Здесь, на демке, мой самописный видео плеер, основанный на библиотеке gstreamer, загружает видео кадры в текстуру OpenGLESv2 каждый раз перед рендерингом. Работает на Raspberry Pi4. Кадры просто копируются в специальным образом выделенную память — а уж DMA переносит их в память GPU, в текстуру. Далее расскажу, как я это делал.

Обычно программист использующий OpenGLESv2 создает текстуру только однажды и потом просто рендерит ее на объекты сцены. Так и бывает, ведь костюмы у персонажей меняются редко и иногда перезагрузить текстуру с помощью glTexSubImage2D() не трудно. Однако, настоящие проблемы начинаются, когда текстура динамичная, когда нужно обновлять ее чуть ли не каждый кадр во время рендеринга. Функция glTexSubImage2D() работает очень медленно. Ну как медленно — конечно, все зависит от компьютера и от графической карты. Я хотел найти такое решение, чтобы работало даже на слабых одноплатниках вроде Raspberry.

Архитектура многих современных компьютеров, в том числе одноплатников на SoC, такова, что память процессора отделена от памяти GPU. Обычно у пользовательских программ нет прямого доступа к памяти GPU и нужно пользоваться различными функциями API вроде той же glTexSubImage2D(). Причем, где-то читал, что внутреннее представление текстуры может отличаться от традиционного представления картинок в виде последовательности пикселей. Уж не знаю насколько это правда. Возможно.

Итак, что дает мне технология DMABUF? Специальным образом выделяется память и процесс из любого потока может туда просто писать пиксели когда ему захочется. DMA само будет переносить все изменения в текстуру находящуюся в памяти GPU. Ну разве это не прелестно?

Сразу скажу, что я знаю про PBO — Pixel Buffer Object, обычно с помощью PBO делается динамическое обновление текстур, там так же вроде бы используется DMA, но PBO появилось только в OpenGLESv3 и далеко не во всех реализациях. Так что нет — увы, это не мой путь.

Статья может быть интересна как программистам для Raspberry, так и разработчикам игр и наверное даже программистам Android, так как и там используется OpenGLES и я уверен, что эта технология DMABUF там так же присутствует (по крайней мере я уверен, что можно ею пользоваться из Android NDK).

Я буду писать программу использующую DMABUF на Raspberry Pi4. Программа так же должна (и будет) работать на обычных Intel компьютерах x86/x86_64 скажем под убунтой.

В этой статье я предполагаю, что вы уже знаете, как программировать графику с АПИ OpenGLESv2. Хотя, тут этих вызовов будет не очень много. В основном у нас будет магия ioctl.

Итак, первое, что нужно сделать — нужно убедиться, что имеющееся на платформе АПИ должно поддерживать DMABUF. Для этого нужно проверить список расширений EGL:

char* EglExtString = (char*)eglQueryString( esContext->eglDisplay, EGL_EXTENSIONS );
if( strstr( EglExtString, "EGL_EXT_image_dma_buf_import") )
{
	cout << "DMA_BUF feature must be supported!!!\n";
}

Так мы сразу поймем, есть ли надежда использовать DMABUF или надежды нет. Вот к примеру на Raspberry Pi3 и всех предыдущих платах надежды нет. Там вообще даже OpenGLESv2 какой-то урезанный, через специальные библиотеки броадком BRCM. А вот уже на Raspberry Pi4 — настоящий OpenGLES, расширение EGL_EXT_image_dma_buf_import есть, ура.

Сразу отмечу, какая у меня ОС стоит на одноплатнике Pi4, а то с этим так же могут быть проблемы:

pi@raspberrypi:~ $ lsb_release -a
No LSB modules are available.
Distributor ID: Raspbian
Description:    Raspbian GNU/Linux 10 (buster)
Release:        10
Codename:       buster
pi@raspberrypi:~ $ uname -a
Linux raspberrypi 4.19.75-v7l+ #1270 SMP Tue Sep 24 18:51:41 BST 2019 armv7l GNU/Linux

Так же отмечу, что расширение EGL_EXT_image_dma_buf_import есть на Orange Pi PC (Mali-400)/PC2 (Mali-450), если конечно вы сможете запустить на этих платах Mali GPU (в официальных сборках его нет, я ставил на Armbian, плюс сам делал сборку драйвера ядра). То есть DMABUF — есть почти везде. Нужно только брать и пользоваться.

Дальше нужно открыть файл /dev/dri/card0 или /dev/dri/card1 — какой-то из них, тут зависит от платформы, бывает по разному, нужно искать тот файл, который поддерживает DRM_CAP_DUMB_BUFFER:

int OpenDrm()
{
	int fd = open("/dev/dri/card0", O_RDWR | O_CLOEXEC);
	if( fd < 0 )
	{
		cout << "cannot open /dev/dri/card0\n";
		return -1;
	}

	uint64_t hasDumb = 0;
	if( drmGetCap(fd, DRM_CAP_DUMB_BUFFER, &hasDumb) < 0 )
	{
		close( fd );
		cout << "/dev/dri/card0 has no support for DUMB_BUFFER\n";

		//maybe Raspberry Pi4 or other platform
		fd = open("/dev/dri/card1", O_RDWR | O_CLOEXEC);
		if( fd < 0 )
		{
			cout << "cannot open /dev/dri/card1\n";
			return -1;
		}

		hasDumb = 0;
		if( drmGetCap(fd, DRM_CAP_DUMB_BUFFER, &hasDumb) < 0 )
		{
			close( fd );
			cout << "/dev/dri/card1 has no support for DUMB_BUFFER\n";
			return -1;
		}
	}

	if( !hasDumb )
	{
		close( fd );
		cout << "no support for DUMB_BUFFER\n";
		return -1;
	}

	//Get DRM authorization
	drm_magic_t magic;
	if( drmGetMagic(fd, &magic) )
	{
		cout << "no DRM magic\n";
		close( fd );
		return -1;
	}

	Window root = DefaultRootWindow( x_display );
	if( !DRI2Authenticate( x_display, root, magic ) )
	{
		close( fd );
		cout << "Failed DRI2Authenticate\n";
		return -1;
	}
	cout << "DRM fd "<< fd <<"\n";
	return fd;
}

Тут кстати есть необъяснимая для меня тонкая тонкость. В некоторых платформах нет библиотек, которые давали бы функцию DRI2Authenticate(). Ее к примеру нет на распберри и в 32х битной версии для Orange Pi PC. Странно все это. Но я нашел такой репозиторий на GITHUB: github.com/robclark/libdri2 его можно взять, собрать и поставить, тогда все ок. Странно, что в моей Ubuntu 18 (64х битная) на ноутбуке с этим проблем нет.

Если смогли найти и открыть /dev/dri/cardX можно двигаться дальше. Нужно получить доступ к трем очень нужным функциям KHR (Khronos):

PFNEGLCREATEIMAGEKHRPROC  funcEglCreateImageKHR = nullptr;
PFNEGLDESTROYIMAGEKHRPROC funcEglDestroyImageKHR = nullptr;
PFNGLEGLIMAGETARGETTEXTURE2DOESPROC funcGlEGLImageTargetTexture2DOES = nullptr;

...
funcEglCreateImageKHR = (PFNEGLCREATEIMAGEKHRPROC) eglGetProcAddress("eglCreateImageKHR");
funcEglDestroyImageKHR = (PFNEGLDESTROYIMAGEKHRPROC) eglGetProcAddress("eglDestroyImageKHR");
funcGlEGLImageTargetTexture2DOES = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES");
if( funcEglCreateImageKHR && funcEglDestroyImageKHR && funcGlEGLImageTargetTexture2DOES )
{
	cout << "DMA_BUF feature supported!!!\n";
}
else
{
	CloseDrm();
}

Теперь нужна функция, которая создает область памяти для DMABUF. Функция принимает параметрами ширину битмапа, высоту, а так же указатели по которым будет возвращен хандлер файлового дескриптора DmaFd и указатель на память битмапа Plane.

nt CreateDmaBuf( int Width, int Height, int* DmaFd, void** Plane )
{
	int dmaFd = *DmaFd = 0;
	void* pplane = *Plane = nullptr;

	// Create dumb buffer
	drm_mode_create_dumb buffer = { 0 };
	buffer.width = Width;
	buffer.height = Height;
	buffer.handle = 0;
	buffer.bpp = 32; //Bits per pixel
	buffer.flags = 0;

	int ret = drmIoctl( DriCardFd, DRM_IOCTL_MODE_CREATE_DUMB, &buffer);
	cout << "DRM_IOCTL_MODE_CREATE_DUMB " << buffer.handle << " " << ret << "\n";
	if (ret < 0)
	{
		cout << "Error cannot DRM_IOCTL_MODE_CREATE_DUMB\n";
		return -1;
	}

	// Get the dmabuf for the buffer
	drm_prime_handle prime;
	memset(&prime, 0, sizeof prime);
	prime.handle = buffer.handle;
	prime.flags = /*DRM_CLOEXEC |*/ DRM_RDWR;

	ret = drmIoctl( DriCardFd, DRM_IOCTL_PRIME_HANDLE_TO_FD, &prime);
	if (ret < 0)
	{
		cout << "Error cannot DRM_IOCTL_PRIME_HANDLE_TO_FD " << errno << " " << ret <<"\n";
		return -1;
	}
	dmaFd = prime.fd;
	// Map the buffer to userspace
	int Bpp = 32;
	pplane = mmap(NULL, Width*Height*Bpp/8, PROT_READ | PROT_WRITE, MAP_SHARED, dmaFd, 0);
	if( pplane == MAP_FAILED )
	{
		cout << "Error cannot mmap\n";
		return -1;
	}

	//return valid values
	*DmaFd = dmaFd;
	*Plane = pplane;
	cout << "DMABUF created "<< dmaFd << " " << (void*)Plane <<"\n";
	return 0;
}

Теперь нужно создать EGL image связанный с хандлером DmaFd:

int CreateDmaBufferImage( ESContext* esContext, int Width, int Height, int* DmaFd, void** Plane, EGLImageKHR* Image )
{
	int dmaFd = 0;
	void* planePtr = nullptr;

	int Bpp = 32;
	int ret0 = CreateDmaBuf( Width, Height, &dmaFd, &planePtr );
	if( ret0<0 )
		return -1;

	EGLint img_attrs[] = {
		EGL_WIDTH, Width,
		EGL_HEIGHT, Height,
		EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_ABGR8888,
		EGL_DMA_BUF_PLANE0_FD_EXT, dmaFd,
		EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0,
		EGL_DMA_BUF_PLANE0_PITCH_EXT, Width * Bpp / 8,
		EGL_NONE
	};

	EGLImageKHR image = funcEglCreateImageKHR( esContext->eglDisplay, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, 0, &img_attrs[0] );

	*Plane = planePtr;
	*DmaFd  = dmaFd;
	*Image = image;
	cout << "DMA_BUF pointer " << (void*)planePtr << "\n";
	cout << "DMA_BUF fd " << (int)dmaFd << "\n";
	cout << "EGLImageKHR " << image << "\n";
	return 0;
}

Ну и, наконец, наши мытарства почти окончены, и мы должны связать EGL image и OpenGLESv2 image. Функция возвращает указатель на память в адресном пространстве процесса. Туда можно просто писать из любого потока процессора и все изменения со временем автоматически оказываются в текстуре GPU через DMABUF.

void* CreateVideoTexture( ESContext* esContext, int Width, int Height )
{
	CreateDmaBufferImage( esContext, Width, Height, &esContext->DmaFd, &esContext->Plane, &esContext->ImageKHR );
	GLuint texId;
	glGenTextures ( 1, &texId );
	glBindTexture ( GL_TEXTURE_2D, texId );
	glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
	glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
	glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
	glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
	funcGlEGLImageTargetTexture2DOES(GL_TEXTURE_2D, esContext->ImageKHR );
	checkGlError( __LINE__ );
	UserData *userData = (UserData*)esContext->userData;
	userData->textureV = texId;
	userData->textureV_ready = true;
	return esContext->Plane;
}

Функция GlEGLImageTargetTexture2DOES(..) как раз и делает это связывание. Она использует обычное создание id текстуры glGenTextures(..) и связывает ее с ранее созданной esContext->ImageKHR EGL image. После этого текстуру userData->textureV можно использовать в обычных шейдерах. А указатель esContext->Plane и есть указатель на область в памяти куда нужно писать для обновления текстуры.

Приведу фрагмент кода, который производит копирование видео кадра:

GstFlowReturn on_new_sample( GstAppSink *pAppsink, gpointer pParam )
{
	GstFlowReturn ret = GST_FLOW_OK;
	GstSample *Sample = gst_app_sink_pull_sample(pAppsink);
	if( Sample )
	{
		if( VideoWidth==0 || VideoHeight==0 )
		{
			GstCaps* caps = gst_sample_get_caps( Sample );
			GstStructure* structure = gst_caps_get_structure (caps, 0);
			gst_structure_get_int (structure, "width", &VideoWidth);
			gst_structure_get_int (structure, "height", &VideoHeight);
			cout << "Stream Resolution " << VideoWidth << " " << VideoHeight << "\n";
		}

		GstBuffer *Buffer = gst_sample_get_buffer( Sample );
		if( Buffer )
		{
			GstMapInfo MapInfo;
			memset(&MapInfo, 0, sizeof(MapInfo));
			gboolean Mapped = gst_buffer_map( Buffer, &MapInfo, GST_MAP_READ );
			if( Mapped )
			{
				if( dmabuf_ptr )
					memcpy( dmabuf_ptr, MapInfo.data, MapInfo.size );
				gst_buffer_unmap( Buffer, &MapInfo);
				frame_ready = true;
				update_cv.notify_one();
			}
		}
		gst_sample_unref( Sample );
	}
	return ret;
}

Эта функция вызывается самим gstreamer каждый раз, когда появляется новый видео кадр. Мы его извлекаем с помощью gst_app_sink_pull_sample(). В этой функции есть memcpy(), которая копирует кадр в память DMABUF. Затем устанавливается флаг frame_ready и через std::condition_variable update_cv.notify_one() пробуждается поток, который занимается рендерингом.

Вот пожалуй и все…

Хотя нет, вру. Еще остаются вопросы синхронизации.

Первое — процессор пишет в память, но эти записи могут оказаться в кэше процессора и подзадержаться там, нужно после записи сделать флуш кэша. Второе — было бы не плохо точно знать, когда DMA уже отработало и можно начинать рендерить. Честно говоря, если первое я еще представляю, как сделать, то второе — нет. Если у вас есть идеи — напишите в комментариях.

И еще один момент. Я использую gstreamer, который воспроизводит видеофайл. Я добавил в pipeline самописный appsink, который получает видео кадры. Я беру пикселы из видео кадров и просто копирую их memcpy() в область памяти DMABUF. Рендеринг идет в отдельном потоке, main(). Но вот хотелось бы избавиться и от этого копирования. Каждое копирование — это зло. Есть даже такой термин zero-copy. Причем судя по документации вроде бы сам gstreamer может отдавать кадры сразу в DMABUF. К сожалению ни одного реального примера я не нашел. Смотрел исходники gstreamer — что-то про это есть, но как этим точно пользоваться не понятно. Если знаете, как сделать настоящий zero-copy кадров с gstreamer в текстуру OpenGLESv2 — напишите.

Пожалуй последнее замечание: в моем проекте я использую 32х битные битмапы, что конкретно в моем случае не есть хорошо. Гораздо разумнее было бы из gstreamer забирать YUV, тогда объем видеокадра получается существенно меньше, но усложняется логика — мне пришлось бы делать 3 DMABUF для трех текстур по отдельности Y, U, V. Ну и шейдер так же усложняется, нужно будет YUV конвертировать в ARGB прямо в шейдере.

Посмотреть весь проект можно на github. Однако, заранее прошу прощения у любителей чистого и правильного кода /стиля. Признаю, что писалось небрежно с помощью гугло-копи-пасты.
Tags:
Hubs:
+12
Comments13

Articles

Change theme settings