كائنات الأجهزة في Vulkan

من ويكي
اذهب إلى: تصفح، ابحث

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

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

حالياً لا تدعم Vulkan استخدام أكثر من جهاز كجهاز واحد مثل Nvidia SLI أو AMD CrossFire، هذه الميزة مخطط لها في الإصدارات القادمة من Vulkan.

سرد الأجهزة الداعمة لـ Vulkan

يمكن سرد الأجهزة التي تدعم Vulkan باستخدام الدالة vkEnumeratePhysicalDevices():

VkResult vkEnumeratePhysicalDevices(
    VkInstance                                  instance,
    uint32_t*                                   pPhysicalDeviceCount,
    VkPhysicalDevice*                           pPhysicalDevices);

حيث تأخذ في المعامل الأول الكائن VkInstance، المتغيرين الثاني والثالث لهما معنيين حسب ما إذا كانت قيمة pPhysicalDevices تساوي NULL أم لا:

  • في حال كانت قيمة pPhysicalDevices تساوي NULL، فإن الدالة ستخزن عدد الأجهزة فقط في المتغير الذي تشير له pPhysicalDeviceCount بغض النظر عن قيمتها، هذا مفيد كي نحجز مصفوفة VkPhysicalDevice بحجم كافي لعدد الأجهزة
  • في حال لم تكن قيمة pPhysicalDevices تساوي NULL ولم تكن قيمة pPhysicalDeviceCount مساوية للصفر، فإن الدالة ستخزن في مصفوفة الكائنات VkPhysicalDevice التي تشير لها pPhysicalDevices بعدد القيمة المخزنة في pPhysicalDeviceCount، ثم تخزن في المتغير pPhysicalDeviceCount عدد الأجهزة التي كتبتها

إذا أعادت vkEnumeratePhysicalDevices() كامل عدد الأجهزة في pPhysicalDevices، بمعنى أن pPhysicalDeviceCount يساوي أو أكبر عدد الأجهزة الفعلي، فإن الدالة ستعيد VK_SUCCESS، عدى هذا ستعيد VK_INCOMPLETE

هذا يعني أنه يمكننا سرد الأجهزة إما بحجز ذاكرة مسبقة نعتقد أنها كافيه، مثلاً:

  uint32_t physicalDevicesCount = 10;
  VkPhysicalDevice physicalDevices[10];
  vkEnumeratePhysicalDevices(instance, &physicalDevicesCount, physicalDevices);
  assert(physicalDevicesCount > 0); // ??
  
  for (uint32_t i = 0 ; i < physicalDevicesCount ; ++i)
  {
    auto physicalDevice = physicalDevices[i];

    ...
  }

أو الطريقة الأفضل وهي حجز الذاكرة ديناميكياً:

  uint32_t physicalDevicesCount;
  vkEnumeratePhysicalDevices(instance, &physicalDevicesCount, nullptr);
  assert(physicalDevicesCount > 0); // ??

  std::vector<VkPhysicalDevice> physicalDevices(physicalDevicesCount);
  vkEnumeratePhysicalDevices(instance, &physicalDevicesCount, physicalDevices.data());

  for (auto physicalDevice : physicalDevices)
  {
    ...
  }

الكائنات VkPhysicalDevice يتم إنشاءها عند إنشاءها وهدمها مع VkInstance>/code> ولا تنشئها وتهدمها بنفسك.

سرد خصائص الجهاز

يمكن الإستعلام وسرد خصائص الجهاز <code>VkPhysicalDevice باستخدام الدالة vkGetPhysicalDeviceProperties():

void vkGetPhysicalDeviceProperties(
    VkPhysicalDevice                            physicalDevice,
    VkPhysicalDeviceProperties*                 pProperties);

حيث تأخذ في المعامل الأول الكائن VkPhysicalDevice، وتأخذ في المعامل الثاني مؤشر للبنية VkPhysicalDeviceProperties، أعضاء هذه البنية:

