تقديم التحسين للجوال

مرحبا عزيزي القارئ ، عشاق ومحترفي برمجة الرسومات! نلفت انتباهك إلى سلسلة من المقالات المكرسة لتحسين العرض للأجهزة المحمولة: الهواتف والأجهزة اللوحية على أساس iOS و Android. تتكون الدورة من ثلاثة أجزاء. في الجزء الأول ، سنفحص ميزات بنية بلاط GPU الشائعة على الهاتف المحمول . في الثانية ، سنستعرض العائلات الرئيسية لوحدات معالجة الرسومات المقدمة في الأجهزة الحديثة ، وننظر في نقاط القوة والضعف فيها. في الجزء الثالث ، سوف نتعرف على ميزات تحسين تظليل.

لذا ، دعونا ننزل إلى الجزء الأول.

تم تطوير بطاقات الفيديو على أجهزة سطح المكتب ووحدات التحكم في غياب قيود كبيرة على استهلاك الطاقة. مع ظهور بطاقات الفيديو للأجهزة المحمولة ، واجه المهندسون مهمة ضمان الأداء المقبول في دقة سطح المكتب القابلة للمقارنة ، في حين أن استهلاك الطاقة لبطاقات الفيديو هذه يجب أن يكون أقل بمقدار مرتين. 



تم العثور على الحل في بنية خاصة تسمى Tile Based Rendering (TBR) . بالنسبة لمبرمج الرسوميات ذو الخبرة في تطوير الكمبيوتر الشخصي ، عندما يتعرف على تطوير الأجهزة المحمولة ، يبدو كل شيء مألوفًا: يتم استخدام OpenGL ES API مماثل ، وهو نفس هيكل خط الرسومات. ومع ذلك ، فإن بنية تجانب وحدات معالجة الرسومات المحمولة تختلف اختلافًا كبيرًا عن تلك المستخدمة في وحدات تحكم الكمبيوتر / الوضع الفوري . ستساعدك معرفة نقاط القوة والضعف في TBR على اتخاذ القرارات الصحيحة والحصول على أداء رائع مع الهاتف المحمول.

فيما يلي رسم بياني مبسط لخط أنابيب الرسومات الكلاسيكية المستخدمة على أجهزة الكمبيوتر وأجهزة التحكم للعقد الثالث.


في مرحلة المعالجة الهندسية ، تتم قراءة سمات القمة من ذاكرة فيديو GPU. بعد العديد من التحولات (Vertex Shader) ، يتم تمرير البدائية الجاهزة للتسليم بالترتيب الأصلي (FIFO) إلى أداة التنقيط ، والتي تقسم البدائية إلى وحدات بكسل. بعد ذلك ، يتم تنفيذ خطوة معالجة الأجزاء لكل بكسل (Fragment Shader) ، وتتم كتابة قيم الألوان التي تم الحصول عليها على ذاكرة التخزين المؤقت للشاشة ، والتي تقع أيضًا في ذاكرة الفيديو. ميزة العمارة التقليدية لـ "الوضع الفوري" هي تسجيل نتيجة Fragment Shader في أقسام عشوائية من ذاكرة التخزين المؤقت للشاشة عند معالجة مكالمة سحب واحدة. وبالتالي ، قد يلزم الوصول إلى المخزن المؤقت للشاشة بالكامل لكل مكالمة سحب. يتطلب العمل مع مجموعة كبيرة من الذاكرة عرض نطاق ناقل مناسبًا ( عرض النطاق الترددي ) ومرتبطًا باستهلاك مرتفع للطاقة. لذلك ، بدأت GPUs المحمولة في اتخاذ نهج مختلف. في هندسة البلاط النموذجية لبطاقات الفيديو المحمولة ، يتم العرض في جزء صغير من الذاكرة المقابلة لجزء الشاشة - البلاط. يتيح لك الحجم الصغير للبلاط (على سبيل المثال ، 16x16 بكسل لبطاقات الفيديو Mali ، و 32 x32 لـ PowerVR) وضعه مباشرة على شريحة بطاقة الفيديو ، مما يجعل سرعة الوصول إليه مماثلة لسرعة الوصول إلى سجلات قلب التظليل ، أي سريع جدا.


ومع ذلك ، نظرًا لأن البدائيين يمكن أن يقعوا في أقسام عشوائية من المخزن المؤقت للشاشة ، ولا يغطي البلاط سوى جزء صغير منه ، فقد كانت هناك حاجة إلى خطوة إضافية في خط الأنابيب. فيما يلي مخطط مبسط لكيفية عمل خط الأنابيب مع بنية البلاط.


