الطوابير وذاكرة الأوامر في Vulkan

من ويكي
اذهب إلى: تصفح، ابحث
هذه الصفحة لاتزال تحت الإنشاء، سيتم تحديثها مستقبلاً

تحدثنا في مقال كائنات الأجهزة في Vulkan عن الأجهزة وتطرقنا للطوابير وأنواع الطوابير، حيث أشرنا لأن جميع المهام التي تريد تنفيذها يجب إرسالها عبر طوابير المعالجة والتي تقسم إلى طوابير الرسوميات والحوسبة والنقل، وأشرنا لأن الطوابير تقسّم لمجموعات كل مجموعة تحتوي على طابور واحد على الأقل، عندما أنشأنا الكائن VkDevice قمنا بتحديد أنواع وعدد الطوابير التي سنستخدمها من كل نوع.

في هذا المقال سنتحدث عن الطوابير وذاكرة الأوامر command buffers المستخدمة لإرسال أوامر المعالجة من تصيير وحوسبة ونقل عبر الطوابير.

الحصول على الكائن VkQueue

عندما ننشيء الجهاز VkDevice فإننا حددنا معلومات الطوابير التي نريد استخدامها في العضو VkDeviceCreateInfo::pQueueCreateInfos، حيث حددنا مؤشر مجموعة الطوابير في العضو VkDeviceQueueCreateInfo::queueFamilyIndex وعدد الطوابير التي نريد إنشاءها من ذلك النوع في VkDeviceQueueCreateInfo::queueCount، عند إنشاء الجهاز فإن Vulkan ستنشئ معه كائنات VkQueue خاصه بكل طابور، بعد إنشاء الجهاز كل مانحتاج إليه للحصول على كائن الطابور VkQueue هو استخدام الدالة vkGetDeviceQueue():

void vkGetDeviceQueue(
    VkDevice                                    device,
    uint32_t                                    queueFamilyIndex,
    uint32_t                                    queueIndex,
    VkQueue*                                    pQueue);

نضع كائن الجهاز في المعامل الأول، ثم نضع مؤشر نوع الطوابير الذي استخدمناه عندما أنشأنا كائن الجهاز، ونضع مؤشر الطابور في ذلك النوع في المعامل queueIndex، ستضع الدالة كائن VkQueue في المعامل الثالث.

انتبه أن الدالة void لاتعيد قيمة، فلا يمكن أن يفشل استدعاءها، لكن قد تخطئ وتمرر مؤشر لنوع طوابير خطأ أو تشير لطابور غير موجود، وقتها سينهار البرنامج، طبعاً مثل هذا الخطأ خطأ بسيط يمكن اكتشافه أثناء التطوير.

الكائن VkQueue يتم إنشاؤه وهدمه مع الكائن VkDevice.

ذاكرة الأوامر

في Vulkan الأوامر مثل أوامر دوال النسخ والرسم والتنفيذ المبدؤة بالسابقة vkCmd*() لاترسل مباشرة بل تسجل الأوامر في في ذاكرة أوامر VkCommandBuffer ثم ترسل هذه الذاكرة دفعة واحدة، ذاكرة الأوامر هذه تخصصها من حوض ذاكرة أوامر VkCommandPool حيث يحجز المشّغل لك ذاكرة VkCommandBuffer مسبقاً ثم يعطيك منها، وعند تحرير أحد كائنات VkCommandBuffer فإنها تعود للحوض لإعادة استخدامها لاحقاً، هذا التصميم يسمح لك بإنشاء عدة خيوط معالجة كل خيط منها يتولى مسؤولية تسجيل مجموعة من الأوامر في ذاكرة أوامر منفصلة، أو يمكنك أن تستخدم حوض ذاكرة VkCommandPool خاص بكل خيط معالجة بحيث تحجز منه دون الحاجة للمزامنة عند حجز الذاكرة بين باقي الخيوط، وتسجّل أوامر التصيير بالتوازي في VkCommandBuffer، ثم ترسل الأوامر VkCommandBuffer تسلسلياً عبر طابور المعالجة VkQueue، يمكنك أيضاً إنشاء أكثر من حوض كلها تتصرف بطريقة مختلفة، مثلاً بعضها لذواكر أوامر ستستخدمها لمدة قصيرة والبعض الآخر لذواكر أوامر ستعيد إرسال أوامرها.