typedef struct VkPhysicalDeviceProperties {
    uint32_t                            apiVersion;
    uint32_t                            driverVersion;
    uint32_t                            vendorID;
    uint32_t                            deviceID;
    VkPhysicalDeviceType                deviceType;
    char                                deviceName[VK_MAX_PHYSICAL_DEVICE_NAME_SIZE];
    uint8_t                             pipelineCacheUUID[VK_UUID_SIZE];
    VkPhysicalDeviceLimits              limits;
    VkPhysicalDeviceSparseProperties    sparseProperties;
} VkPhysicalDeviceProperties;
  • apiVersion تمثل إصدار Vulkan الذي يدعمه الجهاز، يمكن الحصول على أرقام الإصدارات باستخدام الماكرو VK_VERSION_MAJOR(version) للإصدار الرئيسي و VK_VERSION_MINOR(version) للإصدار الفرعي و VK_VERSION_PATCH(version) لرقم الإصلاح
  • driverVersion يمثل إصدار المشغّل، معنى هذا الاصدار يختلف باختلاف المزود
  • vendorID يمثل رمز المزود، عادةً يمثل رقم الـ PCI الخاص بهذا المزود، مثلاً Nvidia رمزها 0x10de أو بالعشري 4318، يمكن الحصول على قاعدة بيانات أرقام الـ PCI الخاصة بالمزودين من موقع PCIDatabase
  • deviceID ويمثل رقم الجهاز الخاص بهذا المزود
  • deviceType ويمثل نوع الجهاز، الأنواع المعرفّة في Vulkan:
    • VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU يعني أن هذا الجهاز معالج رسومي مدمج
    • VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU يعني أن هذا الجهاز معالج رسومي منفصل، عادةً في كرت شاشة
    • VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU للمعالجات الرسومية التخيلية، ممكن تستخدمها الأجهزة التخيلية
    • VK_PHYSICAL_DEVICE_TYPE_CPU يشير لأن الجهاز معالج رئيسي
    • VK_PHYSICAL_DEVICE_TYPE_OTHER لأي جهاز آخر لا تنطبق عليه الصفات السابقة
  • deviceName حيث يحتوي على نصّ UTF-8 يمثّل اسم الجهاز
  • pipelineCacheUUID يمثل معرّف UUID فريد لهذا الجهاز
  • limits يحتوي على بنية VkPhysicalDeviceLimits تمثل حدود الجهاز القصوى، أعضاء هذه البنية كثيرة ولن نتطرق لها، يمكن الإستفادة من موقع Vulkan Hardware Database الذي يحتوي على قاعدة بيانات لمعلومات عن الأجهزة التي تدعم Vulkan وخصائصها
  • sparseProperties يحتوي على بنية VkPhysicalDeviceSparseProperties والتي تمثل دعم الجهاز للموارد المحمّلة جزئياً والتي تمكن من إستخدام مثلاً صور كبيرة قد تكون أكبر من ذاكرة الجهاز وتحميل أجزاء منها فقط حسب الحاجة، هذه الميزة تسفيد منها مثلاً الألعاب التي تحتوي على عوالم كبيرة، عموماً موضوعها متقدم ولن نتحدث عنه الآن

سرد المميزات التي يدعمها الجهاز

الجهاز الي يدعم Vulkan قد لا يدعم كل المميزات التي توفرها Vulkan مثل دعم الـ tessellation الذي يسمح بإضافة المزيد من التفاصيل للمجسمات الثلاثية الأبعاد أثناء التشغيل، بتكلفة تخزين ونقل أقل، يمكنك استخدام الدالة vkGetPhysicalDeviceFeatures() قبل استخدام أحد هذه المميزات للتأكد من دعم الجهاز لها:

void vkGetPhysicalDeviceFeatures(
    VkPhysicalDevice                            physicalDevice,
    VkPhysicalDeviceFeatures*                   pFeatures);

تأخذ الدالة الكائن VkPhysicalDevice في المعامل الأول، وتأخذ مؤشر للبنية VkPhysicalDeviceFeatures في المعامل الثاني، البنية VkPhysicalDeviceFeatures كبيرة ولن نتطرق لها الآن، حيث تحتوي على أسماء المميزات، أعضاءها قيم منطقية VkBool32 تأخذ إما VK_TRUE دلالةً على دعم الميزة أو VK_FALSE دلالةً على عدم دعمها.

الاستعلام عن مجموعات الطوابير

يحتوي الجهاز الواحد على طابور فأكثر والتي سنستخدمها لإرسال المهام المراد تنفيذها من رسم أو حوسبة أو نقل، الطابور ينتمي لأحد أو لعدةً مجموعات من الطوابير queue families، يمكننا الإستعلام عن مجموعات الطوابير التي يدعمها الجهاز باستخدام الدالة vkGetPhysicalDeviceQueueFamilyProperties():

void vkGetPhysicalDeviceQueueFamilyProperties(
    VkPhysicalDevice                            physicalDevice,
    uint32_t*                                   pQueueFamilyPropertyCount,
    VkQueueFamilyProperties*                    pQueueFamilyProperties);

