التنقيح في OpenGL

كتبه بركات يوم الأحد, 10 تـمـوز 2016

لو عملت لفترة مع OpenGL API ربما ستجد أن تصميم الـAPI يصعب التنقيح ويسهل الوقوع في الأخطاء، فلو لاحظت أن كثير من الدوال في OpenGL لاتحدد نوع المتغيرات الممررة خصوصاً تلك التي تأخذ GLenum والذي غالباً مايترجم إلى عدد صحيح مثل:

typedef unsigned int GLenum;

والذي تقبل قيم معرفة كأعداد ثابتة باستخدام macros مثل:

...
#define GL_FOG_COLOR 0x0B66
#define GL_DEPTH_RANGE 0x0B70
#define GL_DEPTH_TEST 0x0B71
#define GL_DEPTH_WRITEMASK 0x0B72
#define GL_DEPTH_CLEAR_VALUE 0x0B73
#define GL_DEPTH_FUNC 0x0B74
...

بدل استخدام الـenum، لذا من السهل أن تخطئ وتمرر قيمة لاعلاقة لها بالقيم التي تقبلها الدالة، والمترجم لن ينتبه للخطأ لأن الدالة تقبل عدد صحيح ومررت لها عدد صحيح، وفي بعض الأحيان تلك القيم تصادف قيمة من قيم الدالة لكنها ليست القيمة التي تريدها، لذا سيعمل البرنامج ولكنه لايؤدي الغرض الذي تريده.

مثل هذه المشكلة تجعل تتبع مكان الخطأ صعب، هذا المقال سيكون بعض النصائح التي يمكنك استخدامها للتسهيل على نفسك تنقيح برامج OpenGL، واكتشاف الأخطاء ومشاكل الأداء.

قراءة الوثائق

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

هناك عدة مصادر للحصول على وثائق OpenGL، يعجبني منها موقع docs.GL.

استخدام glGetError

معظم دوال OpenGL لاتعيد قيمة، void، لذا لاتستطيع مباشرة التأكد من أن الاستدعاء تم بشكل صحيح عن طريق الدالة نفسها، لكن يمكنك التأكد من الخطأ باستخدام دالة أخرى وهي glGetError، لاتأخذ هذه الدالة قيمة، وتعيد آخر خطأ حصل كما هو مشروح في الوثائق، وتعيد GL_NO_ERROR إذا كان كل شيء سليم.

من الأفضل استدعاء glGetError مباشرة بعد أي استدعاء لأي دالة من دوال OpenGL والتأكد من أنها تعيد GL_NO_ERROR، حتى لو كنت تظن أن الإستدعاء سليم، لكن نظراً لأن كثرة استدعاء glGetError قد تقلل الأداء كثيراً على بعض الـimplementations، كون glGetError لم تعد خطأ أثناء التنقيح فمن الأرجح أنها لن تعيد خطأ عند نشر البرنامج إلا إذا كان هذا الخطأ لايمكن تفاديه كاستهلاك الذاكرة، فمن الأفضل إزالتها عند نشر البرنامج واستخدامها فقط اثناء التقيح باستخدام مثلاً assert:

glDrawArrays(GL_TRIANGLES, 0, 36);
assert(glGetError() == GL_NO_ERROR); // Debug only

عندما تبني البرنامج في C/C++ مع تعريف الماكرو NDEBUG سوف تزال جملة assert، طبعاً يمكنك كتابة macro خاص بك بدلاً من assert لإضافة معلومات إضافية عن الخطأ.

انتبه أن هناك مشكلة عند استخدام GLEW مع glewExperimental = GL_TRUE لتحميل دوال OpenGL الحديثة، حيث تبلغك بوجود خطأ، يمكنك بعد استدعاء glewInit والتأكد من أنها لم تعد خطأ أن تستدعي glGetError وتجاهل الخطأ:

glewExperimental = GL_TRUE;
GLenum error = glewInit();
if (error != GLEW_OK)
{
  throw std::runtime_error("glewInit");
}
// Ignore any OpenGL error before this moment
(void)glGetError();

تفعيل الـlogging

وفرت OpenGL بدأ من الإصدار 4.3 الدالة glDebugMessageCallback، حيث تمكنك من تسجيل دالة تستدعيها OpenGL لغرض الـlogging والذي يساعد كثيراً في الحصول على معلومات إضافية عن الأخطاء والمشاكل وكذلك معلومات إضافية عن الأداء وسير البرنامج.

الكثير قد لايملكون كرت شاشة حديث يدعم كل وظائف OpenGL 4 أو قد يريدون استهداف اصدارات أقدم مثل OpenGL 3، لحسن الحظ أن هذه الدالة قد تكون متوفرة باستخدام الإضافة ARB_debug_output بالإسم glDebugMessageCallbackARB إن كان مزود الـAPI يدعمها، يمكنك استدعاءها باستخدام GLEW هذا:

#ifdef _DEBUG
  if (GLEW_ARB_debug_output)
  {
    glDebugMessageCallbackARB(DebugMessageCallback, nullptr);
    assert(glGetError() == GL_NO_ERROR);
  }
  else
  {
    // ARB_debug_output is not available
  }
#endif

هنا مررنا DebugMessageCallback لـglDebugMessageCallbackARB و nullptr، يمكنك استخدامه إن كنت تريد تمرير معلومات خاصة لدالة الـlogging، الفرق بين glDebugMessageCallback و glDebugMessageCallbackARB هو أن المتغير userParam معرف في ARB بأنه ثابت const void *.

ستجد هنا implementation كتبته لـDebugMessageCallback، فقط غير LogDebug لما يناسبك.

استخدام gDEBugger

هناك عدة أدوات خارجية لتقيح برامج OpenGL أفضلها برأيي gDEBugger، حيث يوفر عدة وظائف للتنقيح وجمع معلومات عن أداء البرنامج، وإحصائيات عن دوال OpenGL وكذلك OpenCL، والأخطاء، ورسومات بيانية، والبرنامج مجاني وسهل الاستخدام.

مثال لتنقيح مثال الإبريق GRTeaPot والذي تجده مع البرنامج:

مثال gDEBugger

هناك عدة مستويات من التنقيح يوفرها البرنامج مرتبة من الأسرع والأقل تفاصيل للأبطأ والأكثر تفاصيل (تجدها في القائمة View أو في شريط الأدوات):

  • خيار Profile Mode (أيقونة الرسم البياني): والذي يوفر أقل التفاصيل حيث يمكنك استخدامه لأخذ نظرة عامة على أداء برنامج مع تفعيل أهم خيارات التقيح.
  • خيار Debug Mode (أيقونة الحشرة) والذي يوفر معظم خيارات التقيح لكنه أبطأ من الخيار السابق، يمكنك استخدامه لإيجاد المشاكل والأخطاء.
  • خيار Analyze Mode (أيقونة الترس) والذي يوفر تفاصيل كثيرة ودقيقة عن برنامجك بداً من أكثر الدوال استخدام وطبيعة هذه الدوال، هذا الخيار مهم لإعادة هيكلة البرنامج، لكنه يبطئ البرنامج.

البرنامج سهل الأستخدام ويعطي الكثير من المعلومات المفيدة عن البرنامج ويساعدك في اكتشاف الأخطاء ومشاكل الأداء التي قد لاتنتبه لها، ستجد شرح له في قائمة Help ثم Tutorial.