في البداية وللتعلم يمكنك إنشاء حوض ذاكرة واحد على خيط المعالجة الرئيسي.

إنشاء حوض ذاكرة الأوامر VkCommandPool

لإنشاء حوض ذاكرة أوامر فإننا سنستخدم الدالة vkCreateCommandPool():

VkResult vkCreateCommandPool(
    VkDevice                                    device,
    const VkCommandPoolCreateInfo*              pCreateInfo,
    const VkAllocationCallbacks*                pAllocator,
    VkCommandPool*                              pCommandPool);

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

أعضاء البنية VkCommandPoolCreateInfo:

typedef struct VkCommandPoolCreateInfo {
    VkStructureType             sType;
    const void*                 pNext;
    VkCommandPoolCreateFlags    flags;
    uint32_t                    queueFamilyIndex;
} VkCommandPoolCreateInfo;
  • sType يجب أن يكون VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO و pNext يجب أن يكون NULL
  • flags تمثّل راية بتات تحدد نمط حجز الذاكرة الذي تخطط لاستخدامه، هذه الرايات ستساعد المشغّل لاختيار الطريقة الأمثل في حجز الذاكرة، يمكن أن تأخذ أحد أو مجموعة من هذه الرايات:
    • VK_COMMAND_POOL_CREATE_TRANSIENT_BIT تعني أن ذاكرة الأوامر المخصصة من هذا الحوض لن تعيش إلا لفترة قصيرة ثم تحررها، إذا لم ترفع هذه الراية فهذا يشير لأنك تخطط للإبقاء على ذاكرة الأوامر مدة طويلة نسبياً
    • VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT تمكنك من إعادة ضبط ذاكرة الأوامر الفردية لإعادة استخدامها بدلاً من تحريرها وإنشاء ذاكرة أوامر جديدة، مالم ترفع هذه الراية فلايمكنك سوى إعادة ضبط كامل ذواكر الأوامر في الحوض دفعة واحدة، سنفصّل في هذا لاحقاً
  • queueFamilyIndex ضع به مؤشر نوع الطوابير الذي تريد استخدامه، يمكنك إنشاء ذاكرة الأوامر باستقلال عن الطابور لهذا نحتاج لتحديد نوع الطابور هنا، أنتبه لأن بعض الأوامر لاتستخدم إلا مع نوع طوابير معين، مثلاً أوامر التصيير الرسومي لايمكن تسجيلها وإرسالها إلا عبر طابور يدعم هذه العملية

حجز ذاكرة الأوامر VkCommandBuffer من الحوض

بعدما جهزنا حوض الذاكرة، يمكننا أن نحجز منه ذاكرة أوامر باستخدام الدالة vkAllocateCommandBuffers():

VkResult vkAllocateCommandBuffers(
    VkDevice                                    device,
    const VkCommandBufferAllocateInfo*          pAllocateInfo,
    VkCommandBuffer*                            pCommandBuffers);

تأخذ الدالة في المعامل الأول الجهاز، ثم مؤشر للبنية VkCommandBufferAllocateInfo التي تحتوي على معلومات ذاكرة الأوامر المراد حجزها، ثم مؤشر لمتغير واحد أو مصفوفة المتغيرات التي ستستقبل ذاكرة الأوامر التي تم حجزها، عدد المتغيرات ستحدده في VkCommandBufferAllocateInfo::commandBufferCount.

البنية VkCommandBufferAllocateInfo تحتوي على معلومات ذاكرة الأوامر المراد حجزها:

typedef struct VkCommandBufferAllocateInfo {
    VkStructureType         sType;
    const void*             pNext;
    VkCommandPool           commandPool;
    VkCommandBufferLevel    level;
    uint32_t                commandBufferCount;
} VkCommandBufferAllocateInfo;
  • sType يجب أن تكون VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO و pNext يجب أن تكون NULL
  • commandPool تحدد به الحوض الذي تريد تخصيص ذاكرة الأوامر منه
  • level تحدد نوع ذاكرة الأوامر التي تريد إنشاءها، وهي أحد نوعين:
    • VK_COMMAND_BUFFER_LEVEL_PRIMARY تعني أن هذه ذاكرة أوامر رئيسية يمكن إرسالها مباشرة عبر طابور المعالجة، حالياً سنستخدم هذا النوع فقط، ولاحقاً سنفصّل في ذواكر الأوامر الفرعية
    • VK_COMMAND_BUFFER_LEVEL_SECONDARY تعني أن هذه ذاكرة أوامر فرعية لايمكن إرسالها مباشرة عبر طابور المعالجة، بل يجب إلحاقها لاحقاً بأحد ذواكر المعالجة الرئيسية ثم إرسالها
  • commandBufferCount تحدد به عدد ذواكر الأوامر التي ستحجزها الدالة vkAllocateCommandBuffers()

هدم حوض الذاكرة

يمكن هدم الحوض وتحرير جميع ذواكر الأوامر التي حجزت منه باستخدام الدالة vkDestroyCommandPool():

void vkDestroyCommandPool(
    VkDevice                                    device,
    VkCommandPool                               commandPool,
    const VkAllocationCallbacks*                pAllocator);

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

تحرير ذاكر الأوامر يدوياً

يمكن أيضاً أن تحرر ذاكرة الأوامر يدوياً باستخدام الدالة vkFreeCommandBuffers():

void vkFreeCommandBuffers(
    VkDevice                                    device,
    VkCommandPool                               commandPool,
    uint32_t                                    commandBufferCount,
    const VkCommandBuffer*                      pCommandBuffers);

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

دورة حياة ذاكرة الأوامر

قبل الاستمرار في حديثنا عن ذاكرة الأوامر يجب أن نفهم دورة حياة ذاكرة الأوامر كي نعرف كيف نستخدمها استخدام سليم، ذاكرة الأوامر في Vulkan في أي لحظة يمكن أن تكون في أحد هذه الحالات:

الحالة الأولية initial state: عندما نحجز ذاكرة أوامر باستخدام vkAllocateCommandBuffers() فإن ذاكرة الأوامر ستكون في حالتها الأولية، يمكننا أن نحررها أو نبدأ بستجيل الأوامر فيها، يمكننا أيضاً إعادة ضبط ذاكرة أوامر في حالة التسجيل، أو الحالة القابلة للتنفيذ، أو الحالة الباطلة وإعادتها إلى الحالة الأولية باستخدام vkResetCommandBuffer().

حالة التسجيل recording state: عندما تستدعي الدالة vkBeginCommandBuffer() مع أي ذاكرة أوامر ليست في حالة انتظار التنفيذ أو في حالة التسجيل فإنها ستتحول لحالة التسجيل، عندما تكون ذاكرة الأوامر في هذه الحالة فيمكننا تسجيل الأوامر باستخدام دوال vkCmd*().

الحالة القابلة للتنفيذ executable state: عندما تستدعي الدالة vkEndCommandBuffer() مع ذاكرة أوامر في حالة التسجيل فإنها ستتحول للحالة القابلة للتنفيذ، عندما تكون في هذه الحالة فإنه يمكننا إرسالها للمعالجة باستخدام vkQueueSubmit().