حيث تأخذ في المعامل الأول الكائن VkPhysicalDevice، وتأخذ في المعامل الثاني مؤشر لعدد الطوابير وفي الثالث مؤشر للمصفوفة التي ستحتوي معلومات أنواع الطوابير، يمكننا استدعاء الدالة مرتين مثلما فعلنا مع الدالة vkEnumeratePhysicalDevices() للحصول على كامل المجموعات:

uint32_t queueFamilyPropertyCount;
vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyPropertyCount, nullptr);

std::vector<VkQueueFamilyProperties> queueFamilyProperties(queueFamilyPropertyCount);
vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyPropertyCount, queueFamilyProperties.data());

كل عنصر في هذه المصفوفة يمثل مجموعة طوابير واحد، VkQueueFamilyProperties يمثل بنية أعضاؤها:

typedef struct VkQueueFamilyProperties {
    VkQueueFlags    queueFlags;
    uint32_t        queueCount;
    uint32_t        timestampValidBits;
    VkExtent3D      minImageTransferGranularity;
} VkQueueFamilyProperties;
  • queueFlags يمثل أنواع العمليات التي تدعمها هذه المجموعة، هذا العضو عبارة عن راية بتات، هناك أربعة عمليات:
    • VK_QUEUE_GRAPHICS_BIT ويعني أن هذا الطابور يدعم الرسوميات
    • VK_QUEUE_COMPUTE_BIT طابور يدعم الحوسبة
    • VK_QUEUE_TRANSFER_BIT طابور يدعم نقل البيانات
    • VK_QUEUE_SPARSE_BINDING_BIT طابور يدعم عمليات إدارة sparse resources، سنؤجل الحديث عنها لوقت لاحق
  • queueCount تشير لعدد الطوابير في هذه المجموعة
  • timestampValidBits يمثل عدد بتات في المتغير المستخدم عند الإستعلام عن الوقت من مجموعة الطوابير هذه، عدد البتات يتراوح بين 36 و 64 بت، إذا كانت قيمة هذا الحقل صفر، فهذا يعني أن هذه المجموعة لاتدعم الإستعلام عن الوقت، الإستعلام عن الوقت مفيد عندما تريد التحقق من الوقت الذي استغرقته عملية ما لمعرفة أداءها
  • minImageTransferGranularity والذي نوعة VkExtent3D يمثل أقل أبعاد صورة يمكن نقلها، وهي بنية بسيطة تمثل العرض والطول والعمق:
typedef struct VkExtent3D {
    uint32_t    width;
    uint32_t    height;
    uint32_t    depth;
} VkExtent3D;

بعض الملاحظات:

  • للتحقق من queueFlags عبارة عن راية بتات، للتحقق مثلاً من دعم المجموعة للرسوميات سنستخدم العملية يمكنك استخدام (queueFamilyProperty.queueFlags & VK_QUEUE_GRAPHICS_BIT) != 0
  • إذا كانت هناك أي مجموعة تدعم الرسوميات فيجب أن تكون هناك مجموعة واحدة على الأقل تدعم كلاً من الرسوميات والحوسبة معاً، سواءً نفس المجموعة أم مجموعة أخرى
  • إذا كانت المجموعة تدعم الرسوميات أو الحوسبة فهي ضمنياً تدعم نقل البيانات سواء ذكر هذا أم لا
  • إذا كانت المجموعة تدعم الرسوميات أو الحوسبة فإن قيمة minImageTransferGranularity يجب أن تكون VkExtent3D{1, 1, 1}

استخدم الموقع Vulkan Hardware Database للإطلاع على معلومات عدد كبير من الأجهزة في Vulkan، مثلاً على كرت GeForce GTX 980 Ti هناك مجموعتين للطوابير:

  • المجموعة الأولى تدعم كل العمليات وفيها 16 طابور، وتدعم وقت بدقة 64 بت
  • المجموعة الثانية فيها طابور واحد يدعم النقل فقط، ووقت بدقة 64 بت

من المهم أن نعرف مؤشر index مصفوفة الـ VkQueueFamilyProperties لمجموعة الطوابير الذي نريد استخدامها لأن هذا المؤشر ستستخدمه لاحقاً عندما تريد إنشاء الكائن VkDevice.

إنشاء الكائن VkDevice

الآن أصبح بمتناولنا جميع المعلومات التي نحتاجها لإنشاء الكائن VkDevice، والذي يمثل اتصالنا بالجهاز الفيزيائي VkPhysicalDevice وإعداداته مثل أنواع الطوابير التي سنستخدمها والخصائص التي نريد تفعيلها، من الكائن VkDevice يمكننا إنشاء جميع الموارد مثل حجز الذاكرة ونسخ البيانات لذاكرة الجهاز وإنشاء ذاكرة الأوامر وغيرها من العمليات.

