في هذه المقالة ، أود التحدث عن مدى سهولة تحديث قوام OpenGLES من خلال DMABUF. نظرت إلى هبر ، ولمفاجأتي ، لم أجد مقالاً واحداً حول هذا الموضوع. في هبر سؤال وجواب أيضا لم تجد أي شيء من هذا. وهذا غريب بعض الشيء بالنسبة لي. ظهرت هذه التقنية منذ فترة ، رغم أنه لا توجد معلومات كثيرة عنها على الشبكة ، إلا أنها غامضة ومتناقضة.
لقد جمعت كل هذه المعلومات شيئًا فشيئًا من مصادر مختلفة قبل أن أتمكن من كتابة مشغل فيديو مثل هذا على العرض التوضيحي أعلاه. هنا ، في عرض توضيحي ، يقوم مشغل الفيديو الخاص بي القائم على مكتبة gstreamer بتحميل إطارات الفيديو في نسيج OpenGLESv2 في كل مرة قبل التقديم. مدعوم من التوت Pi4. يتم نسخ الإطارات ببساطة إلى ذاكرة مخصصة بشكل خاص - ويقوم DMA بنقلها إلى ذاكرة GPU ، إلى النسيج. بعد ذلك ، سوف أخبرك كيف فعلت ذلك.
عادةً ما يقوم مبرمج يستخدم OpenGLESv2 بإنشاء نسيج مرة واحدة فقط ثم يعرضه على كائنات المشهد. يحدث هذا ، لأن أزياء الشخصيات نادراً ما تتغير وتعيد في بعض الأحيان إعادة تحميل النسيج باستخدام glTexSubImage2D () ليست صعبة. ومع ذلك ، تبدأ المشاكل الحقيقية عندما يكون النسيج ديناميكيًا ، عندما تحتاج إلى تحديثه كل إطار تقريبًا أثناء التقديم. وظيفة glTexSubImage2D () بطيئة للغاية. حسنًا ، كم هي بطيئة - بالطبع ، كل هذا يتوقف على الكمبيوتر وعلى بطاقة الرسومات. كنت أرغب في إيجاد حل نجح حتى في وضع البطاقات الفردية الضعيفة مثل توت العليق.
بنية العديد من أجهزة الكمبيوتر الحديثة ، بما في ذلك أجهزة الكمبيوتر ذات اللوحة المفردة 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 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 وجميع اللوحات السابقة ، لا يوجد أمل. هناك ، بشكل عام ، حتى OpenGLESv2 يتم تجريده بطريقة أو بأخرى ، من خلال مكتبات خاصة مع بروش BRCM. والآن على 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) ، ما لم يكن بالطبع يمكنك تشغيل GPU Mali على هذه اللوحات (في التجميعات الرسمية ، لم تكن هناك ، لقد قمت بتثبيته على 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 (). على سبيل المثال ، ليست في الكراك وفي الإصدار 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;
نحتاج الآن إلى إنشاء صورة EGL مرتبطة بمعالج 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 وصورة OpenGLESv2. تقوم الدالة بإرجاع مؤشر إلى الذاكرة في مساحة عنوان العملية. هناك يمكنك ببساطة الكتابة من أي مؤشر ترابط المعالج وكل التغييرات التي تظهر بمرور الوقت تظهر تلقائيًا في نسيج 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 (..) بهذا الربط فقط. ويستخدم إنشاء معرف النسيج الطبيعي 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 ، الذي يلعب ملف فيديو. أضفت appinkink عام إلى خط الأنابيب ، الذي يستقبل إطارات الفيديو. آخذ البيكسلات من إطارات الفيديو وقم ببساطة بنسخها memcpy () إلى منطقة ذاكرة DMABUF. التقديم في موضوع منفصل ، رئيسي (). لكن أود التخلص من هذه النسخة. كل نسخة شريرة. هناك حتى مثل هذا المصطلح صفر نسخة. واستنادا إلى الوثائق ، يبدو أن gstreamer نفسها يمكن أن تجعل الإطارات على الفور في DMABUF. لسوء الحظ ، لم أجد مثالًا حقيقيًا واحدًا. نظرت إلى مصادر gstreamer - هناك شيء حيال ذلك ، ولكن كيفية استخدامه بالضبط غير واضح. إذا كنت تعرف كيفية إنشاء إطارات ذات نسخ صفرية حقيقية باستخدام gstreamer في نسيج OpenGLESv2 - اكتب.
ربما النقطة الأخيرة: في مشروعي أستخدم الصور النقطية 32 بت ، وهي ليست جيدة في حالتي. سيكون من المعقول أن تأخذ YUV من gstreamer ، ثم يكون حجم إطار الفيديو أصغر بكثير ، ولكن المنطق معقد - يجب أن أقوم بـ 3 DMABUF لثلاثة مواد منفصلة بشكل منفصل Y ، U ، V. حسنًا ، التظليل معقد أيضًا ، تحتاج إلى تحويل YUV إلى ARGB الحق في تظليل.
يمكنك عرض المشروع بأكمله
على جيثب . ومع ذلك ، أعتذر مقدماً لعشاق الكود / النمط النظيف والصحيح. أعترف أنه تمت كتابته بلا مبالاة بمساعدة Google-mine-لصق.