Memuat ulang tekstur OpenGLESv2 melalui DMABUF


Pada artikel ini saya ingin berbicara tentang betapa mudahnya memperbarui tekstur OpenGLES melalui DMABUF. Saya mencari di Habr dan, yang mengejutkan saya, tidak menemukan satu artikel pun tentang topik ini. Dalam Habr, T&J juga tidak menemukan ini. Dan ini agak aneh bagi saya. Teknologi ini muncul beberapa waktu yang lalu, meskipun sebenarnya tidak banyak informasi tentang itu di jaringan, semua itu tidak jelas dan kontradiktif.

Saya mengumpulkan semua informasi ini sedikit demi sedikit dari sumber yang berbeda sebelum saya dapat menulis pemutar video seperti pada demo di atas. Di sini, pada sebuah demo, pemutar video buatan saya berdasarkan pustaka gstreamer memuat bingkai video ke dalam tekstur OpenGLESv2 setiap kali sebelum rendering. Didukung oleh Raspberry Pi4. Frame hanya disalin ke memori yang dialokasikan khusus - dan DMA mentransfernya ke memori GPU, ke tekstur. Selanjutnya, saya akan memberi tahu Anda bagaimana saya melakukannya.

Biasanya, seorang programmer yang menggunakan OpenGLESv2 menciptakan tekstur hanya sekali dan kemudian membuatnya menjadi objek adegan. Ini terjadi, karena kostum karakter jarang berubah dan terkadang memuat ulang tekstur dengan glTexSubImage2D () tidak sulit. Namun, masalah sebenarnya dimulai ketika teksturnya dinamis, ketika Anda perlu memperbarui hampir setiap frame selama rendering. Fungsi glTexSubImage2D () sangat lambat. Nah, seberapa lambat - tentu saja, semua itu tergantung pada komputer dan kartu grafis. Saya ingin menemukan solusi yang berfungsi bahkan pada kartu papan tunggal yang lemah seperti Raspberry.

Arsitektur banyak komputer modern, termasuk SoC single-board, sedemikian rupa sehingga memori prosesor terpisah dari memori GPU. Biasanya, program pengguna tidak memiliki akses langsung ke memori GPU dan Anda perlu menggunakan berbagai fungsi API seperti glTexSubImage2D () yang sama. Selain itu, saya membaca di suatu tempat bahwa representasi internal tekstur mungkin berbeda dari representasi tradisional gambar sebagai urutan piksel. Saya tidak tahu seberapa benar ini. Mungkin

Jadi apa yang diberikan teknologi DMABUF kepada saya? Memori dialokasikan secara khusus dan sebuah proses dari utas apa saja dapat menulis piksel di sana kapan pun diinginkan. DMA sendiri akan mentransfer semua perubahan pada tekstur di memori GPU. Bukankah itu cantik?

Saya harus segera mengatakan bahwa saya tahu tentang PBO - Pixel Buffer Object, biasanya dengan bantuan PBO, pembaruan tekstur dinamis dilakukan, DMA tampaknya juga digunakan di sana, tetapi PBO hanya muncul di OpenGLESv3 dan tidak di semua implementasi. Jadi tidak - sayangnya, ini bukan cara saya.

Artikel ini mungkin menarik bagi programmer Raspberry dan pengembang game, dan mungkin bahkan programmer Android, karena OpenGLES juga digunakan di sana dan saya yakin bahwa teknologi DMABUF ini juga ada di sana (setidaknya saya yakin Anda dapat menggunakannya dari Android NDK).

Saya akan menulis sebuah program menggunakan DMABUF pada Raspberry Pi4. Program ini juga harus (dan akan) bekerja pada komputer Intel x86 / x86_64 biasa, katakan di bawah ubuntu.

Pada artikel ini, saya berasumsi bahwa Anda sudah tahu cara memprogram grafik dengan OpenGLESv2 API. Meskipun, tidak akan banyak tantangan ini. Sebagian besar kita akan memiliki sihir ioctl.

Jadi, hal pertama yang harus dilakukan adalah memastikan bahwa API yang tersedia di platform harus mendukung DMABUF. Untuk melakukan ini, periksa daftar ekstensi 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"; } 

Jadi kita akan segera mengerti jika ada harapan untuk menggunakan DMABUF atau jika tidak ada harapan. Misalnya, di Raspberry Pi3 dan semua papan sebelumnya tidak ada harapan. Di sana, secara umum, bahkan OpenGLESv2 entah bagaimana dilucuti, melalui perpustakaan khusus dengan bros BRCM. Dan sekarang di Raspberry Pi4 ada OpenGLES nyata, ekstensi EGL_EXT_image_dma_buf_import adalah, hore.

Saya akan segera mencatat OS apa yang saya miliki pada Pi4 papan tunggal, jika tidak, mungkin ada masalah dengan ini:

 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 

Saya juga mencatat bahwa ekstensi EGL_EXT_image_dma_buf_import ada di Orange Pi PC (Mali-400) / PC2 (Mali-450), kecuali tentu saja Anda dapat menjalankan GPU Mali di papan ini (di majelis resmi tidak ada di sana, saya menginstalnya di Armbian, ditambah saya melakukannya sendiri di Armbian, plus saya melakukannya sendiri rakitan driver kernel). Artinya, DMABUF hampir di mana-mana. Hanya perlu untuk mengambil dan menggunakan.

