OpenGLESv2-Texturen über DMABUF neu laden


In diesem Artikel möchte ich darüber sprechen, wie einfach es ist, OpenGLES-Texturen über DMABUF zu aktualisieren. Ich habe in Habr nachgesehen und zu meiner Überraschung keinen einzigen Artikel zu diesem Thema gefunden. In Habr Q & A habe ich auch nichts davon gefunden. Und das ist ein bisschen komisch für mich. Die Technologie erschien vor einiger Zeit, obwohl es im Netzwerk nicht viele Informationen darüber gibt, ist alles vage und widersprüchlich.

Ich habe all diese Informationen Stück für Stück aus verschiedenen Quellen zusammengetragen, bevor ich einen solchen Videoplayer wie in der obigen Demo schreiben konnte. In dieser Demo lädt mein selbst erstellter Videoplayer, der auf der gstreamer-Bibliothek basiert, jedes Mal vor dem Rendern Videobilder in die OpenGLESv2-Textur. Angetrieben von Raspberry Pi4. Frames werden einfach in einen speziell zugewiesenen Speicher kopiert - und DMA überträgt sie in den GPU-Speicher, in die Textur. Als nächstes erzähle ich Ihnen, wie ich es gemacht habe.

In der Regel erstellt ein Programmierer, der OpenGLESv2 verwendet, eine Textur nur einmal und rendert sie dann einfach in Szenenobjekte. Dies passiert, weil sich die Kostüme der Charaktere selten ändern und manchmal das Neuladen der Textur mit glTexSubImage2D () nicht schwierig ist. Die eigentlichen Probleme treten jedoch auf, wenn die Textur dynamisch ist und Sie sie während des Renderns fast bei jedem Frame aktualisieren müssen. Die Funktion glTexSubImage2D () ist sehr langsam. Nun, wie langsam - natürlich hängt alles vom Computer und von der Grafikkarte ab. Ich wollte eine Lösung finden, die auch auf schwachen Single-Board-Karten wie Raspberry funktioniert.

Die Architektur vieler moderner Computer, einschließlich SoC-Single-Board-Computern, ist derart, dass der Prozessorspeicher vom GPU-Speicher getrennt ist. Normalerweise haben Benutzerprogramme keinen direkten Zugriff auf den GPU-Speicher, und Sie müssen verschiedene API-Funktionen wie das gleiche glTexSubImage2D () verwenden. Außerdem habe ich irgendwo gelesen, dass die interne Darstellung der Textur von der herkömmlichen Darstellung von Bildern als Folge von Pixeln abweichen kann. Ich weiß nicht, wie wahr das ist. Möglicherweise.

Was bringt mir die DMABUF-Technologie? Der Speicher wird speziell zugewiesen und ein Prozess aus einem beliebigen Thread kann dort einfach Pixel schreiben, wann immer er möchte. DMA selbst überträgt alle Änderungen an der Textur im Speicher der GPU. Ist das nicht hübsch

Ich muss gleich sagen, dass ich über PBO - Pixel Buffer Object Bescheid weiß, in der Regel mit Hilfe von PBO wird die dynamische Texturaktualisierung durchgeführt, DMA scheint auch dort verwendet zu werden, aber PBO erschien nur in OpenGLESv3 und nicht in allen Implementierungen. Also nein, leider ist das nicht mein Weg.

Der Artikel könnte sowohl für Raspberry-Programmierer als auch für Spieleentwickler und wahrscheinlich sogar für Android-Programmierer von Interesse sein, da OpenGLES auch dort verwendet wird und ich sicher bin, dass diese DMABUF-Technologie auch dort vorhanden ist (zumindest bin ich sicher, dass Sie sie von Android aus verwenden können) NDK).

Ich werde ein Programm mit DMABUF auf einem Raspberry Pi4 schreiben. Das Programm sollte (und wird) auch auf normalen Intel x86 / x86_64-Computern funktionieren, etwa unter Ubuntu.

In diesem Artikel gehe ich davon aus, dass Sie bereits wissen, wie Grafiken mit der OpenGLESv2-API programmiert werden. Allerdings wird es nicht viele dieser Herausforderungen geben. Meistens werden wir ioctl Magie haben.

Als Erstes müssen Sie sicherstellen, dass die auf der Plattform verfügbare API DMABUF unterstützt. Überprüfen Sie dazu die Liste der EGL-Erweiterungen:

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"; } 

Wir werden also sofort verstehen, ob es Hoffnung gibt, DMABUF zu verwenden, oder ob es keine Hoffnung gibt. Zum Beispiel gibt es auf Raspberry Pi3 und allen vorherigen Boards keine Hoffnung. Dort wird in der Regel sogar OpenGLESv2 durch spezielle Bibliotheken mit der BRCM-Brosche reduziert. Und jetzt gibt es auf Raspberry Pi4 ein echtes OpenGLES, die Erweiterung EGL_EXT_image_dma_buf_import ist, Hurra.

Ich werde sofort notieren, welches Betriebssystem ich auf einem Single-Board-Pi4 habe, andernfalls kann es auch Probleme damit geben:

 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 

Ich stelle auch fest, dass sich die EGL_EXT_image_dma_buf_import-Erweiterung auf dem Orange Pi PC (Mali-400) / PC2 (Mali-450) befindet, es sei denn, Sie können die Mali-GPU auf diesen Boards ausführen (in offiziellen Assemblys ist sie nicht vorhanden, ich habe sie auf Armbian installiert und ich habe es selbst gemacht) Kernel-Treiber-Assembly). Das heißt, DMABUF ist fast überall. Es ist nur notwendig, zu nehmen und zu verwenden.