لإنشاء الكائن يلزمنا إستدعاء الدالة vkCreateDevice():

VkResult vkCreateDevice(
    VkPhysicalDevice                            physicalDevice,
    const VkDeviceCreateInfo*                   pCreateInfo,
    const VkAllocationCallbacks*                pAllocator,
    VkDevice*                                   pDevice);

تأخذ الدالة في المعامل الأول الكائن VkPhysicalDevice الخاص بالجهاز الذي نريد إستخدامه، وتأخذ في الثاني مؤشر للبنية VkDeviceCreateInfo والتي ستحتوي على إعدادات الجهاز، وفي الثالث مؤشر للبنية VkAllocationCallbacks، وفي المعامل الأخير مؤشر للكائن VkDevice والذي ستخزن فيه الكائن VkDevice الذي تم إنشاؤه.

تعريف البنية VkDeviceCreateInfo:

typedef struct VkDeviceCreateInfo {
    VkStructureType                    sType;
    const void*                        pNext;
    VkDeviceCreateFlags                flags;
    uint32_t                           queueCreateInfoCount;
    const VkDeviceQueueCreateInfo*     pQueueCreateInfos;
    uint32_t                           enabledLayerCount;
    const char* const*                 ppEnabledLayerNames;
    uint32_t                           enabledExtensionCount;
    const char* const*                 ppEnabledExtensionNames;
    const VkPhysicalDeviceFeatures*    pEnabledFeatures;
} VkDeviceCreateInfo;
  • sType يجب أن يكون VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO
  • pNext ضع NULL
  • flags غير مستخدم حالياً، ضعه صفر
  • queueCreateInfoCount يمثّل عدد أنواع الطوابير التي سنستخدمها
  • pQueueCreateInfos مؤشر لمصفوفة بنى من نوع VkDeviceQueueCreateInfo بعدد queueCreateInfoCount حيث تحتوي معلومات أنواع الطوابير التي سنستخدمها، سنتحدث عنها بعد قليل
  • enabledLayerCount تمثل عدد الطبقات في ppEnabledLayerNames التي سننشئها على مستوى الجهاز، هذا العضو مهمل وسيتم تجاهله، حيث استعيض عنه بالطبقات على مستوى VkInstance، ضعه 0
  • ppEnabledLayerNames كما أشرت سيتم تجاهل هذا العضو، ضعه NULL
  • enabledExtensionCount يمثل هذا العضو عدد إضافات الجهاز التي نريد استخدامها، هناك عدد من الإضافات المهمة سنستخدمها في وقت لاحق، لكن حالياً يمكن أن تضعه صفر
  • ppEnabledExtensionNames مؤشر لأسماء تلك الإضافات، حالياً يمكن أن تضعه NULL
  • pEnabledFeatures يشير هذا العضو للبنية VkPhysicalDeviceFeatures التي تحدثنا عنها سابقاً، حيث تشير هنا للميزات التي نريد تفعيلها، حالياً لانريد استخدام ميزات متقدمه، لذا ضعه NULL إلى وقت آخر

البنية VkDeviceQueueCreateInfo كما أشرنا إلى نوع الطابور الذي نريد استخدامه، أعضاؤها:

typedef struct VkDeviceQueueCreateInfo {
    VkStructureType             sType;
    const void*                 pNext;
    VkDeviceQueueCreateFlags    flags;
    uint32_t                    queueFamilyIndex;
    uint32_t                    queueCount;
    const float*                pQueuePriorities;
} VkDeviceQueueCreateInfo;
  • sType يجب أن يكون VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO
  • pNext ضعه NULL
  • flags غير مستخدم حالياً، ضعه صفر
  • queueFamilyIndex يمثّل مؤشر النوع في مصفوفة VkQueueFamilyProperties الذي أشرنا لها في القسم السابق
  • queueCount يمثّل عدد الطوابير التي نريد استخدامها من هذا النوع
  • pQueuePriorities مؤشر لمصفوفة float بعدد queueCount، حيث تمثل أوليّة الطوابير، وتعطى رقم في المدى 0.0 و 1.0، حيث أن 0.0 أقل أولوية و 1.0 يمثلّ أعلى أولوية، ستتم جدولة المهام المرسلة للطابور حسب الأولوية التي تحددها

احتفظ بمؤشرات الطوابير التي تحددها عند إنشاء الجهاز لأنك ستحتاج لها لاحقاً.

هدم الكائن VkDevice

