السلام عليكم،
رأينا في الحلقة الأولى الوجه الأهم في التوازي بين المعالج المركزي ومعالج الرسوميات. اليوم سنتعرف في هذه الحلقة على أساليب ممتازة لتخريب هذا التوازي وإجبار الأداء على الانحدار لأسوأ درجة ممكنة. طبعاً الهدف هو أن نتفادى الإتيان بهذا التصرف في برامجنا، أو كما يقولون في البرامج الأمريكية: لا تفعلوا هذا في المنزل أيها الأطفال (don't do this at home kids).
لنبدأ بمثال بسيط في دايركت ثري دي:
1: pD3D9Device->Clear(0, NULL, 3, D3DCOLOR_ARGB(0,0x42,0x4b,0x79), 1.000f, 0);
2: pD3D9Device->SetLight(0, &light);
3: pD3D9Device->LightEnable(0, TRUE);
4: pD3D9Device->SetVertexShaderConstantF(1, pVertexShader, 1);
5: pD3D9Device->BeginScene();
6: pD3D9Device->SetTransform(D3DTS_WORLD, &matWorld);
7: pD3D9Device->SetTransform(D3DTS_VIEW, &matView);
8: pD3D9Device->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
9: pD3D9Device->SetMaterial(&material);
10: pD3D9Device->SetTexture(0, pTexture);
11: pD3D9Device->SetVertexDeclaration(pVertexDecl);
12: pD3D9Device->SetStreamSource(0, pVertexBuffer, 0, 44);
13: pD3D9Device->SetIndices(pIndexBuffer);
14: pD3D9Device->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, 8, 0, 6);
15: pD3D9Device->EndScene();
كل سطر من الأسطر أعلاه يُولد أمراً واحداً يتم تكديسه في مخزن الأوامر الذي يراقبه معالج الرسوميات. لاحظ أن السطر 14 هو أمر رسم مثلثات، وكما تحدثنا سابقاً، هو الأمر الذي يستغرق وقتاً ملموساً لتنفيذه.. عندما يقوم برنامجنا بالمرور على هذه السطور فإنه لا ينتظر انتهاء معالج الرسوميات من تنفيذ الأمر، وإنما يستمر البرنامج في السير دون انتظار… هذا يعني أننا سننتهي من تنفيذ كافة السطور على المعالج المركزي، لكن معالج الرسوميات سيكون مشغولاً لبعض الوقت في سحب الأوامر وتنفيذها الواحد تلو الآخر.
لاحظ أن هذه التفاصيل التي نتحدث عنها غير ظاهرة للمبرمج العادي، وهذه إحدى فوائد البرمجة باستخدام واجهات برمجية (API) مثل دايركت ثري دي أو أوبن جي إل. بعبارة أخرى، هذه الواجهات البرمجية تبسط برمجة الرسوميات وتخفي بعض التعقيدات عن المبرمج.
قف!
حسناً، ليست كل الأوامر المرسلة من المعالج المركزي إلى معالج الرسوميات يتم تكديسها وتنفيذها بالتوازي. هناك بعض العمليات التي تضطر المعالجَين للتنسيق والعمل خطوة بخطوة، مما يكسر التوازي ويبطئ الأداء إلى حد بعيد. القائمة التالية تلخص أصناف هذه العمليات:
- الكتابة أو القراءة من ذاكرة محجوزة لمورد (إكساء، مخزن رؤوس، مخزن فهارس، سطح رسم) قيد الاستخدام في معالج الرسوميات.
- حجز أو تحرير مورد محجوز في ذاكرة معالج الرسوميات.
- ملء مكدس الأوامر بأوامر ضخمة تستنفذ كامل ذاكرة المكدس.
لنتحدث قليلاً عن كل من هذه الأصناف:
الصنف الأول: لننظر إلى المثال التالي. في الكود المذكور أعلاه، في السطر 12 تحديداً، نقوم بإخبار معالج الرسوميات بأن يستخدم مخزن رؤوس معين لاستخراج معلومات الرؤوس منه (الإحداثيات، اللون، النواظم، … الخ). وفي السطر 14 نرسل أمر الرسم ليبدأ الرسم باستخدام هذا المخزن. الآن، تخيل لو قمنا بتنفيذ التالي بعد الأسطر السابقة:
1: void *pVBData = NULL;
2: pVertexBuffer->Lock(0,0,&pVBData,0);
3: // pVBData عدّل على البيانات في الذاكرة عند
4: pVertexBuffer->Unlock();
السطر الثاني يقوم "بقفل" ذاكرة مخزن الرؤوس في ذاكرة معالج الرسوميات (القفل هو مصطلح خاص بدايركت ثري دي، ويعني الحصول على مؤشر لذاكرة مورد ما يقبع على ذاكرة معالج الرسوميات). السطر الثالث يمكننا استبداله بأية عمليات تقوم بتعديل محتويات مخزن الرسوميات. السطر الرابع يقوم بفك القفل عن المخزن، مما يُعلم معالج الرسوميات أنه الآن يستطيع القراءة من هذا المخزن بأمان.
عملية القفل هذه ستضطر المعالج المركزي للانتظار ريثما ينتهي معالج الرسوميات من القراءة من مخزن الرسوميات هذا، قبل أن يستطيع المعالج المركزي تعديل المحتوى بأمان. بوووم.. انتهى التوازي…
هذه العملية هي أكثر العمليات شيوعاً في الحقيقة، فتعديل محتويات مخزن الرؤوس يعتبر أسلوب من أساليب تحريك المجسمات. مثلاً التلبيس (skinning) والتحوير (morphing) للشخصيات ومحاكاة السطوح المائية (water simulation) ورسم المسطحات الأرضية (terrain) ومحاكاة الجزيئات (particle simulation). هناك أساليب عدة لتفادي هذه الأزمة، والواجهات البرمجية تقدم تسهيلات للمبرمج لحل المشكلة دون خسارة التوازي، لكننا لن نتحدث عن هذه الحلول اليوم.
الصنف الثاني: وهو خطأ يقع فيه المبتدؤون في الغالب، وهو القيام بتحميل المجسمات والإكساءات أثناء الرسم. مثلاً، اللاعب في مرحلة، وحان وقت ظهور زعيم الأشرار، فيقوم المبرمج في هذه اللحظة بتحميل مجسم الزعيم وإكساءاته كي يستطيع رسمه وإظهاره. عملية التحميل هذه تتضمن حجز الذاكرة للموارد الجديدة في ذاكرة معالج الرسوميات، وهذه العملية تجبر المعالج المركزي على انتظار معالج الرسوميات ريثما ينتهي كلياً من تفريغ مكدس الأوامر قبل حجز الذاكرة. بوووم.. انتهى التوازي ثانيةً… أيضاً هناك حلول سنتطرق لها في تدوينات أخرى إن شاء الله…
الصنف الثالث: وهو أيضاً خطأ يقع فيه المبتدؤون، وإن واجهتُ شخصياً بعض المحترفين الذين يقعون فيه أيضاً (نعم، أنا أعنيك أنت يا محرك أنريل). كل أمر يستهلك مساحة معينة من مكدس الأوامر. هناك بعض الأوامر التي تستهلك حجماً كبيراً، أبرزها أمر الرسم من ذاكرة المعالج المركزي. في دايركت ثري دي هناك الأمرين DrawPrimitiveUP و DrawIndexedPrimitiveUP، وفي أوبن جي إل هناك كافة أوامر الرسم المباشر بدون استخدام مخازن الرؤوس، والتي تتلخص بمجموعة أوامر glBegin و glVertex و glEnd. هذه الأوامر تتسبب بنسخ كافة معلومات الرؤوس من ذاكرة المعالج المركزي إلى مكدس الأوامر. فلو كان عدد الرؤوس كبيراً، فإن المكدس سيمتلئ بسرعة ويضطر المعالج المركزي للانتظار ريثما يفرغه معالج الرسوميات. هذا واحد من الأسباب الشائعة لبطء الأداء الملحوظ في الألعاب التي يكتبها من بدأ تعلم برمجة الألعاب للتوّ. الحل الصحيح يكمن في الاستعمال الصحيح للمخازن، وسنسهب في هذا الموضوع قريباً إن شاء الله.
مما سبق، يسهل تخريب التوازي بين معالج الرسوميات والمعالج المركزي دون الشعور بذلك. فالمبرمج دون قصد أو علم، قد يرتكب خطأ من أي من الأصناف آنفة الذكر. تتبع مشاكل الأداء من هذا النوع ليس سهلاً، ويحتاج إلى خبرة ووقت وأدوات. لكني أضمن لك أن اللعبة التي تحوي مثل هذه المشاكل تفقد الكثير من أدائها دون مبرر، وهناك ألعاب فعلية منتشرة ترتكب هذه الأخطاء. وفي بعض الأحيان تكون اللعبة من الشهرة مما يدفع شركات بطاقات العرض مثل إنفيديا و AMD ليقوموا ببعض الحركات البهلوانية في برامج التشغيل (drivers) لتصحيح المشكلة والتفاخر بأنهم يحققون أداءً أعلى في هذه اللعبة من منافسيهم… لكني لا أنصح شخصياً بالدخول بمثل هذه المسائل
في العدد القادم سنتحدث عن وجه آخر من أوجه التوازي في معالجة الرسوميات، لكنه توازي داخلي في قلب المعالج، وليس بينه وبين معالج آخر… لكن هذه حلقة أخرى.
السلام عليكم ورحمة الله وبركاته