在本文中,我想谈谈通过DMABUF更新OpenGLES纹理有多么容易。 我看着哈勃(Habr),令我惊讶的是,没有找到关于该主题的一篇文章。 在哈勃问答中,Q&A也没有找到任何东西。 这对我来说有点奇怪。 该技术出现在很早以前,尽管网络上确实没有太多有关该技术的信息,但所有这些技术都是模糊且矛盾的。
在像上面的演示中那样编写这样的视频播放器之前,我从不同的来源逐步收集了所有这些信息。 在这里,在一个演示中,我基于gstreamer库的自制视频播放器在渲染之前每次都将视频帧加载到OpenGLESv2纹理中。 由Raspberry Pi4驱动。 只需将帧复制到专门分配的内存中,然后DMA将其传输到GPU内存和纹理中。 接下来,我将告诉您我是如何做到的。
通常,使用OpenGLESv2的程序员仅创建一次纹理,然后将其渲染到场景对象即可。 发生这种情况是因为角色的服装很少更改,有时使用glTexSubImage2D()重新加载纹理并不困难。 但是,真正的问题始于纹理是动态的,当您需要在渲染期间几乎每帧更新它时。 glTexSubImage2D()函数非常慢。 好吧,速度有多慢-当然,这完全取决于计算机和图形卡。 我想找到一种解决方案,即使在像Raspberry这样的弱单板卡上也可以使用。
许多现代计算机(包括SoC单板计算机)的体系结构都使得处理器内存与GPU内存分离。 通常,用户程序无法直接访问GPU内存,因此您需要使用各种API函数,例如相同的glTexSubImage2D()。 此外,我在某处读到,纹理的内部表示形式可能与传统的图片表示形式(像素序列)有所不同。 我不知道这是真的。 可能吧
那么DMABUF技术给我带来了什么? 内存是专门分配的,任何线程的进程都可以随时在其中写入像素。 DMA本身会将所有更改转移到GPU内存中的纹理。 那不是很漂亮吗?
我必须马上说出我对PBO-像素缓冲区对象的了解,通常是在PBO的帮助下完成了动态纹理更新,似乎也在那里使用了DMA,但是PBO仅出现在OpenGLESv3中,而不是出现在所有实现中。 所以不,a,这不是我的方式。
Raspberry程序员和游戏开发人员甚至Android程序员都可能对本文感兴趣,因为在那里也使用了OpenGLES,而且我敢肯定,那里也有这种DMABUF技术(至少我敢肯定,您可以在Android上使用它) NDK)。
我将在Raspberry Pi4上使用DMABUF编写程序。 该程序还应该(并且将会)在普通的Intel x86 / x86_64计算机上运行,例如在ubuntu下。
在本文中,我假设您已经知道如何使用OpenGLESv2 API对图形进行编程。 虽然,这些挑战不会很多。 通常,我们会拥有ioctl魔术。
因此,首先要做的是确保平台上可用的API必须支持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和以前的所有板上,都没有希望。 通常,通过带有BRCM胸针的特殊库,甚至在某种程度上甚至剥离了OpenGLESv2。 现在在Raspberry Pi4上有一个真正的OpenGLES,扩展名为EGL_EXT_image_dma_buf_import是hooray。
我会立即记下单板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";
顺便说一下,这对我来说是莫名其妙的微妙之处。 某些平台没有提供DRI2Authenticate()函数的库。 例如,对于Orange Pi PC而言,它并不是处于最佳状态,而是32位版本。 这一切都很奇怪。 但是我在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文件描述符处理程序的指针和指向平面位图内存的指针。
nt CreateDmaBuf( int Width, int Height, int* DmaFd, void** Plane ) { int dmaFd = *DmaFd = 0; void* pplane = *Plane = nullptr;
现在,我们需要创建一个与DmaFd处理程序关联的EGL图像:
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图像和OpenGLESv2图像链接起来。 该函数返回指向进程地址空间中的内存的指针。 在那里,您可以轻松地从任何处理器线程进行写入,并且随着时间的推移,所有更改都会通过DMABUF自动显示在GPU纹理中。
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图像相关联。 之后,可以在常规着色器中使用纹理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。 我在管道中添加了一个通用的appink,用于接收视频帧。 我从视频帧中获取像素,然后将它们简单地将memcpy()复制到DMABUF存储区。 呈现在单独的线程main()中。 但我想摆脱这个副本。 每个副本都是邪恶的。 甚至有一个术语“零复制”。 从文档来看,似乎gstreamer本身可以立即在DMABUF中渲染帧。 不幸的是,我还没有找到一个真实的例子。 我查看了gstreamer的来源-关于它的内容,但是如何确切地使用它尚不清楚。 如果您知道如何在OpenGLESv2纹理中使用gstreamer制作真正的零拷贝帧,请编写。
也许最后一点:在我的项目中,我使用32位位图,这对我而言不是很好。 从gstreamer中获取YUV会更合理,然后视频帧的大小要小得多,但是逻辑很复杂-我必须分别对三个纹理Y,U,V进行3个DMABUF。嗯,着色器也很复杂,您需要将YUV转换为ARGB在着色器中。
您可以
在github上查看整个项目。 但是,我预先向干净和正确的代码/样式的爱好者致歉。 我承认这是在Google矿山粘贴的帮助下粗心编写的。