بعد معالجة القمم وبناء البدائيات ، فإن الأخيرة ، بدلاً من إرسالها إلى خط أنابيب الشظايا ، تقع في ما يسمى ببلط . هنا يتم توزيع المواد الأولية عن طريق البلاط ، في البكسلات التي تقع فيها. بعد هذا التوزيع ، الذي ، كقاعدة عامة ، يغطي جميع مكالمات السحب الموجهة إلى كائن إطار مؤقت واحد (ويعرف أيضًا باسم هدف التقديم ) ، يتم عرض المربعات بالتسلسل. لكل تجانب ، يتم تنفيذ التسلسل التالي من الإجراءات:

  1. تحميل محتويات FBO القديمة من ذاكرة النظام ( تحميل
  2. جعل البدائيين يسقطون في هذا البلاط
  3. تحميل محتوى FBO جديد على ذاكرة النظام ( المتجر )


وتجدر الإشارة إلى أنه يمكن اعتبار عملية التحميل تراكبًا إضافيًا لـ "مادة ملء الشاشة" بدون ضغط. إذا أمكن ، تجنب هذه العملية ، أي لا تسمح لـ FBO بالتبديل ذهابًا وإيابًا. إذا تم مسح جميع محتوياته قبل العرض في FBO ، فلن يتم تنفيذ عملية التحميل . ومع ذلك ، من أجل إرسال الإشارة الصحيحة إلى السائق ، يجب أن تستوفي معايير هذا التنظيف معايير معينة:

  1. يجب أن يتم تعطيل Rector Rect
  2. يجب السماح بالتسجيل في جميع قنوات الألوان والألفا.

لمنع عملية تحميل المخزن المؤقت للعمق والاستنسل ، يجب أيضًا تنظيفها قبل بدء التقديم.

من الممكن أيضًا تجنب تشغيل المتجر للمخزن المؤقت للعمق / الاستنسل. بعد كل شيء ، لا يتم عرض محتويات هذه المخازن المؤقتة بأي شكل من الأشكال على الشاشة. قبل عملية glSwapBuffers ، يمكنك استدعاء glDiscardFramebufferEXT أو glInvalidateFramebuffer

const GLenum attachments[] = {GL_DEPTH_ATTACHMENT, GL_STENCIL_ATTACHMENT};
glDiscardFramebufferEXT (GL_FRAMEBUFFER, 2, attachments);

const GLenum attachments[] = {GL_DEPTH_ATTACHMENT, GL_STENCIL_ATTACHMENT};
glInvalidateFramebuffer(GL_FRAMEBUFFER, 2, attachments);

هناك سيناريوهات تجسيد لا يلزم فيها وضع المخازن المؤقتة للعمق / الاستنسل ، وكذلك المخازن المؤقتة لـ MSAA في ذاكرة النظام. على سبيل المثال ، إذا كان العرض في FBO مع المخزن المؤقت للعمق متواصلًا ، ولم يتم استخدام معلومات العمق من الإطار السابق ، فلا يلزم تحميل المخزن المؤقت للعمق في ذاكرة التجانب قبل بدء العرض ، أو إلغاء تحميله بعد اكتمال العرض. لذلك ، لا يمكن تخصيص ذاكرة النظام تحت المخزن المؤقت العمق. تسمح لك واجهات برمجة التطبيقات الحديثة للرسومات مثل Vulkan و Metal بتعيين وضع الذاكرة بشكل صريح لنظرائك في FBO  ( MTLStorageModeMemoryless in Metal، VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT + VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT في فولكان ).

من الجدير بالذكر هو تنفيذ MSAA على هياكل البلاط. لا يترك المخزن المؤقت عالي الدقة لـ MSAA ذاكرة التجانب عن طريق تقسيم FBO إلى المزيد من التجانبات. على سبيل المثال ، بالنسبة إلى MSAA 2x2 ، سيتم حل مربعات 16x16 على أنها 8x8 أثناء عملية المتجر ، أي في المجموع ، سيكون من الضروري معالجة 4 أضعاف البلاط. لكن الذاكرة الإضافية لـ MSAA غير مطلوبة ، وبسبب العرض في ذاكرة التجانب السريع ، لن تكون هناك قيود كبيرة على النطاق الترددي. ومع ذلك استخدميزيد MSAA على هندسة البلاط الحمل على Tiler ، مما يمكن أن يؤثر سلبًا على أداء عرض المشاهد مع الكثير من الهندسة.

تلخيص ما سبق ، نقدم المخطط المطلوب للعمل مع FBO على هندسة البلاط:

// 1.   ,    auxFBO
glBindFramebuffer(GL_FRAMEBUFFER, auxFBO);
glDisable(GL_SCISSOR);
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glDepthMask(GL_TRUE);
// glClear,     
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | 
           GL_STENCIL_BUFFER_BIT);

renderAuxFBO();         

//   /      
glInvalidateFramebuffer(GL_FRAMEBUFFER, 2, depth_and_stencil);
// 2.   mainFBO
glBindFramebuffer(GL_FRAMEBUFFER, mainFBO);
glDisable(GL_SCISSOR);

glClear(...);
//   mainFBO    auxFBO
renderMainFBO(auxFBO);

glInvalidateFramebuffer(GL_FRAMEBUFFER, 2, depth_and_stencil);