حالة انتظار التنفيذ pending state: عندما نرسل ذاكرة أوامر في الحالة القابلة للتنفيذ فإننها ستتحول لحالة انتظار التنفيذ، حسب الخيارات التي تحددها في vkQueueSubmit() فإن ذاكرة الأوامر ستتحول سواءً للحالة القابلة للتنفيذ أو الحالة الباطلة في حال أرسلتها باستخدام VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT والتي تشير لأن هذه الذاكرة ستستخدم مرة واحدة، عندما تكون ذاكرة الأوامر في حالة انتظار التنفيذ يجب أن ننتظر إلى أن ينتهي منها الجهاز باستخدام أحد أدوات المزامنة التي سنتحدث عنها لاحقاً.

الحالة الباطلة invalid state: كما أشرنا فإن ذاكرة الأوامر قدد تتحول للحالة الباطلة عندما ترسل ذاكرة أوامر تستخدم مرة، عندما تكون في هذه الحالة فإنه لايمكن تسجيل الأوامر عليها بل يمكنك إما أن تعيد ضبطها وإعادتها للحالة الأولية أو تعيد التسجيل عليها وتحولها لحالة التسجيل أو تحررها من الذاكرة، بعض الأخطاء قد تؤدي بذاكرة الأوامر لهذه الحالة.

قد تبدو هذه الحالات متشابكة إلا أنها منطقية وستصبح أكثر منطقية مع الوقت.

إعادة ضبط ذاكرة الأوامر

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

مالم تكن ذاكرة الأوامر في انتظار التنفيذ، فإنه يمكننا إعادة ضبطها للحالة الأولية باستخدام الدالة vkResetCommandBuffer():

VkResult vkResetCommandBuffer(
    VkCommandBuffer                             commandBuffer,
    VkCommandBufferResetFlags                   flags);

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

يمكنك أيضاً أن تستخدم الدالة vkResetCommandPool() والتي تمكنك من إعادة ضبط كامل ذواكر الأوامر المخصصة من حوض ذاكرة دفعة واحدة:

VkResult vkResetCommandPool(
    VkDevice                                    device,
    VkCommandPool                               commandPool,
    VkCommandPoolResetFlags                     flags);

حيث تأخذ نفس المعاملات التي تأخذها vkResetCommandBuffer() عدى أنها تأخذ حوض ذاكرة في المعامل الثاني بدلاً من ذاكرة أوامر ولاتتطلب أنك أنشأت حوض الذاكرة باستخدام VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT لأن هذا الأخير يستخدم فقط إذا أردت تحرير ذاكرة أوامر مفردة.

بدء وإنهاء تسجيل الأوامر في ذاكرة الأوامر

قبل أن تبدأ باستخدام دوال vkCmd*() وتسجّل الأوامر عليك نقل ذاكرة الأوامر لحالة التسجيل باستخدام الدالة vkBeginCommandBuffer():

VkResult vkBeginCommandBuffer(
    VkCommandBuffer                             commandBuffer,
    const VkCommandBufferBeginInfo*             pBeginInfo);

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

تأخذ الدالة في المعامل الثانية مؤشر للبنية VkCommandBufferBeginInfo التي تحتوي على بعض المعلومات الإضافية، وأعضاؤها:

