Rechargement des textures OpenGLESv2 via DMABUF


Dans cet article, je veux parler de la facilité de mise à jour des textures OpenGLES via DMABUF. J'ai regardé dans Habr et, à ma grande surprise, je n'ai trouvé aucun article sur ce sujet. Dans Habr Q&A n'a également rien trouvé de tout cela. Et c'est un peu étrange pour moi. La technologie est apparue il y a un certain temps, bien qu'il n'y ait vraiment pas beaucoup d'informations à ce sujet sur le réseau, toutes sont vagues et contradictoires.

J'ai collecté toutes ces informations petit à petit auprès de différentes sources avant de pouvoir écrire un tel lecteur vidéo comme sur la démo ci-dessus. Ici, sur la démo, mon lecteur vidéo self-made basé sur la bibliothèque gstreamer charge les images vidéo dans la texture OpenGLESv2 à chaque fois avant le rendu. Propulsé par Raspberry Pi4. Les images sont simplement copiées dans une mémoire spécialement allouée - et DMA les transfère vers la mémoire GPU, vers la texture. Ensuite, je vais vous dire comment je l'ai fait.

En règle générale, un programmeur utilisant OpenGLESv2 crée une texture une seule fois, puis la restitue simplement aux objets de la scène. Cela se produit, car les costumes des personnages changent rarement et recharger parfois la texture avec glTexSubImage2D () n'est pas difficile. Cependant, les vrais problèmes commencent lorsque la texture est dynamique, lorsque vous devez la mettre à jour presque chaque image pendant le rendu. La fonction glTexSubImage2D () est très lente. Eh bien, c'est lent - bien sûr, tout dépend de l'ordinateur et de la carte graphique. Je voulais trouver une solution qui fonctionnait même sur des cartes à carte unique faibles comme Raspberry.

L'architecture de nombreux ordinateurs modernes, y compris ceux à carte unique SoC, est telle que la mémoire du processeur est distincte de la mémoire du GPU. Habituellement, les programmes utilisateur n'ont pas d'accès direct à la mémoire du GPU et vous devez utiliser diverses fonctions API comme le même glTexSubImage2D (). De plus, j'ai lu quelque part que la représentation interne de la texture peut différer de la représentation traditionnelle des images comme une séquence de pixels. Je ne sais pas à quel point c'est vrai. C'est possible.

Alors qu'est-ce que la technologie DMABUF me donne? La mémoire est spécialement allouée et un processus de n'importe quel thread peut simplement y écrire des pixels quand il le souhaite. Le DMA lui-même transférera toutes les modifications apportées à la texture dans la mémoire du GPU. N'est-ce pas joli?

Je dois dire tout de suite que je connais PBO - Pixel Buffer Object, généralement avec l'aide de la mise à jour de texture dynamique PBO, DMA semble être utilisé là aussi, mais PBO est apparu uniquement dans OpenGLESv3 et pas dans toutes les implémentations. Donc non - hélas, ce n'est pas mon chemin.

L'article peut intéresser à la fois les programmeurs de framboises et les développeurs de jeux, et probablement même les programmeurs Android, car OpenGLES y est également utilisé et je suis sûr que cette technologie DMABUF y est également présente (au moins, je suis sûr que vous pouvez l'utiliser à partir d'Android NDK).

J'écrirai un programme utilisant DMABUF sur un Raspberry Pi4. Le programme devrait également (et fonctionnera) sur des ordinateurs Intel x86 / x86_64 ordinaires, par exemple sous ubuntu.

Dans cet article, je suppose que vous savez déjà programmer des graphiques avec l'API OpenGLESv2. Cependant, il n'y aura pas beaucoup de ces défis. Surtout, nous aurons la magie ioctl.

Donc, la première chose à faire est de s'assurer que l'API disponible sur la plate-forme doit prendre en charge DMABUF. Pour ce faire, consultez la liste des extensions 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"; } 

Nous comprendrons donc immédiatement s'il y a un espoir d'utiliser DMABUF ou s'il n'y a aucun espoir. Par exemple, sur Raspberry Pi3 et toutes les cartes précédentes, il n'y a aucun espoir. Là, en général, même OpenGLESv2 est en quelque sorte dépouillé, via des bibliothèques spéciales avec la broche BRCM. Et maintenant sur Raspberry Pi4 il y a un vrai OpenGLES, l'extension EGL_EXT_image_dma_buf_import est, hourra.

Je noterai tout de suite le système d'exploitation que j'ai sur un Pi4 à carte unique, sinon il peut y avoir aussi des problèmes avec ceci:

 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 

Je note également que l'extension EGL_EXT_image_dma_buf_import est sur le PC Orange Pi (Mali-400) / PC2 (Mali-450), à moins bien sûr que vous puissiez exécuter le GPU Mali sur ces cartes (dans les assemblées officielles, il n'est pas là, je l'ai installé sur Armbian, plus je l'ai fait moi-même assemblage du pilote du noyau). Autrement dit, DMABUF est presque partout. Il est seulement nécessaire de prendre et d'utiliser.