Als nächstes müssen Sie die Datei / dev / dri / card0 oder / dev / dri / card1 öffnen - eine davon hängt von der Plattform ab, es passiert anders, Sie müssen nach der Datei suchen, die DRM_CAP_DUMB_BUFFER unterstützt:

 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; } 

Hier gibt es übrigens eine unerklärliche Subtilität für mich. Einige Plattformen verfügen nicht über Bibliotheken, die die DRI2Authenticate () - Funktion bereitstellen. Zum Beispiel ist es nicht am Riss und in der 32-Bit-Version für Orange Pi PC. Das ist alles seltsam. Aber ich habe ein solches Repository auf GITHUB gefunden: github.com/robclark/libdri2 es kann genommen, zusammengebaut und installiert werden, dann ist alles in Ordnung. Es ist seltsam, dass es bei meinem Ubuntu 18 (64 Bit) auf einem Laptop kein Problem gibt.

Wenn Sie / dev / dri / cardX finden und öffnen könnten, könnten Sie weitermachen. Sie müssen auf die drei wichtigsten Funktionen von KHR (Khronos) zugreifen:

 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(); } 

Jetzt brauchen wir eine Funktion, die einen Speicherbereich für DMABUF erstellt. Die Funktion akzeptiert Parameter als Bitmapbreite, -höhe sowie Zeiger, auf die der DmaFd-Dateideskriptorhandler und ein Zeiger auf den Plane-Bitmapspeicher zurückgegeben werden.

 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; } 

Jetzt müssen wir ein EGL-Image erstellen, das dem DmaFd-Handler zugeordnet ist:

 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; } 

Und schließlich sind unsere Prüfungen fast vorbei, und wir müssen das EGL-Image und das OpenGLESv2-Image verknüpfen. Die Funktion gibt einen Zeiger auf den Speicher im Adressraum des Prozesses zurück. Dort können Sie einfach von jedem Prozessorthread aus schreiben und alle Änderungen im Laufe der Zeit erscheinen automatisch in der GPU-Textur über 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; } 

Die Funktion GlEGLImageTargetTexture2DOES (..) nimmt diese Bindung nur vor. Es verwendet die übliche Textur-ID-Erstellung glGenTextures (..) und ordnet sie dem zuvor erstellten esContext-> ImageKHR EGL-Bild zu. Danach kann die Textur userData-> textureV in regulären Shadern verwendet werden. Der Zeiger esContext-> Plane ist ein Zeiger auf den Bereich im Speicher, in den Sie schreiben müssen, um die Textur zu aktualisieren.

Hier ist ein Codeausschnitt, der ein Videobild kopiert:

 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; } 

Diese Funktion wird von gstreamer selbst jedes Mal aufgerufen, wenn ein neues Videobild erscheint. Wir rufen es mit gst_app_sink_pull_sample () ab. Diese Funktion hat memcpy (), das den Frame in den DMABUF-Speicher kopiert. Dann wird das frame_ready-Flag gesetzt und durch std :: condition_variable update_cv.notify_one () der gerenderte Stream aufgeweckt.

Das ist wahrscheinlich alles ...

Obwohl nein, ich lüge. Es gibt immer noch Probleme bei der Synchronisierung.

Das erste ist, dass der Prozessor in den Speicher schreibt, diese Datensätze jedoch möglicherweise im Cache des Prozessors gespeichert werden. Nach der Aufzeichnung müssen Sie einen Cache-Cache erstellen. Die zweite - es wäre nicht schlecht, genau zu wissen, wann der DMA bereits funktioniert hat und Sie können mit dem Rendern beginnen. Mal ehrlich, wenn ich mir beim ersten noch vorstelle, wie es geht, dann beim zweiten - nein. Wenn Sie Ideen haben, schreiben Sie in die Kommentare.

Und noch eine Sache. Ich benutze gstreamer, der eine Videodatei abspielt. Ich habe der Pipeline einen generischen Appsink hinzugefügt, der Videoframes empfängt. Ich nehme die Pixel aus den Videoframes und kopiere sie einfach memcpy () in den DMABUF-Speicherbereich. Das Rendern erfolgt in einem separaten Thread, main (). Aber ich möchte diese Kopie loswerden. Jede Kopie ist böse. Es gibt sogar einen solchen Begriff Nullkopie. Und nach der Dokumentation scheint es, dass gstreamer selbst Frames in DMABUF sofort rendern kann. Leider habe ich kein einziges echtes Beispiel gefunden. Ich habe mir die Quellen von gstreamer angesehen - da ist etwas dran, aber wie man es genau benutzt, ist nicht klar. Wenn Sie wissen, wie Sie mit gstreamer in OpenGLESv2-Textur echte Zero-Copy-Frames erstellen können, schreiben Sie.

Vielleicht der letzte Punkt: In meinem Projekt verwende ich 32-Bit-Bitmaps, was in meinem Fall nicht gut ist. Es wäre viel vernünftiger, YUV von gstreamer zu nehmen, dann ist die Videorahmengröße viel kleiner, aber die Logik ist kompliziert - ich müsste 3 DMABUF für drei Texturen separat ausführen. Y, U, V. Nun, der Shader ist auch kompliziert, Sie müssen YUV in ARGB konvertieren direkt im Shader.

Sie können das gesamte Projekt auf Github anzeigen . Ich entschuldige mich jedoch im Voraus bei Liebhabern von sauberem und korrektem Code / Stil. Ich gebe zu, dass es mit Hilfe von Google-Mine-Paste nachlässig geschrieben wurde.

Source: https://habr.com/ru/post/de481540/


All Articles