typedef struct VkCommandBufferBeginInfo {
    VkStructureType                          sType;
    const void*                              pNext;
    VkCommandBufferUsageFlags                flags;
    const VkCommandBufferInheritanceInfo*    pInheritanceInfo;
} VkCommandBufferBeginInfo;
  • sType يجب أن يكون VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO و pNext يجب أن يكون NULL
  • flags تمثل راية بتات مركبة من:
    • VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT تعني أنك تريد إرسال ذاكرة الأوامر مرة واحدة فقط ثم تنتهي منه سواءً بتحريره من الذاكرة أو بإعادة ضبطه، عند إرسال ذاكرة الأوامر فإن هذه الراية تجعل الدالة vkQueueSubmit() تحوّل حالة ذاكرة الأوامر الحالة الباطلة
    • VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT تعني أن ذاكرة الأوامر جزء من مرحلة تصيير render pass، سنتحدث عن مراحل التصيير لاحقاً، تنطبق هذه الراية فقط على ذواكر الأوامر الفرعية وسيتم تجاهلها عند استخدامها مع ذاكرة أوامر رئيسية
    • VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT تعني أن ذاكرة الأوامر هذه قد يتم إعادة إرسالها للتنفيذ حتى وهي أثناء التنفيذ بالتوزاي أو قد تستخدم كذاكرة أوامر فرعية في أكثر من ذاكرة أوامر رئيسية ترسل بالتوازي، لايشترط أن ترفع هذه الراية في حال أردت إعادة ارسال ذاكرة الأوامر وكنت متأكد من أن ذاكرة الأوامر لن تنفذ بالتوازي
  • pInheritanceInfo تستخدم مع ذواكر الأوامر الفرعية وسيتم تجاهلها في حال كانت ذاكرة الأوامر هذه ذاكرة رئيسية، ضعها NULL في هذه الحالة، سنتطرق للبنية VkCommandBufferInheritanceInfo لاحقاً عند حديثنا عن مراحل التصيير ومراحل التصيير المتعددة

بعد استدعاء هذه الدالة ستصبح ذاكرة الأوامر في حالة التسيجل وجاهزة لتسجل الأوامر فيها باستخدام دوال vkCmd*()، سنطرق لاحقاً للأوامر كلاً في مقالها.

عند الإنتهاء من تسجيل الأوامر استخدم الدالة vkEndCommandBuffer() لإنهاء تسجيل الأوامر:

VkResult vkEndCommandBuffer(
    VkCommandBuffer                             commandBuffer);

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

إرسال ذاكرة الأوامر للمعالجة

بعد الإنتهاء من تسجيل الأوامر الرئيسية يمكننا جدولتها للتنفيذ بإرسالها للطابور باستخدام الدالة vkQueueSubmit():

VkResult vkQueueSubmit(
    VkQueue                                     queue,
    uint32_t                                    submitCount,
    const VkSubmitInfo*                         pSubmits,
    VkFence                                     fence);

تأخذ الدالة في المعامل الأول كائن الطابور الذي تريد استخدامه لإرسال ذاكرة الأوامر، في المعامل الثاني نحدد عدد الأوامر التي نريد إرسالها، وفي المعامل الثالث نضع مؤشر لمتغير أو مصفوفة VkSubmitInfo والتي تحتوي على معلومات ذواكر الأوامر المراد إرسالها، المعامل الأخير نضع كائن السياج VkFence وهو أحد أدوات المزامنة المستخدمة للمزامنة بين الجهاز والمضيف والتي تستخدم هنا للتحقق من إتمام تنفيذ ذاكرة الأوامر، سنتطرق لهذا الكائن لاحقاً عند حديثنا عن المزامنة، يمكننا حالياً تمرير VK_NULL_HANDLE في هذا المعامل وانتظار إتمام جميع المهام المرسلة عبر الطابور باستخدام الدالة vkQueueWaitIdle() أو إنتظار إتمام جميع المهام على جميع طوابير الجهاز باستخدام vkDeviceWaitIdle()، في حال استخدمت السياج فإنه يجب أن يكون غير جاهز unsignaled.

البنية VkSubmitInfo تحتوي على المعلومات اللازمة لإرسال ذاكرة الأوامر للمعالجة أعضاؤها:

typedef struct VkSubmitInfo {
    VkStructureType                sType;
    const void*                    pNext;
    uint32_t                       waitSemaphoreCount;
    const VkSemaphore*             pWaitSemaphores;
    const VkPipelineStageFlags*    pWaitDstStageMask;
    uint32_t                       commandBufferCount;
    const VkCommandBuffer*         pCommandBuffers;
    uint32_t                       signalSemaphoreCount;
    const VkSemaphore*             pSignalSemaphores;
} VkSubmitInfo;
  • sType يجب أن تكون VK_STRUCTURE_TYPE_SUBMIT_INFO و pNext يجب أن تكون NULL
  • waitSemaphoreCount تأخذ عدد السيمافورات التي ستمررها في العضو pWaitSemaphores
  • pWaitSemaphores تأخذ مؤشر لمتغير سيمافور VkSemaphore أو مصفوفة سيمافورات وهو أيضاً أحد أدوات المزامنة ويستخدم في Vulkan للمزامنة بين طوابير المعالجة داخل الجهاز، سنتحدث عنها بالتفصيل لاحقاً، قبل تنفيذ ذاكرة الأوامر سينتظر الجهاز أن تعطي كل هذه السيمافورات إشارة الجاهزية قبل البدء في تنفيذ ذاكرة الأوامر
  • pWaitDstStageMask عبارة عن مصفوفة لراية بتات من النوع VkPipelineStageFlagBits بعدد waitSemaphoreCount، حيث أن كل pWaitDstStageMask[i] تحدد المرحلة التي سيتم انتظار إشارة جاهزية السيمافور المقابل في pWaitSemaphores[i]، سنتحدث عن هذه المراحل لاحقاً
  • commandBufferCount يمثّل عدد ذواكر الأوامر التي ستمررها في pCommandBuffers
  • pCommandBuffers تأخذ مؤشر لمتغير أو مصفوفة ذواكر أوامر، يجب أن تكون كلها ذواكر أوامر أوليّة في حالة قابلة للتنفيذ
  • signalSemaphoreCount تمثّل عدد السيمافورات التي ستمررها في العضو pSignalSemaphores
  • pSignalSemaphores تأخذ مؤشر لمتغيّر أو مصفوفة سيمافورات ستعطي إشارة الجهازية بعد انتهاء تنفيذ ذاكرة الأوامر

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

يمكنك حالياً تمرير NULL في pWaitSemaphores و pSignalSemaphores، وتمرير صفر في waitSemaphoreCount و pWaitDstStageMask وكذلك signalSemaphoreCount.

عند إرسال ذاواكر المعالجة لطابور المعالجة فستتحول حالتها إلى انتظار التنفيذ، عندما تنتهي فستعود حالتها إلى إما قابلة للتنفيذ أو ستصبح باطلة في حال استخدمت الراية VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT عندما استدعيت vkBeginCommandBuffer().

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

انتظار خمول الطابور والجهاز

أحد الطرق البسيطة لانتظار تنفيذ الأوامر هي استخدام الدالة vkQueueWaitIdle():

VkResult vkQueueWaitIdle(
    VkQueue                                     queue);

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

هناك طريقة اخرى وهي استخدام الدالة vkDeviceWaitIdle():

VkResult vkDeviceWaitIdle(
    VkDevice                                    device);

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

غالباً لاتريد استخدام vkQueueWaitIdle() ولا vkDeviceWaitIdle() سوى مثلاً قبل إنهاء البرنامج للتأكد من تمام جميع المهام قبل البدء بهدم الكائنات، بل ستستخدم بدلها VkFence لأنها على مستوى الإرسالية، فلو أرسلت مثلاً 8 دفعات من ذواكر الأوامر للمعالجة، وانتهت إثنتان منها، فلست بحاجة لإنتظار الإرساليات الخمس المتبقية، بل يمكنك البدء فوراً في إرسال المزيد من المهام للجهاز وإبقاءه مشغول لتحقيق أقصى استفاده، توفر VkFence أيضاً دوال للتحقق من إنتهاء الإرسالية دون الحاجة لإيقاف البرنامج في وضع الإنتظار، يمكنك استغلال هذا الوقت في عمل شيء مفيد على المضيف كالتجهيز للإرساليات التالية، تحقيق أقصى استفادة من الجهاز هو سبب وجود Vulkan، لكن غالباً تريد تقليل عدد الإرساليات لأن الإرسال عملية مكلفة.