Ensuite, vous devez ouvrir le fichier / dev / dri / card0 ou / dev / dri / card1 - l'un d'eux, cela dépend de la plate-forme, cela se produit différemment, vous devez rechercher le fichier qui prend en charge 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; } 

Ici, au fait, il y a une subtilité inexplicable pour moi. Certaines plates-formes ne disposent pas de bibliothèques fournissant la fonction DRI2Authenticate (). Par exemple, il n'est pas au rendez-vous et dans la version 32 bits pour Orange Pi PC. Tout cela est étrange. Mais j'ai trouvé un tel référentiel sur GITHUB: github.com/robclark/libdri2 il peut être pris, assemblé et installé, alors tout va bien. Il est étrange que dans mon Ubuntu 18 (64 bits) sur un ordinateur portable, il n'y ait pas de problème.

Si vous pouviez trouver et ouvrir / dev / dri / cardX, vous pouvez continuer. Vous devez accéder aux trois fonctions très nécessaires de 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(); } 

Maintenant, nous avons besoin d'une fonction qui crée une zone mémoire pour DMABUF. La fonction prend des paramètres comme largeur, hauteur de bitmap, ainsi que des pointeurs vers lesquels le gestionnaire de descripteur de fichier DmaFd et un pointeur vers la mémoire bitmap du plan seront renvoyés.

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

Maintenant, nous devons créer une image EGL associée au gestionnaire 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; } 

Et enfin, nos épreuves sont presque terminées, et nous devons lier l'image EGL et l'image OpenGLESv2. La fonction renvoie un pointeur vers la mémoire dans l'espace d'adressage du processus. Là, vous pouvez simplement écrire à partir de n'importe quel thread de processeur et toutes les modifications au fil du temps apparaissent automatiquement dans la texture GPU via 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; } 

La fonction GlEGLImageTargetTexture2DOES (..) ne fait que cette liaison. Il utilise la création d'ID de texture normale glGenTextures (..) et l'associe à l'image esContext-> ImageKHR EGL précédemment créée. Après cela, la texture userData-> textureV peut être utilisée dans des shaders réguliers. Et le pointeur esContext-> Plane est un pointeur sur la zone en mémoire où vous devez écrire pour mettre à jour la texture.

Voici un extrait de code qui copie une image vidéo:

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

Cette fonction est appelée par gstreamer lui-même chaque fois qu'une nouvelle image vidéo apparaît. Nous le récupérons en utilisant gst_app_sink_pull_sample (). Cette fonction a memcpy (), qui copie la trame dans la mémoire DMABUF. Ensuite, l'indicateur frame_ready est défini et via std :: condition_variable update_cv.notify_one (), le flux qui s'affiche est réveillé.

C'est probablement tout ...

Bien que non, je mens. Il y a encore des problèmes de synchronisation.

La première est que le processeur écrit en mémoire, mais ces enregistrements peuvent se retrouver dans le cache du processeur et y être conservés, vous devez créer un cache cache après l'enregistrement. La seconde - il ne serait pas mauvais de savoir exactement quand le DMA a déjà fonctionné et vous pouvez commencer le rendu. Honnêtement, si le premier j'imagine encore comment faire, alors le second - non. Si vous avez des idées, écrivez dans les commentaires.

Et encore une chose. J'utilise gstreamer, qui lit un fichier vidéo. J'ai ajouté un appsink générique au pipeline, qui reçoit des images vidéo. Je prends les pixels des images vidéo et je les copie simplement memcpy () dans la zone mémoire DMABUF. Le rendu est dans un thread séparé, main (). Mais je voudrais me débarrasser de cette copie. Chaque copie est mauvaise. Il existe même un tel terme zéro-copie. Et à en juger par la documentation, il semble que gstreamer lui-même puisse rendre les images immédiatement dans DMABUF. Malheureusement, je n'ai trouvé aucun exemple réel. J'ai regardé les sources de gstreamer - il y a quelque chose à ce sujet, mais comment l'utiliser exactement n'est pas clair. Si vous savez comment créer de véritables cadres sans copie avec gstreamer dans la texture OpenGLESv2 - écrivez.

Peut-être le dernier point: dans mon projet, j'utilise des bitmaps 32 bits, ce qui n'est pas bon dans mon cas. Il serait beaucoup plus raisonnable de prendre YUV de gstreamer, alors la taille de l'image vidéo est beaucoup plus petite, mais la logique est compliquée - je devrais faire 3 DMABUF pour trois textures séparément Y, U, V.Eh bien, le shader est également compliqué, vous devez convertir YUV en ARGB en plein shader.

Vous pouvez visualiser l'ensemble du projet sur github . Cependant, je m'excuse à l'avance auprès des amateurs de code / style propre et correct. J'avoue qu'il a été écrit négligemment avec l'aide de Google-mine-paste.

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


All Articles