Selanjutnya, Anda perlu membuka file / dev / dri / card0 atau / dev / dri / card1 - salah satunya, itu tergantung pada platform, itu terjadi secara berbeda, Anda perlu mencari file yang mendukung 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; } 

Di sini omong-omong ada kehalusan yang tak bisa dijelaskan untuk saya. Beberapa platform tidak memiliki perpustakaan yang menyediakan fungsi DRI2Authenticate (). Sebagai contoh, itu tidak di celah dan dalam versi 32-bit untuk Orange Pi PC. Semua ini aneh. Tapi saya menemukan repositori di GITHUB: github.com/robclark/libdri2 dapat diambil, dirakit dan diinstal, maka semuanya baik-baik saja. Sungguh aneh bahwa di Ubuntu 18 (64 bit) saya di laptop tidak ada masalah.

Jika Anda dapat menemukan dan membuka / dev / dri / cardX Anda dapat melanjutkan. Anda perlu mengakses tiga fungsi yang sangat penting dari 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(); } 

Sekarang kita membutuhkan fungsi yang menciptakan area memori untuk DMABUF. Fungsi mengambil parameter sebagai lebar bitmap, tinggi, serta pointer ke mana penangan deskriptor file DmaFd dan pointer ke memori bitmap Plane akan dikembalikan.

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

Sekarang kita perlu membuat gambar EGL yang terkait dengan penangan 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; } 

Dan akhirnya, cobaan kita hampir berakhir, dan kita harus menautkan gambar EGL dan gambar OpenGLESv2. Fungsi mengembalikan pointer ke memori di ruang alamat proses. Di sana Anda cukup menulis dari utas prosesor apa pun dan semua perubahan dari waktu ke waktu secara otomatis muncul dalam tekstur GPU melalui 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; } 

Fungsi GlEGLImageTargetTexture2DOES (..) hanya melakukan pengikatan ini. Ini menggunakan id pembuatan tekstur normal glGenTextures (..) dan menghubungkannya dengan esContext-> ImageKHR EGL gambar yang dibuat sebelumnya. Setelah itu, tekstur userData-> teksturV dapat digunakan dalam shader biasa. Dan pointer esContext-> Plane adalah pointer ke area di memori di mana Anda perlu menulis untuk memperbarui tekstur.

Berikut ini cuplikan kode yang menyalin bingkai video:

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

Fungsi ini disebut oleh gstreamer sendiri setiap kali bingkai video baru muncul. Kami mengambilnya menggunakan gst_app_sink_pull_sample (). Fungsi ini memiliki memcpy (), yang menyalin bingkai ke memori DMABUF. Kemudian flag frame_ready diatur dan melalui std :: condition_variable update_cv.notify_one (), aliran yang dirender dibangunkan.

Itu mungkin saja ...

Meski tidak, saya bohong. Masih ada masalah sinkronisasi.

Yang pertama adalah bahwa prosesor menulis ke memori, tetapi catatan ini mungkin berakhir di cache prosesor dan ditahan di sana, Anda harus membuat cache cache setelah merekam. Yang kedua - tidak akan buruk untuk mengetahui secara pasti kapan DMA telah bekerja dan Anda dapat mulai render. Jujur, jika yang pertama saya masih membayangkan bagaimana melakukannya, maka yang kedua - tidak. Jika Anda punya ide, tulis di komentar.

Dan satu hal lagi. Saya menggunakan gstreamer, yang memutar file video. Saya menambahkan tautan aplikasi generik ke pipa, yang menerima bingkai video. Saya mengambil piksel dari bingkai video dan cukup menyalinnya memcpy () ke area memori DMABUF. Rendering ada di utas terpisah, main (). Tetapi saya ingin menyingkirkan salinan ini. Setiap salinan itu jahat. Bahkan ada istilah zero-copy. Dan dilihat dari dokumentasi, tampaknya gstreamer itu sendiri dapat membuat frame langsung di DMABUF. Sayangnya, saya belum menemukan satu pun contoh nyata. Saya melihat sumber-sumber gstreamer - ada sesuatu tentangnya, tetapi bagaimana menggunakannya sebenarnya tidak jelas. Jika Anda tahu cara membuat frame zero-copy nyata dengan gstreamer dalam tekstur OpenGLESv2 - tulis.

Mungkin poin terakhir: dalam proyek saya, saya menggunakan bitmap 32-bit, yang tidak bagus dalam kasus saya. Akan jauh lebih masuk akal untuk mengambil YUV dari gstreamer, maka ukuran bingkai videonya jauh lebih kecil, tetapi logikanya rumit - saya harus melakukan 3 DMABUF untuk tiga tekstur secara terpisah Y, U, V. Nah, shader juga rumit, Anda perlu mengonversi YUV ke ARGB tepat di shader.

Anda dapat melihat keseluruhan proyek di github . Namun, saya mohon maaf sebelumnya kepada pecinta kode / gaya yang bersih dan benar. Saya akui itu ditulis dengan sembrono dengan bantuan Google-mine-paste.

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


All Articles