لهدم الكائن VkDevice استخدم الدالة vkDestroyDevice() ومرر لها الكائن VkDevice:

void vkDestroyDevice(
    VkDevice                                    device,
    const VkAllocationCallbacks*                pAllocator);

مثال

في هذا المثال سننشئ كائن VkDevice مستخدمين أول جهاز فيزيائي، وسنبحث في مجموعة الطوابير التي يوفرها عن أي مجموعة طوابير تدعم الرسوميات، لنأخذ مؤشرها ثم ننشيء طابور واحد بأعلى أولولية:

  ...
  // إنشاء الجهاز

  VkDevice device;

  // الإستعلام عن الأجهزة المتوفرة
  uint32_t physicalDeviceCount;
  vkEnumeratePhysicalDevices(instance, &physicalDeviceCount, nullptr);
  assert(physicalDeviceCount > 0);
  std::vector<VkPhysicalDevice> physicalDevices(physicalDeviceCount);
  vkEnumeratePhysicalDevices(instance, &physicalDeviceCount, physicalDevices.data());

  // سأخذ أوّل جهاز، يفترض أن يترك خيار الجهاز المراد استخدامه للمستخدم
  auto physicalDevice = physicalDevices[0];

  // الإستعلام عن أنواع الطوابير المتوفرة على هذا الجهاز
  uint32_t queueFamilyPropertyCount;
  vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyPropertyCount, nullptr);
  std::vector<VkQueueFamilyProperties> queueFamilyProperties(queueFamilyPropertyCount);
  vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyPropertyCount, queueFamilyProperties.data());

  // نبحث عن مؤشر لأي مجموعة طوابير تدعم الرسوميات في ذلك الجهاز
  uint32_t queueFamilyIndex = 0;
  for (queueFamilyIndex = 0 ; queueFamilyIndex < queueFamilyProperties.size() ; ++queueFamilyIndex)
  {
    if (queueFamilyProperties[queueFamilyIndex].queueFlags & VK_QUEUE_GRAPHICS_BIT)
      break;
  }

  assert(queueFamilyIndex < queueFamilyProperties.size());

  // سننشئ طابور واحد بأعلى أولوية
  static const std::vector<float> queuePriorities = { 1.0f };

  VkDeviceQueueCreateInfo deviceQueueCreateInfo;
  deviceQueueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
  deviceQueueCreateInfo.pNext = nullptr;
  deviceQueueCreateInfo.flags = 0;
  deviceQueueCreateInfo.queueFamilyIndex = queueFamilyIndex;
  deviceQueueCreateInfo.queueCount = static_cast<uint32_t>(queuePriorities.size()); // عدد الطوابير
  deviceQueueCreateInfo.pQueuePriorities = queuePriorities.data();

  std::vector<VkDeviceQueueCreateInfo> queueCreateInfos = {
    deviceQueueCreateInfo
  };

  VkDeviceCreateInfo deviceCreateInfo;
  deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
  deviceCreateInfo.pNext = nullptr;
  deviceCreateInfo.flags = 0;
  deviceCreateInfo.queueCreateInfoCount = static_cast<uint32_t>(queueCreateInfos.size());
  deviceCreateInfo.pQueueCreateInfos = queueCreateInfos.data();
  deviceCreateInfo.enabledLayerCount = 0;
  deviceCreateInfo.ppEnabledLayerNames = nullptr;
  deviceCreateInfo.enabledExtensionCount = 0; // لانريد إستخدام أي إضافة حالياً
  deviceCreateInfo.ppEnabledExtensionNames = nullptr;
  deviceCreateInfo.pEnabledFeatures = nullptr; // لانريد تفعيل أي ميزة حالياً

  result = vkCreateDevice(physicalDevice, &deviceCreateInfo, nullptr, &device);
  assert(result == VK_SUCCESS);
  ...

طبعاً سنهدم الكائن VkDevice في نهاية البرنامج وقبل أن نهدم الكائن VkInstance:

  ...
  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  // هدم الكائنات

  vkDestroyDevice(device, nullptr);

  ////

  auto pfn_vkDestroyDebugReportCallbackEXT = reinterpret_cast<PFN_vkDestroyDebugReportCallbackEXT>(
    vkGetInstanceProcAddr(instance, "vkDestroyDebugReportCallbackEXT")
  );
  assert(pfn_vkDestroyDebugReportCallbackEXT != nullptr);

  pfn_vkDestroyDebugReportCallbackEXT(instance, debugReportCallback, nullptr);

  ////

  vkDestroyInstance(instance, nullptr);

  return 0;
}