إذا قمت بالتبديل إلى عرض auxFBO في منتصف تشكيل mainFBO ، يمكنك الحصول على عمليات تحميل وتخزين غير ضرورية ، والتي يمكن أن تزيد بشكل كبير من وقت تشكيل الإطار. في ممارستنا ، واجهنا تباطؤًا في العرض حتى في حالة إعدادات FBO الخاملة ، أي بدون التقديم الفعلي فيها. نظرًا لبنية المحرك ، بدت دائرتنا القديمة كما يلي:

//   mainFBO
glBindFramebuffer(GL_FRAMEBUFFER, mainFBO);
//   
glBindFramebuffer(GL_FRAMEBUFFER, auxFBO);
//  auxFBO
renderAuxFBO();

glBindFramebuffer(GL_FRAMEBUFFER, mainFBO);
//   mainFBO
renderMainFBO(auxFBO);

على الرغم من عدم وجود مكالمات gl بعد التثبيت الأول لـ mainFBO ، إلا أننا حصلنا على عمليات تحميل وتخزين إضافية وأداء أسوأ على بعض الأجهزة .

لتحسين فهمنا للنفقات العامة من استخدام FBOs الوسيطة ، قمنا بقياس الوقت الضائع لتبديل FBOs بملء الشاشة باستخدام اختبار اصطناعي. يوضح الجدول الوقت الذي يقضيه في عملية المتجر عند تبديل FBO عدة مرات في إطار واحد (يتم إعطاء وقت هذه العملية). عملية التحميل غائبة بسبب glClear، بمعنى آخر. تم قياس سيناريو أكثر ملاءمة. ساهم الإذن المستخدم على الجهاز. يمكن أن تتوافق إلى حد ما مع قوة GPU المثبتة. لذلك ، تعطي هذه الأرقام فقط فكرة عامة عن مدى تكلفة تحويل الأهداف على بطاقات الفيديو المحمولة للأجيال المختلفة.
GPUمللي ثانيةGPUمللي ثانية
Adreno 3205.2
Adreno 5120.74
PowerVR G62003.3Adreno 6150.7
مالي 4003.2Adreno 5300.4
مالي- T7201.9مالي- G510.32
PowerVR SXG 5441.4مالي- t830
0.15

استنادًا إلى البيانات التي تم الحصول عليها ، يمكننا أن نصل إلى التوصية بعدم استخدام أكثر من مفتاح أو اثنين من مفاتيح FBO لكل إطار ، على الأقل لبطاقات الفيديو القديمة. إذا كانت اللعبة تحتوي على تمرير رمز منفصل للأجهزة ذات النهاية المنخفضة ، فمن المستحسن عدم استخدام تغيير FBO هناك. ومع ذلك ، في النهاية المنخفضة ، غالبًا ما تصبح مسألة خفض الدقة ذات صلة. على نظام التشغيل Android ، يمكنك خفض دقة العرض دون اللجوء إلى استخدام وسيط FBO عن طريق الاتصال بـ SurfaceHolder.setFixedSize ():

surfaceView.getHolder().setFixedSize(...)

لن تعمل هذه الطريقة إذا تم تقديم اللعبة من خلال تطبيق Surface الرئيسي (مخطط نموذجي للعمل مع NativeActivity ). إذا كنت تستخدم Surface الرئيسي ، فيمكن تعيين دقة أقل عن طريق استدعاء الوظيفة الأصلية ANativeWindow_setBuffersGeometry.

JNIEXPORT void JNICALL Java_com_organization_app_AppNativeActivity_setBufferGeometry(JNIEnv *env, jobject thiz, jobject surface, jint width, jint height)
{
ANativeWindow* window = ANativeWindow_fromSurface(env, surface); 
ANativeWindow_setBuffersGeometry(window, width, height, AHARDWAREBUFFER_FORMAT_R8G8B8X8_UNORM); 
}

في Java:

private static native void setBufferGeometry(Surface surface, int width , int height )
...
//   SurfaceHolder.Callback
@Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height)
{
     setBufferGeometry(holder.getSurface(), 768, 1366); /* ... */
...

أخيرًا ، نذكر أمر ADB المناسب للتحكم في مخازن السطح المختارة على Android:

adb shell dumpsys surfaceflinger

يمكنك الحصول على استنتاج مماثل يسمح لك بتقدير استهلاك الذاكرة للمخازن المؤقتة على السطح:


تُظهر لقطة الشاشة النظام الذي يبرز 3 مخازن مؤقتة للتخزين المؤقت الثلاثي للعبة GLSurfaceView (مظللة باللون الأصفر) ، بالإضافة إلى مخزنين مؤقتين للسطح الرئيسي (مظلل باللون الأحمر). في حالة العرض من خلال جهاز Surface الرئيسي ، وهو المخطط الافتراضي عند استخدام NativeActivity ، يمكن تجنب تخصيص المخازن المؤقتة الإضافية. 

هذا كل شئ حتى الان. في المقالات التالية ، سنقوم بتصنيف GPUs المحمولة ، بالإضافة إلى تحليل طرق تحسين التظليل لهم.

All Articles