الموارد في Vulkan

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

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

هناك نوعين من الموارد في Vulkan: الذاكرة الوسيطة buffers والصور images.

الذاكرة الوسيطة

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

إنشاء الذاكرة الوسيطة والكائن VkBuffer

لإنشاء كائن VkBuffer لذاكرة وسيطة نستخدم الدالة vkCreateBuffer():

VkResult vkCreateBuffer(
    VkDevice                                    device,
    const VkBufferCreateInfo*                   pCreateInfo,
    const VkAllocationCallbacks*                pAllocator,
    VkBuffer*                                   pBuffer);

حيث تأخذ الكائن VkDevice، والبنية VkAllocationCallbacks التي تصف معلومات هذه الذاكرة، وفي المعامل الأخير تأخذ مؤشر لمتغير VkBuffer سيخزن فيه الكائن الذي تم إنشاءه.

البنية VkBufferCreateInfo:

typedef struct VkBufferCreateInfo {
    VkStructureType        sType;
    const void*            pNext;
    VkBufferCreateFlags    flags;
    VkDeviceSize           size;
    VkBufferUsageFlags     usage;
    VkSharingMode          sharingMode;
    uint32_t               queueFamilyIndexCount;
    const uint32_t*        pQueueFamilyIndices;
} VkBufferCreateInfo;
  • تأخذ البنية VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO في sType و NULL في pNext
  • flags تأخذ رايات خاصة بالموارد المحمّلة جزئياً، سنؤجل الحديث عنها، يمكن وضعها صفر
  • size يشير لحجم الذاكرة التي يمثلها هذا المورد
  • usage تستطيع باستخدام هذا العضو أن تخبر Vulkan عن كيف ستستخدم الذاكرة المرتبطة مع هذا الكائن، الإستخدام الذي تحدده غالباً سيحدد أين يمكنك إستخدام هذه الذاكرة، هناك عدّة رايات يمكنك تركيبها لوصف الإستخدام:
    • VK_BUFFER_USAGE_TRANSFER_SRC_BIT تشير لأن الذاكرة ستستخدم كوجهة لنقل البيانات لها من عن طريق طابور نقل البيانات، للجهاز مثلاً
    • VK_BUFFER_USAGE_TRANSFER_DST_BIT يعني أن الذاكرة ستسخدم كمصدر لنقل البيانات لها، سنفصل نقل البيانات لاحقاً عند حديثنا عن الطوابير
    • VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT تعني أن الذاكرة يمكن استخدامها لإنشاء كائن VkBufferView يمكننا من قراءة الذاكرة كتكسلات[١] ثابتة ولاتتغير طوال تنفيذ المظلل، بمعنى أنه يمكننا القراءة منها فقط، سنتحدث عنها لاحقاً في هذا المقال
    • VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT تعني أن الذاكرة ستستخدم لإنشاء كائن VkBufferView يمكننا من قراءة الذاكرة كتكسلات يمكن الكتابة إليها من المظللات
    • VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT تعني أن الذاكرة ستمثّل بيانات عامة ثابتة طوال تنفيذ المظلل
    • VK_BUFFER_USAGE_STORAGE_BUFFER_BIT شبيهة بالسباقة لكن الذاكرة سيكتب عليها من المظلل
    • VK_BUFFER_USAGE_INDEX_BUFFER_BIT تعني أن الذاكرة ستمثّل مؤشرات نقاط[٢] والتي تستخدم لتقليل عدد النقاط عن طريق إعادة إستخدامها عند الرسم، سنتحدث عنها عند حديثنا عن الرسم
    • VK_BUFFER_USAGE_VERTEX_BUFFER_BIT تعني أن الذاكرة ستمثّل معلومات نقاط
    • VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT تعني أن الذاكرة ستحتوي على معلومات الرسم الغير مباشر[٣]
  • sharingMode يشير لما إذا كان هذا المورد ستتشاركه عدةً طوابير أم لا، يأخذ إما VK_SHARING_MODE_EXCLUSIVE حيث تعني أن هذا المورد لن يستخدمه إلا في طابور واحد في أي لحظة بغض النظر عن عدد الطوابير التي قد تستخدمه، أو VK_SHARING_MODE_CONCURRENT في حال سيستخدم هذا المورد في أكثر من طابور معالجة في نفس اللحظة، طبعاً هذا قد يكون له تبعات على الأداء، لو كنت تستخدم هذا المورد في أكثر من طابور لكن تضمن أنه لن يستخدمه إلا طابور واحد في أي لحظة فضع VK_SHARING_MODE_EXCLUSIVE
  • queueFamilyIndexCount في وضعت VK_SHARING_MODE_CONCURRENT في العضو السابق فيجب أن تحدد عدد هذه الطوابير التي ستستخدم هذا المورد بالتزامن، ويجب أن تكون أكثر من واحد، وتحدد مؤشرات هذه الطوابير في pQueueFamilyIndices، راجع مقال الكائن VkPhysicalDevice، في حال كنت ستستخدم المورد في طابور واحد في أي لحظة VK_SHARING_MODE_EXCLUSIVE فضع صفر في queueFamilyIndexCount و NULL في pQueueFamilyIndices

الإستخدام usage لايعني دائماً "محتويات الذاكرة"، فمثلاً حتى تنقل مؤشرات نقاط index buffer إلى ذاكرة محلية عللى الجهاز قد تنشيء كائنين VkBuffer لذاكرتين أحدهما VK_BUFFER_USAGE_TRANSFER_SRC_BIT والثانية VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT، وتخزن مؤشرات النقاط في الأولى كي تنقلها للثانية باستخدام vkCmdCopyBuffer() التي تشترط أن يكون المصدر VK_BUFFER_USAGE_TRANSFER_SRC_BIT والوجهة VK_BUFFER_USAGE_TRANSFER_DST_BIT، كلا الذاكرتين تحتوي على مؤشرات نقاط لكن الثانية فقط يمكن إستخدامها كمؤشرات نقاط مع الدالة vkCmdBindIndexBuffer() لأنها VK_BUFFER_USAGE_INDEX_BUFFER_BIT، سنتطرق لدوال vkCmd*() عند حديثنا عن الطوابير.

الربط بين المورد والذاكرة المحجوزة

عندما نريد الربط بين كائن VkBuffer وبين جزء من الذاكرة التي حجزناها باستخدام vkAllocateMemory() فإننا سنستخدم الدالة vkBindBufferMemory():

VkResult vkBindBufferMemory(
    VkDevice                                    device,
    VkBuffer                                    buffer,
    VkDeviceMemory                              memory,
    VkDeviceSize                                memoryOffset);

حيث تأخذ في المعامل الأول الكائن VkDevice الخاص بالجهاز، وكائن الذاكرة الوسيطة VkBuffer، وكائن الذاكرة المحجوزة على الجهاز VkDeviceMemory، وتأخذ في المعامل الأخير memoryOffset وهو الإزاحة من بداية عنوان الذاكرة المحجوزة، الإزاحة يجب أن تكون بمضاعفات المحاذاة المطلوبة لهذه الذاكرة والمحددة في العضو VkMemoryRequirements::alignment، سنتحدث عنها بعد قليل.

بمجرد ربطك الكائن مع الذاكرة فلن يمكنك فك هذا الإرتباط إلا بهدم الكائن.

هدم الكائن VkBuffer

لهدم الكائن VkBuffer وبعد التأكد من أنه لم يعد مستخدماً استدع الدالة vkDestroyBuffer():

void vkDestroyBuffer(
    VkDevice                                    device,
    VkBuffer                                    buffer,
    const VkAllocationCallbacks*                pAllocator);

متطلبات الذاكرة للذاكرة الوسيطة

هناك بعض الإشتراطات على الذاكرة المخصصة للكائن VkBuffer تحدد حجم البيانات المحجوزة والمحاذاة ونوع الذاكرة، فقد لايحجز الجهاز إلا مثلاً ذاكرة بمضاعفات 256 بايت، لأنه يتطلب عناوين محاذاة على هذا العدد، لذا وقبل إستدعاء vkAllocateMemory() استدع الدالة vkGetBufferMemoryRequirements() لمعرفة متطلبات الذاكرة:

void vkGetBufferMemoryRequirements(
    VkDevice                                    device,
    VkBuffer                                    buffer,
    VkMemoryRequirements*                       pMemoryRequirements);

حيث تأخذ الكائن VkDevice و VkBuffer في الأول والثاني، وتأخذ مؤشر للبنية VkMemoryRequirements التي تحتوي على متطلبات الذاكرة، أعضاء البنية VkMemoryRequirements:

typedef struct VkMemoryRequirements {
    VkDeviceSize    size;
    VkDeviceSize    alignment;
    uint32_t        memoryTypeBits;
} VkMemoryRequirements;
  • size يمثّل الحجم المطلوب بالبايت، فحتى لو طلبت مثلاً 100 بايت، قد يحتاج المورد الذي طلبته 256 مثلاً لإبقاء المحاذاة
  • alignment المحاذاة المطلوبة بالبايت، فيجب أن يكون العنوان محاذى لعناوين من مضاعفات المحاذاة، تضمن Vulkan أن المضاعفات دوماً من مضاعفات 2، وأن الدالة تعيد نفس القيمة في alignment إذا استدعيتها مع أي كائن VkBuffer له نفس القيمة في usage و flags، لذا يمكنك استدعائها مرة واحدة مع الأنواع المشابهة
  • memoryTypeBits تمثّل راية بتات بعدد أنواع الذاكرة المدعومة، حيث أن كل موقع كل بت يمثل راية للنوع الذي يمكن استخدامه، مثلاً لو كانت قيمته بالثنائي 110100000012 فهذا يعني أن النوع الأول والثامن والعاشر والحادي عشر أنواع مناسبة، تضمن Vulkan أن أحد هذه الأنواع دائماً VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT، وطالما لم تضع VK_BUFFER_CREATE_SPARSE_BINDING_BIT في VkBufferCreateInfo::flags عند إنشاء الكائن VkBuffer فإن أحد هذه الأنواع مرئي من المضيف ولذاكرة متماسكة في نفس الوقت VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT، وأن الدالة تعيد أيضاً نفس القيمة في memoryTypeBits إذا استدعيتها مع أي كائن VkBuffer له نفس القيمة في usage و flags

متطلبات الذاكرة والمحاذاة قد تفرض علينا الإهتمام بإدارة الذاكرة نظراً لمقدار الذاكرة الكبير نسبياً الذي يضيع بسبب متطلبات المحاذاة.

مثال

في هذا المثال الفرضي سننشئ كائن VkBuffer لذاكرة بحجم 128 ميجابايت نخطط لإستخدامها كوجهة نكتب فيها البيانات مؤقتاً لإرسالها على طابور النقل مثلاً لنقل البيانات إلى ذاكرة محلية على الجهاز، الشرح في التعليقات:

  // نستعلم مسبقاً عن معلومات ذاكرة الجهاز وأنواعها
  VkPhysicalDeviceMemoryProperties physicalDeviceMemoryProperties;
  vkGetPhysicalDeviceMemoryProperties(physicalDevice, &physicalDeviceMemoryProperties);

  // حجم البيانات التي نريد حجزها
  static const VkDeviceSize bufferDataSize = 128 * 1024 * 1000; // 128 MB

  // إنشاء كائن الذاكرة الوسيطة
  VkBuffer buffer;

  VkBufferCreateInfo bufferCreateInfo{};
  bufferCreateInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
  bufferCreateInfo.pNext = nullptr;
  bufferCreateInfo.flags = 0;
  bufferCreateInfo.size = bufferDataSize;
  // نريد إستخدامه مثلاً كمصدر ننقل منه البيانات عن طريق طابور النقل
  bufferCreateInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
  bufferCreateInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
  bufferCreateInfo.queueFamilyIndexCount = 0;
  bufferCreateInfo.pQueueFamilyIndices = nullptr;

  result = vkCreateBuffer(device, &bufferCreateInfo, nullptr, &buffer);
  assert(result == VK_SUCCESS);

  // نستعلم عن معلومات الذاكرة المطلوبة لكائن الذاكرة الوسيطة
  VkMemoryRequirements bufferMemoryRequirements;
  vkGetBufferMemoryRequirements(device, buffer, &bufferMemoryRequirements);

  // نبحث عن أي ذاكرة تفي بمتطلبات الذاكرة مع تفضيلنا للذاكرة المرئية والمتناسقة، هناك دائماً واحدة بهذا النوع
  static const uint32_t bufferPreferredMemoryType =
    VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
  uint32_t bufferMemoryTypeIndex;

  for (bufferMemoryTypeIndex = 0; bufferMemoryTypeIndex < physicalDeviceMemoryProperties.memoryTypeCount; ++bufferMemoryTypeIndex)
  {
    if ((bufferMemoryRequirements.memoryTypeBits & (1 << bufferMemoryTypeIndex))
        && (physicalDeviceMemoryProperties.memoryTypes[bufferMemoryTypeIndex].propertyFlags & bufferPreferredMemoryType))
    {
      break;
    }
  }

  // نخصص الذاكرة
  VkDeviceMemory bufferDeviceMemory;

  VkMemoryAllocateInfo bufferMemoryAllocateInfo{};
  bufferMemoryAllocateInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
  bufferMemoryAllocateInfo.pNext = nullptr;
  bufferMemoryAllocateInfo.allocationSize = bufferMemoryRequirements.size;
  bufferMemoryAllocateInfo.memoryTypeIndex = bufferMemoryTypeIndex;

  result = vkAllocateMemory(device, &bufferMemoryAllocateInfo, nullptr, &bufferDeviceMemory);
  assert(result == VK_SUCCESS);

  // نربط الذاكرة مع الكائن
  vkBindBufferMemory(device, buffer, bufferDeviceMemory, 0);

  // نربط الذاكرة مع ذاكرتنا الرئيسية كي ننسخ البيانات لها
  // لولا أن الذاكرة مرئية للمضيف
  // VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT
  // لما أمكننا عمل هذا
  void* mappedMemoryAddress = nullptr;
  result = vkMapMemory(device, bufferDeviceMemory, 0, bufferDataSize, 0, &mappedMemoryAddress);
  assert(result == VK_SUCCESS);

  // لمجرّد التجربة فقط سنكتب أصفار في الذاكرة :)
  memset(mappedMemoryAddress, 0, bufferDataSize);

  // كون الذاكرة متماسكة فلا حاجة لإستدعاء
  // vkFlushMappedMemoryRanges
  // وإنهاء صلاحية الذاكرة

  // نفك الربط مع ذاكرتنا الرئيسية
  vkUnmapMemory(device, bufferDeviceMemory);

  ...

   // هدم كائن الذاكرة الوسيطة في نهاية البرنامج
  vkDestroyBuffer(device, buffer, nullptr);

  // تحرير الذاكرة المحجوزة
  vkFreeMemory(device, bufferDeviceMemory, nullptr);

الصور

الصور هي النوع الثاني من الموارد في Vulkan ويمثّلها الكائن VkImage، وهي بيانات متعددة الأبعاد ولها عدّة تنسيقات ويمكن القراءة والكتابة بعدة طرق، سنتطرق لها لاحقاً.

إنشاء الصور والكائن VkImage

طريقة إنشاء الصورة شبيهة بطريقة إنشاء الذاكرة الوسيطة، يمكننا إنشاء صورة باستخدام الدالة vkCreateImage():

VkResult vkCreateImage(
    VkDevice                                    device,
    const VkImageCreateInfo*                    pCreateInfo,
    const VkAllocationCallbacks*                pAllocator,
    VkImage*                                    pImage);

تأخذ الدالة كائن الجهاز ومؤشر لمخصص الذاكرة، وتأخذ الدالة البنية VkImageCreateInfo التي تحتوي معلومات الصورة التي تريد إنشاءها وتعيد كائن الصورة VkImage في pImage، أعضاء البنية VkImageCreateInfo:

typedef struct VkImageCreateInfo {
    VkStructureType          sType;
    const void*              pNext;
    VkImageCreateFlags       flags;
    VkImageType              imageType;
    VkFormat                 format;
    VkExtent3D               extent;
    uint32_t                 mipLevels;
    uint32_t                 arrayLayers;
    VkSampleCountFlagBits    samples;
    VkImageTiling            tiling;
    VkImageUsageFlags        usage;
    VkSharingMode            sharingMode;
    uint32_t                 queueFamilyIndexCount;
    const uint32_t*          pQueueFamilyIndices;
    VkImageLayout            initialLayout;
} VkImageCreateInfo;
  • ضع VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO في sType و NULL في pNext
  • العضو flags يحتوي على خمسة رايات، ثلاثة منها للموارد المحمّلة جزئياً، سنؤجل الحديث عنها، الرايتين الباقية:
    • VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT لإنشاء كائن VkImageView يحمل تنسيق مختلف عن الصورة الأصلية، سنتحدث عن هذا الكائن لاحقاً
    • VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT لإنشاء كائن VkImageView يمثل خريطة مكعبة cube map والذي يستخدم في الإضاءة ويستخدم خصوصاً لإعطاء وهم البيئة مثل السماء والسحاب البعيد، سنتحدث عنها وعن الخريطة المكعبة في وقت لاحق
  • imageType يمثّل أبعاد الصورة، للصور الأحادية البعد ضع VK_IMAGE_TYPE_1D، VK_IMAGE_TYPE_2D للثنائية، و VK_IMAGE_TYPE_3D للثلاثية
  • format يمثل تنسيق الصورة، هناك عدد كبير جداً من التنسيقات، منها ماهو إلزامي وأكثرها إختياري وقد تحتاج للتأكد من أن الجهاز يدعمها، سنتحدث لاحقاً عن كيفية قراءة التنسيقات، عادةً الهدف من استخدام الصورة هو ما يحدد التنسيق المناسب، بعض التنسيقات مثل التنسيقات المضغوطة يمكن القراءة منها لكن لا يمكن الكتابة إليها، سنتطرق لهذه التفاصيل حينما يحين وقتها
  • extent يشير إلى أبعاد الصورة المراد إنشاءها، هو عبارة عن بنية VkExtent3D، في حال كانت الصورة أحادية VK_IMAGE_TYPE_1D البعد فستضع طولها في VkExtent3D::width ويجب أن تضع 1 في باقي القيم VkExtent3D::height و VkExtent3D::depth، وفي حالة VK_IMAGE_TYPE_2D يجب أن تضع 1 في VkExtent3D::depth
  • mipLevels يشير لعدد النسخ المصغّرة mipmaps المراد إنشاءها من الصورة، كل نسخة من الصورة بنصف أبعاد سابقتها أو بنصف طول أكبر ضلع في سابقتها في حال لم تكن مربعة، هذه النسخ المصغرة تستخدم لتقليل تكسير الإكساء وتسريع التصيير باستخدام صور صغيرة للأجسام البعيدة، عدد الصور المصغرة يجب أن لا يزيد عن floor( log2( max(extent.width, extent.height, extent.depth) ) ) + 1، في Vulkan أنت بحاجة لتوليدها بنفسك إما باستخدام صور سبق توليدها في ملف أو توليدها أثناء التشغيل سواءً باستخدام مكتبة صور أو الأفضل باستخدام vkCmdBlitImage()
  • arrayLayers تمثل عدد الصور في حال كان الكائن VkImage يمثل مصفوفة صور، يجب أن يكون على الأقل 1 وعلى الأكثر VkImageFormatProperties::maxArrayLayers، وفي حال كان flags يحتوي على VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT فيجب أن تكون 6، وفي حال كانت الصورة ثلاثية الأبعاد VK_IMAGE_TYPE_3D فيجب أن تكون 1
  • samples يمثل عدد العينات التي تريد أخذها لتنعيم الصورة وتقليل التكسير، تأخذ VK_SAMPLE_COUNT_1_BIT، أو VK_SAMPLE_COUNT_2_BIT، أو VK_SAMPLE_COUNT_4_BIT هكذا نضاعف عدد العينات إلى VK_SAMPLE_COUNT_64_BIT
  • tiling يشير لترتيب بيانات الصورة في الذاكرة، حيث تأخذ قيمتين فقط وهما VK_IMAGE_TILING_OPTIMAL والتي تعني ترك الخيار للجهاز ليقرر أفضل ترتيب والذي قد يختلف بين جهاز آخر، أو VK_IMAGE_TILING_LINEAR حيث ترتب بيانات الصورة صف يلي صف من أعلى لأسفل، وتستخدم هذه الأخيرة لنقل الصور من وإلى المضيف، هناك قيود على استخدام VK_IMAGE_TILING_LINEAR، أنظر لآخر هذا القسم
  • usage راية بتات تحدد الهدف من إنشاء الصورة، هناك عدّة إستخدامات يمكنك الإختيار منها وتركيبها:
    • VK_IMAGE_USAGE_TRANSFER_SRC_BIT لإستخدام الصورة كمصدر تنقل منه باستخدام طابور النقل
    • VK_IMAGE_USAGE_TRANSFER_DST_BIT لإستخدام الصورة كمصدر تنقل إليه باستخدام طابور النقل
    • VK_IMAGE_USAGE_SAMPLED_BIT لإنشاء VkImageView يمكننا من خلاله القراءة من الصورة في المظللات
    • VK_IMAGE_USAGE_STORAGE_BIT لإنشاء VkImageView يمكننا من خلاله الكتابة على الصورة في المظللات
    • VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT لإنشاء صورة إخراج تستخدم كذاكرة ألوان، وهي الذاكرة التي تكتب عليها عند تصيير الصورة النهائي
    • VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT لإنشاء صورة إخراج تستخدم كذاكرة تخزين لإختبار العمق أو كشبلونة أو الإثنين معاً
    • VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT لإنشاء نوع خاص من الصور تحجز ذاكرته عن الحاجة لإستخدامها كذاكرة وسيطة بين مراحل التصيير في التصيير متعدد المراحل
    • VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT لإنشاء صورة إخراج يمكن أيضاً إستخدامها كمدخل مع إستخدامها الأصلي، مثلاً يمكنك ادخال ذاكرة العمق لإستخدامها في المظلل
  • بالنسبة لأعضاء sharingMode وqueueFamilyIndexCount وpQueueFamilyIndices فراجع شرحها في VkBufferCreateInfo
  • initialLayout، صحيح أن هذا المتغير نوعه VkImageLayout، إلا أن هذا العضو يأخذ أحد قيمتين فقط وهي إما VK_IMAGE_LAYOUT_UNDEFINED والتي تعني أن محتويات الصورة الأولية غير محددة، بمعنى أن من يستخدمها يجب أن يتجاهل محتوياتها في أول إستخدام لأنها لم يتم تهيئتها وقد تحتوي قيم عشوائية، أما القيمة الأخرى هي VK_IMAGE_LAYOUT_PREINITIALIZED وتعني أن الصور مهيأة، باقي القيم الأخرى غير مستخدمة عند إنشاء الصورة بل تستخدم في أماكن أخرى.

في حال كانت قيمة tiling تساوي VK_IMAGE_TILING_LINEAR فإن Vulkan تضمن فقط أن هذه الإعدادات مدعومة:

  • imageType تساوي VK_IMAGE_TYPE_2D
  • format ليس أحد التنسيقات المستخدمة للعمق أو الشبلونة
  • mipLevels تساوي 1
  • arrayLayers تساوي 1
  • samples تساوي 1
  • usage تساوي VK_IMAGE_USAGE_TRANSFER_SRC_BIT أو VK_IMAGE_USAGE_TRANSFER_DST_BIT أو كليهما

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

أنواع التنسيقات في VkFormat

هناك أكثر من 180 تنسيق للصور في VkFormat، شرحها كلها غير عملي، معظم هذه التنسيقات إختياري وقد لايدعمها إلا عدد قليل من الأجهزة، ولن تستخدمها كلها، فبعضها متقارب ويمكنك أن تحدد عدد معيّن من هذه التنسيقات لتستخدمها وغالباً ستختار الأكثر دعماً، إنظر إلى قاعدة Vulkan Hardware Database[٤]، والبعض الآخر من هذه التنسيقات له إستخدامها محددة.

كل هذه التنسيقات تبدأ بالسابقة VK_FORMAT_، أولها VK_FORMAT_UNDEFINED والتي تعني تنسيق غير محدد، تستخدمها بعض إضافات Vulkan كقيمة غير محددة.

بعض التنسيقات تحتوي على أحرف مركّبات[٥] مثل R8G8B8A8 و B5G6R5 تقرأ من اليسار لليمين، حيث R المركب الأول، و G الثاني، و B الثالث و A الرابع، أنت من تحدد معنى هذه المركبات، والرقم الذي يليها عدد البتات المخصصة لهذا المركب، معظم الأحيان ستستخدم R للأحمر G للأخضر و B للأزرق و A للشفافية، مثلاً B8G8R8 تعني أن المركب الأول في البايت الثالث، و المركب الثالث في البايت الأول، بعض التنسيقات مثل هناك مرّكب واحد R8 و تعني أن هناك مركّبين R8G8، التنسيق ذو المركب الواحد R8 قد يستفاد منه لتخزين الصور ذات تدرج أحادي من 8 بت مثل الرمادي[٦].

بعض الأنواع تحتوي على UNORM، حيث تعني أن هذا العدد عدد عشري موجب مضغوط، مثلاً R8_UNORM لو كان يحتوي 3 فسيفسّر على أنه R = 3.0/255.0، بينما R16G16_UNORM واحتوى على 120 في R و 2000 في G، فإن R =120.0/65535.0 و G =2000.0/65535.0، القيمة دوماً بين [0.0, 1.0]، بينما SNORM فهي لتمثيل الأعداد الكسرية ذات الإشارة، ومداها بين [-1.0, 1.0]، مثلاً لو احتوى R8_SNORM على 111110112، بعد أخذ المتممة الثنائية للعدد وتحويله إلى سالب فستفسّر R = -5.0/255.0.

تعني UINT أن العدد عدد صحيح موجب، وتعني SINT أن العدد صحيح بإشارة، وتعني USCALED عدد عشري موجب سيفسّر على أنه عدد كسري جزئه الكسري صفر، مثلاً R8_USCALED لو احتوت على 5 فستفسّر 5.0، ونفس الشيء مع SSCALED والتي تحتوي على عدد بإشارة، وتشير UFLOAT إلى عدد كسري موجب و SFLOAT لعدد كسري بإشارة.

تعني PACK مثل PACK16 أن المرّكبات مخزنة في مجموعة بيانات واحدة مثلاً uint16_t بدلاً من تخزين المركبات كبيانات منفصلة مثلاً uint8_t[2].

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

التنسيقات المنتهية باللاحقة BLOCK تمثّل تنسيقات مضغوطة، وتلزم Vulkan بدعم على الأقل أحد عائلة تلك التنسيقات المضغوطة، تلك المبدوءة بالسابقة VK_FORMAT_BC* عائلة تنسيقات مضغوطة تدعمها معظم الأجهزة خصوصاً تلك التي تدعم Direct3D، بينما VK_FORMAT_ETC2_* وVK_FORMAT_EAC_* وVK_FORMAT_ASTC_* عوائل تنسيقات مضغوطة شائعة على المعالجات الرسومية في الجوالات والأنظمة المدمجة، جميع التنسيقات المضغوطة هذه تضيع شيء من جودة الصورة ولاتضغط الصورة كثيراً، لكنها تقلل الصورة إلى مثلاً 4 مرات مع سرعة فك ضغط عالية جداً يجعل عملية فك الضغط رخيصة، بل على العكس تسرع الأداءً عن طريق تقليل كمية البيانات المنقولة، بعض هذه الصيغ لاتدعم الشفافية أو لاتصلح لأنواع معينة من الصور كالتدرجات اللونية، سنطرق لضغط الإكساءات في مقالات منفصلة.

تبقى بعض التنسيقات التي تحتوي حرف D مثل VK_FORMAT_D32_SFLOAT وهذه تستخدم كذاكرة عمق، والتي تحتوي S مثل VK_FORMAT_D24_UNORM_S8_UINT تستخدم كذاكرة شبلونة stencil، ويعني X في التنسيق VK_FORMAT_X8_D24_UNORM_PACK32 أن هناك 8 بتات غير مستخدمة.

بعض تلك الصيغ كما أشرنا إلزامية في بعض الإستخدامات، ولو نظرت قاعدة بيانات التنسيقات في الموقع Vulkan Hardware Database ستميزها بأنها تلك التنسيقات التي تدعمها 94%-100% من الأجهزة، النسبة المتبقية من تقارير أجهزة تستخدم مشغلات قديمة، ويمكن الإطلاع على الجداول في القسم Required Format Support (تحذير، حجم الصفحة يقارب 5 ميجابايت) في وثائق Vulkan، التنسيقات في الصفوف المشار إليها بالرمز ✓ لابد أن تكون متوفرة للإستخدام المحدد في العمود، أما التنسيقات المشار إليها بالرمز † لابد أن يدعم المزود أحدها على الأقل، مثلاً للعمق لابد أن يوفر المزود التنسيق VK_FORMAT_D16_UNORM وعلى الأقل أحد هذه التنسيقات: VK_FORMAT_X8_D24_UNORM_PACK32 أو VK_FORMAT_D32_SFLOAT أو VK_FORMAT_D24_UNORM_S8_UINT أو VK_FORMAT_D32_SFLOAT_S8_UINT.

التحقق من دعم التنسيق

للتحقق من دعم تنسيق معيّن وحدود استخدامه يمكن الاستفادة من الدالة vkGetPhysicalDeviceImageFormatProperties():

VkResult vkGetPhysicalDeviceImageFormatProperties(
    VkPhysicalDevice                            physicalDevice,
    VkFormat                                    format,
    VkImageType                                 type,
    VkImageTiling                               tiling,
    VkImageUsageFlags                           usage,
    VkImageCreateFlags                          flags,
    VkImageFormatProperties*                    pImageFormatProperties);

دعم التنسيق خاصية من خصائص الجهاز الفيزيائي، لذا تحتاج لتمرير VkPhysicalDevice الذي استخدمته عند إنشاء VkDevice، في باقي معاملات الدالة ستمرر نفس الخصائص التي ستمررها للبنية VkImageCreateInfo، في المعامل الأخير مرر مؤشّر للبنية VkImageFormatProperties التي تحتوي على تلك المعلومات، هذه المعلومات قد تخلف باختلاف الإعدادات التي مررتها للدالة:

typedef struct VkImageFormatProperties {
    VkExtent3D            maxExtent;
    uint32_t              maxMipLevels;
    uint32_t              maxArrayLayers;
    VkSampleCountFlags    sampleCounts;
    VkDeviceSize          maxResourceSize;
} VkImageFormatProperties;
  • maxExtent يمثّل الأبعاد القصوى للصورة التي يمكن استخدامها
  • maxMipLevels يمثّل العدد الأقصى للصور المصغرة التي يمكن توليدها، في حال كانت tiling تساوي VK_IMAGE_TILING_LINEAR فستكون دوماً 1، عدا هذا فستكون floor( log2( max(maxExtent.width, maxExtent.height, maxExtent.depth) ) ) + 1
  • maxArrayLayers العدد الأقصى للمصفوفات التي يمكن توليدها، ستكون دوماً 1 أو تساوي أو أقل من VkPhysicalDeviceLimits::maxImageArrayLayers، ستكون 1 فقط في حال كانت tiling تساوي VK_IMAGE_TILING_LINEAR أو كانت type تساوي VK_IMAGE_TYPE_3D
  • sampleCounts راية بتات للعينات التي يمكن استخدامها
  • maxResourceSize الحجم الأقصى للصورة بالبايت شاملة جميع الموارد التابعة لهذه الصورة مثل المصفوفات والصور المصغّرة

في حال لم تعيد الدالة القيمة VK_SUCCESS، في حال فشلها ستعيد قيمة أخرى، في حال كانت VK_ERROR_FORMAT_NOT_SUPPORTED فهذا يعني أن هذا التنسيق غير مدعوم.

الربط بين الصورة والذاكرة المحجوزة

عندما نريد الربط بين كائن VkImage وبين جزء من الذاكرة التي حجزناها باستخدام vkAllocateMemory() فإننا سنستخدم الدالة vkBindImageMemory():

VkResult vkBindImageMemory(
    VkDevice                                    device,
    VkImage                                     image,
    VkDeviceMemory                              memory,
    VkDeviceSize                                memoryOffset);

حيث تأخذ في المعامل الأول الكائن VkDevice الخاص بالجهاز، وكائن الصورة VkImage، وكائن الذاكرة المحجوزة على الجهاز VkDeviceMemory، وتأخذ في المعامل الأخير memoryOffset وهو الإزاحة من بداية عنوان الذاكرة المحجوزة، الإزاحة يجب أن تكون بمضاعفات المحاذاة المطلوبة للصورة والمحددة في العضو VkMemoryRequirements::alignment التي أعطتك إياها vkGetImageMemoryRequirements()، بمجرد ربطك الصورة مع الذاكرة فلن يمكنك فك هذا الإرتباط إلا بهدم الصورة.


هدم الكائن VkImage

لهدم الكائن VkImage بعد التأكد من أنها لم تعد بالإستخدام استدع الدالة vkDestroyImage():

void vkDestroyImage(
    VkDevice                                    device,
    VkImage                                     image,
    const VkAllocationCallbacks*                pAllocator);

متطلبات الذاكرة للصور

الحصول على متطلبات الذاكرة للصور مشابهة تماماً لما عملنا مع الذاكرة الوسيطة VkBuffer، وذلك باستخدام الدالة vkGetImageMemoryRequirements():

void vkGetImageMemoryRequirements(
    VkDevice                                    device,
    VkImage                                     image,
    VkMemoryRequirements*                       pMemoryRequirements);

تحتوي البنية VkMemoryRequirements وهي نفسها البنية التي تحدثنا عنها سابقاً عند حديثنا عن الدالة vkGetBufferMemoryRequirements().

في حال كانت الصورة VK_IMAGE_TILING_LINEAR فتضمن Vulkan أن أحد أنواع الذاكرة في memoryTypeBits يشير لنوع ذاكرة متناسقة VK_MEMORY_PROPERTY_HOST_COHERENT_BIT ومرئية للمضيف VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT في نفس الوقت، وتضمن أيضاً أحد أحد هذه الأنواع محلّي للجهاز VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT.

ربما من الأفضل استخدام صور أبعادها بمضاعفات 2 مثل 256 بكسل أو 512 بكسل، سواءً بتصغير أبعادها لأقرب مضاعف 2 لتقليل الذاكرة الضائعة بسبب المحاذاة، أو بتكبيرها لأقرب مضاعف 2 والاستفادة من المساحة الضائعة لزيادة الدقة.

نقل البيانات للصورة

هناك ثلاثة طرق ننقل معلومات التكسلات للصورة، طريقتين تستخدم طابور النقل سنفصّلها لاحقاً، وطريقة نقل مباشرة باستخدام ذاكرة المضيف:

  • يمكن نقل البيانات للصورة عن طريق إنشاء كائن VkBuffer يمثل مصدر نقل VK_BUFFER_USAGE_TRANSFER_SRC_BIT، ثم ننقل محتوياته للصورة الوجهة VK_IMAGE_USAGE_TRANSFER_DST_BIT باستخدام الدالة vkCmdCopyBufferToImage() عبر طابور النقل، هذه الطريقة قد تكون الأفضل خصوصاً لأننا غالباً نريد نقل بيانات لذاكرة محليّة على ذاكرة قد لاتكون مرئية من المضيف دون الدخول في تعقيد استخدام vkCmdBlitImage()
  • الطريقة الأخرى باستخدام الدالة vkCmdBlitImage()، الدالة ذات استخدامات كثيرة، لكن يمكننا استخدامها لنسخ البيانات من صورة موجودة، والنقل يتم عبر طابور النقل، تعقيدها وتوفّر خيارات أبسط قد لايجعلها الخيار الأنسب لهذا الغرض
  • الطريقة الثالثة عن طريق إنشاء صورة خطيّة VK_IMAGE_TILING_LINEAR وننقل البيانات لها، قد نستخدمها كمرحلة أولية قبل vkCmdBlitImage() في حال أرنا النقل لذاكرة محلية على الجهاز غير مرئية للمضيف، سنوضّح هنا طريقة الكتابة للصورة الخطية فقط ونؤجل الطرق الباقية إلى أن نتحدث عن الطوابير

في البداية وقبل الكتابة للصورة يلزمنا ذاكرة على الجهاز سبق حجزها باستخدام vkAllocateMemory() من ذاكرة مرئية للمضيف، تلك الذاكرة يجب أن تفي بمتطلبات ذاكرة الصورة التي تحددها الدالة vkGetImageMemoryRequirements()، بعدها يلزمنا ربط الذكرة المحجوز بذاكرة المضيف باستخدام vkMemoryMap()، قبل أن نكتب للصورة نحتاج لمعرفة كيف نتنقل مثلاً بين صفوف الصورة، فلو حجزت مثلاً صورة تنسيقها R8G8B8A8 وعرضها 7 بكسل وبارتفاع معين، فحجم الصف البكسلات الحقيقي سيكون 7 × 4 = 28 بايت، إلا أنه ممكن أن تحتاج الصورة لعمود إضافي لأجل المحاذاة بحيث يكون حجم الصف 8 × 7 = 32 بايت، وكذلك بالنسبة لمصفوفات الصور والصور الثلاثية الأبعاد فوجود المحاذاة يجعل استخدام الأبعاد غير دقيق للتنقل بين صفوف الصورة أو مصفوفات الصور والطبقات في الصور الثلاثية الأبعاد، فكما أشرنا فالصورة مورد يحتوي على موارد فرعية مثل الصور المصغّرة ومصفوفات صور، تلك المعلومات يمكن الحصول عليها باستخدام الدالة vkGetImageSubresourceLayout():

void vkGetImageSubresourceLayout(
    VkDevice                                    device,
    VkImage                                     image,
    const VkImageSubresource*                   pSubresource,
    VkSubresourceLayout*                        pLayout);

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

البنية VkImageSubresource تحتوي على معلومات المورد الفرعي الذي تريد الاستعلام عنه:

typedef struct VkImageSubresource {
    VkImageAspectFlags    aspectMask;
    uint32_t              mipLevel;
    uint32_t              arrayLayer;
} VkImageSubresource;
  • aspectMask بعض تنسيقات الصور قد تستخدم لأكثر من غرض في نفس الوقت مثلاً لتخزين العمق و الشبلونة في نفس الوقت، بعض الأجهزة تخزن كلاً من تلك المعلومات في ذاكرة منفصلة، لذا هذا العضو يستخدم لتحديد أي غرض تريد الاستعلام عنه، حيث تأخذ أحد هذه القيم فقط (حتى لو كانت الصيغة تستخدم للعمق والشبلونة في نفس الوقت يجب أن تختار أحدهما فقط هنا):
    • VK_IMAGE_ASPECT_COLOR_BIT للون
    • VK_IMAGE_ASPECT_DEPTH_BIT للعمق
    • VK_IMAGE_ASPECT_STENCIL_BIT للشبلونة
    • VK_IMAGE_ASPECT_METADATA_BIT للبيانات الوصفية، تستخدم مع الموارد المحمّلة جزئياً
  • mipLevel في حال كانت الصورة تحتوي على عدّة صور مصغّرة فحدد هنا أي صورة مصغّرة تريد الاستعلام عنها، مثلاً 0 للصورة الأولى، و 1 للصورة الثانية
  • arrayLayer في حال كانت الصورة مصفوفة صور، فحدد هنا أي صورة تريد الاستعلام عنها من مصفوفة الصور

كوننا نسّتعلم عن صورة خطية ذات صورة واحدة وصورة مصغّرة واحدة فخياراتنا محدودة بوضع VK_IMAGE_ASPECT_COLOR_BIT في aspectMask و 0 في mipLevel و arrayLayer.

البنية VkSubresourceLayout تحتوي على معلومات الذاكرة الخاصة بمورد الصورة المحدد في VkImageSubresource:

typedef struct VkSubresourceLayout {
    VkDeviceSize    offset;
    VkDeviceSize    size;
    VkDeviceSize    rowPitch;
    VkDeviceSize    arrayPitch;
    VkDeviceSize    depthPitch;
} VkSubresourceLayout;

لو افترضنا أننا استخدمنا vkMemoryMap() وربطنا عنوان الذاكرة عند عنوان uint8_t *p فإن:

  • offset تحدد موقع بداية أول بيانات الذاكرة من بعد هذا العنوان بالبايت، p + subresourceLayout.offset ستحدد أول تكسل في الصورة
  • size ستحدد حجم بيانات الصورة الكلي
  • rowPitch ستحدد بداية الصف التالي في الصورة من عند بداية الصف السابق، لو كنا عند أول تكسل في الصورة ونريد الانتقال للتكسل الأول في الصف الثاني فإن موقعه سيكون p + subresourceLayout.offset + subresourceLayout.rowPitch
  • arrayPitch يحدد موقع بداية الصورة التالية في حال كان كائن الصورة خاص بمصفوفة صور، موقع الصورة التالية سيكون p + subresourceLayout.offset + subresourceLayout.arrayPitch
  • depthPitch في حال كانت الصورة ثلاثية الأبعاد فسيشير هذا العضو لبداية صورة الطبقة التالية، موقع صورة الطبقة التالية سيكون p + subresourceLayout.offset + subresourceLayout.depthPitch

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

مثال

في هذا المثال سننشئ صورة خطية عرضها 7 بكسلات و ارتفاعها 3 بكسل بتنسيق R8G8B8A8_SRGB ونريد جعل كل تكسلاتها حمراء:

  // أبعاد الصورة المراد حجزها
  static const uint32_t width = 7;
  static const uint32_t height = 3;

  // معلومات صورة R8G8B8A_SRGB خطية
  VkImageCreateInfo imageCreateInfo{};
  imageCreateInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
  imageCreateInfo.pNext = nullptr;
  imageCreateInfo.flags = 0;
  imageCreateInfo.imageType = VK_IMAGE_TYPE_2D;
  imageCreateInfo.format = VK_FORMAT_R8G8B8A8_SRGB;
  imageCreateInfo.extent = {width, height, 1};
  imageCreateInfo.mipLevels = 1;
  imageCreateInfo.arrayLayers = 1;
  imageCreateInfo.samples = VK_SAMPLE_COUNT_1_BIT;
  imageCreateInfo.tiling = VK_IMAGE_TILING_LINEAR;
  imageCreateInfo.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
  imageCreateInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
  imageCreateInfo.queueFamilyIndexCount = 0;
  imageCreateInfo.pQueueFamilyIndices = nullptr;
  imageCreateInfo.initialLayout = VK_IMAGE_LAYOUT_PREINITIALIZED;

  // إنشاء كائن الصورة
  VkImage image;
  result = vkCreateImage(device, &imageCreateInfo, nullptr, &image);
  assert(result == VK_SUCCESS);

  // الاستعلام عن متطلبات الذاكرة الكافية للصورة
  VkMemoryRequirements imageMemoryRequirements;
  vkGetImageMemoryRequirements(device, image, &imageMemoryRequirements);

  // البحث عن ذاكرة مرئية ومتناسقة بين أنواع الذواكر المقترحة
  static const uint32_t imagePreferredMemoryType =
    VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
  uint32_t imageMemoryTypeIndex;

  for (imageMemoryTypeIndex = 0;
       imageMemoryTypeIndex < physicalDeviceMemoryProperties.memoryTypeCount; ++imageMemoryTypeIndex)
  {
    if ((imageMemoryRequirements.memoryTypeBits & (1 << imageMemoryTypeIndex))
        && (physicalDeviceMemoryProperties.memoryTypes[imageMemoryTypeIndex].propertyFlags & imagePreferredMemoryType))
    {
      break;
    }
  }

  // حجز الذاكرة على الجهاز بحجم الصورة
  VkDeviceMemory imageDeviceMemory;

  VkMemoryAllocateInfo imageMemoryAllocateInfo{};
  imageMemoryAllocateInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
  imageMemoryAllocateInfo.pNext = nullptr;
  imageMemoryAllocateInfo.allocationSize = imageMemoryRequirements.size;
  imageMemoryAllocateInfo.memoryTypeIndex = imageMemoryTypeIndex;

  result = vkAllocateMemory(device, &imageMemoryAllocateInfo, nullptr, &imageDeviceMemory);
  assert(result == VK_SUCCESS);

  // الربط بين الذاكرة وكائن الصورة
  vkBindImageMemory(device, image, imageDeviceMemory, 0);

  // الاستعلام عن تركيبة الصورة تمهيداً للكتابة إليها
  VkImageSubresource imageSubresource{};
  imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
  imageSubresource.mipLevel = 0;
  imageSubresource.arrayLayer = 0;

  VkSubresourceLayout subresourceLayout{};
  vkGetImageSubresourceLayout(device, image, &imageSubresource, &subresourceLayout);

  // ربط ذاكرة الجهاز مع المضيف
  uint8_t* mappedImageMemoryAddress;
  vkMapMemory(device, imageDeviceMemory, 0, imageMemoryRequirements.size, 0,
              reinterpret_cast<void**>(&mappedImageMemoryAddress));

  // نحرك المؤشر لبداية الصورة
  mappedImageMemoryAddress += subresourceLayout.offset;

  for (uint32_t y = 0; y < height; ++y)
  {
    struct R8G8B8A8_UNORM
    {
      uint8_t r;
      uint8_t g;
      uint8_t b;
      uint8_t a;
    };

    // نحرك المؤشر لبداية صفّ الصورة
    R8G8B8A8_UNORM* row = reinterpret_cast<R8G8B8A8_UNORM*>(mappedImageMemoryAddress + y * subresourceLayout.rowPitch);

    // نريد جعل التكسلات حمراء
    for (uint32_t x = 0; x < width; ++x)
    {
      row[x].r = 0xff;
      row[x].g = 0x00;
      row[x].b = 0x00;
      row[x].a = 0xff;
    }
  }

  // نفك الربط بين ذاكرة الجهاز والمضيف
  vkUnmapMemory(device, imageDeviceMemory);

الكائنين VkBufferView و VkImageView

كائنات VkView* تسمح لك بإعادة تفسير بيانات الذاكرة من ذاكرة وسيطة وصور، مثلاً تسمح لك بالقراءة من الذاكرة الوسيطة كأنك تقرأ تكسلات، وتسمح لك بالقراءة من الصور بصيغة أخرى مختلفة عن الصيغة الأصلية، الذاكرة الوسيطة VkBuffer يمكن قراءتها مباشرة من المظللات ولست بحاجة للكائن VkBufferView إلا لو أدرت قراءتها كتكسلات، لكن لقراءة الصور VkImage أنت بحاجة لإنشاء الكائن VkImageView.

تستهدف Vulkan نطاق عريض من المستخدمين لذا توفر VkBufferView و VkImageView مرونة كبيرة لعمل الكثير من الأشياء التي قد لاتهم المستخدم الذي يريد فقط تصيير مجسم ثلاثي الأبعاد.

الكائن VkBufferView

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

لإنشاء الكائن نستخدم الدالة vkCreateBufferView():

VkResult vkCreateBufferView(
    VkDevice                                    device,
    const VkBufferViewCreateInfo*               pCreateInfo,
    const VkAllocationCallbacks*                pAllocator,
    VkBufferView*                               pView);

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

البنية VkBufferViewCreateInfo:

typedef struct VkBufferViewCreateInfo {
    VkStructureType            sType;
    const void*                pNext;
    VkBufferViewCreateFlags    flags;
    VkBuffer                   buffer;
    VkFormat                   format;
    VkDeviceSize               offset;
    VkDeviceSize               range;
} VkBufferViewCreateInfo;
  • sType يجب أن يكون VK_STRUCTURE_TYPE_BUFFER_VIEW_CREATE_INFO و pNext يجب أن يكون NULL، العضو flags غير مستخدم حالياً، ضعه صفر
  • buffer ضع به الكائن VkBuffer، كما أشرنا يجب أن تكون أنشأت VkBuffer مع الاستخدام VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT أو VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT، الكائن VkBuffer يجب أن يكون مرتبط بذاكرة باستخدام vkBindBufferMemory() قبل استدعاء vkCreateBufferView()
  • format يشير لتنسيق التكسل
  • offset تشير للإزاحة بالبايت من بداية الذاكرة الوسيطة، ضعه صفر للبدء من بداية الذاكرة، عدى هذا فيجب أن تكون الإزاحة من مضاعفات VkPhysicalDeviceLimits::minTexelBufferOffsetAlignment
  • range يشير لحجم بيانات الذاكرة من بعد الإزاحة، يمكنك وضع VK_WHOLE_SIZE لاستخدام كامل الذاكرة المتبقية من بعد offset، عدى هذا فيجب أن تتأكد أن offset + range أقل أو يساوي مساحة الذاكرة، ويجب أن تتأكد أن range من مضاعفات حجم التنسيق وأنه أقل أو يساوي VkPhysicalDeviceLimits::maxTexelBufferElements

مثال لإعادة تفسير ذاكرة VkBuffer كعدد float:

  VkBufferViewCreateInfo bufferViewCreateInfo{};
  bufferViewCreateInfo.sType = VK_STRUCTURE_TYPE_BUFFER_VIEW_CREATE_INFO;
  bufferViewCreateInfo.pNext = nullptr;
  bufferViewCreateInfo.flags = 0;
  bufferViewCreateInfo.buffer = buffer;
  bufferViewCreateInfo.format = VK_FORMAT_R32_SFLOAT;
  bufferViewCreateInfo.offset = 0;
  bufferViewCreateInfo.range = VK_WHOLE_SIZE;

  VkBufferView bufferView;
  result = vkCreateBufferView(device, &bufferViewCreateInfo, nullptr, &bufferView);
  assert(result == VK_SUCCESS);

الكائن VkImageView

الصور VkImage لايمكن استخدامها مباشرة في المظللات، بل نحتاج لإنشاء كائن VkImageView من الصورة ومنه نستخدم الصورة، في حال أنشأت الصورة VkImage باستخدام VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT فيمكنك إعادة تفسير التنسيق لتنسيق آخر في حال احتجت لعمل هذا كأن تقرأ البيانات المضغوطة مباشرة دون فك ضغطها أو تقرأ الصورة SRGB مباشرة دون فك ترميز جاما، لإنشاء الكائن VkImageView نستخدم الدالة vkCreateImageView():

VkResult vkCreateImageView(
    VkDevice                                    device,
    const VkImageViewCreateInfo*                pCreateInfo,
    const VkAllocationCallbacks*                pAllocator,
    VkImageView*                                pView);

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

البنية VkImageViewCreateInfo تحتوي على معلومات الكائن VkImageView وأعضاؤها:

typedef struct VkImageViewCreateInfo {
    VkStructureType            sType;
    const void*                pNext;
    VkImageViewCreateFlags     flags;
    VkImage                    image;
    VkImageViewType            viewType;
    VkFormat                   format;
    VkComponentMapping         components;
    VkImageSubresourceRange    subresourceRange;
} VkImageViewCreateInfo;
  • sType يجب أن يكون VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO و pNext يجب أن يكون NULL، العضو flags غير مستخدم حالياً، ضعه صفر
  • image ضع به كائن VkImage، هذا الكائن يجب أن يكون مرتبط بذاكرة باستخدام vkBindImageMemory() قبل استدعاء vkCreateImageView()
  • viewType تحدد أبعاد كائن VkImageView الذي تريد إنشاءه، اطلع على VkImageViewType ستجد قائمة بالخيارات المتاحة، واطلع على الجدول في وثائق VkImageViewCreateInfo، اختيار القيمة المناسبة نسبياً بسيط:
    • يمكنك أن تختار أن تفسّر صورة VK_IMAGE_TYPE_2D على أنها VK_IMAGE_VIEW_TYPE_2D أو على أنها مصفوفة VK_IMAGE_VIEW_TYPE_2D_ARRAY ذات عنصر واحد، ولو كانت الصورة مصفوفة صور VK_IMAGE_TYPE_2D قيمة arrayLayers > 1 فيمكنك إنشاء VK_IMAGE_VIEW_TYPE_2D لأحد صور المصفوفة أو إنشاء VK_IMAGE_VIEW_TYPE_2D_ARRAY لكل أو بعض الصور، نفس المبدأ ينطبق على الصور الأحادية الأبعاد
    • الصور الثلاثية الأبعاد VK_IMAGE_TYPE_3D يمكن فقط إنشاء VK_IMAGE_VIEW_TYPE_3D منها
    • يمكن استخدام مصفوفة VK_IMAGE_TYPE_2D من ستة صور على الأقل لإنشاء VK_IMAGE_VIEW_TYPE_CUBE وذلك لاستخدام الصور تلك كخريطة مكعبة، ولإنشاء مصفوفة خرائط مكعبة VK_IMAGE_VIEW_TYPE_CUBE_ARRAY يجب أن يكون عدد الصور من مضاعفات 6 على الأقل، مثلاً 13 صورة تمكنك من إنشاء مصفوفة فيها خريطتين مكعبة، الزيادات ستتجاهلها لاحقاً في العضو subresourceRange، تلك الصورة يجب أن تكون في كل الحالتين قد أنشأت كائن الصورة VkImage مع رفع الراية VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT كما أشرنا سابقاً
  • format التنسيق المستخدم مع الكائن VkImageView يجب أن يكون نفسه المستخدم مع VkImage مالم تنشئ الصورة باستخدام الراية VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT كما أشرنا لاحقاً، حينها يمكنك استخدام أي تنسيق متوافق، اطلع هنا على التنسيقات المتوافقة Format Compatibility Classes (تحذير، حجم الصفحة يقارب 5 ميجابايت)
  • العضو components مركبة من البنية VkComponentMapping والتي تتكون من أربعة أعضاء وهم r و g و b و a كلها تأخذ أحد قيم VkComponentSwizzle، حيث تسمح لك بالتلاعب في ترتيب المركبات RGBA والقيم التي تعطيك إياها عند القراءة منها، مثلاً يمكنك التبديل بين قيم المركبات أو جعل قيمها تأخذ مركبات أخرى، هذه الأخيرة مفيدة مع تنسيقات الصور ذات المركب والمركبين، ويمكنك أن تجعل أحد المركبات تعطي دوماً صفر أو تجعلها تعطي دوماً واحد، أعضاء VkComponentSwizzle:
    • VK_COMPONENT_SWIZZLE_IDENTITY تجعل المركب يأخذ قيمته الأصلية، في حال كانت الصورة مستخدمة للكتابة كصور التخزين فيجب أن تضع هذه القيمة في كل المركبات، يمكنك التلاعب في القراءة من المركبات فقط
    • VK_COMPONENT_SWIZZLE_ZERO تجعل المركب يعطي دوماً صفر
    • VK_COMPONENT_SWIZZLE_ONE تجعل المركب يعطي دوماً واحد
    • VK_COMPONENT_SWIZZLE_R تجعل المركب يعطي قيمة r الأصلية، وكذلك VK_COMPONENT_SWIZZLE_G و VK_COMPONENT_SWIZZLE_B و VK_COMPONENT_SWIZZLE_A
  • العضو subresourceRange ونوعه VkImageSubresourceRange يحدد الموارد الفرعية التي تريد استخدامها من VkImage، حيث تحدد نوع الصورة سواءً صورة لونية أو عمق أو شابلونة في حال كانت الصيغة المستخدمة لها أكثر من استخدام، وتمكنك كذلك من اختيار كل أو بعض الصور المصغّرة، وتمكنك أيضاً من اختيار كل أو بعض الصور في مصفوفات الصور، سنوضحها بعد قليل

البنية VkImageSubresourceRange أعضاؤها:

typedef struct VkImageSubresourceRange {
    VkImageAspectFlags    aspectMask;
    uint32_t              baseMipLevel;
    uint32_t              levelCount;
    uint32_t              baseArrayLayer;
    uint32_t              layerCount;
} VkImageSubresourceRange;
  • aspectMask سبق أن تحدثنا عنها عند حديثنا عن الدالة vkGetImageSubresourceLayout()، لكن استخدامها هنا مختلف، حيث تسمح لك باختيار فقط إما VK_IMAGE_ASPECT_COLOR_BIT لصور التنسيقات اللونية، أو VK_IMAGE_ASPECT_DEPTH_BIT للعمق أو VK_IMAGE_ASPECT_STENCIL_BIT للشبلونة، وفي حال كانت الصيغة صيغة عمق وشبلونة في نفس الوقت فيمكنك دمج الرايتين الأخيرتين VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT، لكن لايمكن استخدام VK_IMAGE_ASPECT_METADATA_BIT الخاصة بالموارد المحمّلة جزئياً
  • baseMipLevel تحدد بها مؤشر أو صورة مصغّرة تريد استخدامها، غالباً ستضع صفر لأول صورة مصغّرة
  • levelCount تحدد به عدد الصور المصغّرة التي تريد استخدامها من بعد أول صورة حددتها في baseMipLevel، عددها بعد أول صورة يجب أن لايزيد عن عدد الصور المصغّرة التي عرفتها لما أنشأت VkImage، يمكنك أن تضع هنا القيمة VK_REMAINING_MIP_LEVELS لاستخدام كل الصور المصغّرة المتبقية بغض النظر عن عددها
  • baseArrayLayer تحدد به مؤشر أول صورة في مصفوفة الصور التي تريد استخدامها
  • layerCount تحدد به عدد الصور من بعد baseArrayLayer، أو يمكنك فقط وضع VK_REMAINING_ARRAY_LAYERS لاستخدام كل الصور المتبقية

لو كانت VkImage تحتوي مثلاً على صورة واحدة نوعها VK_IMAGE_TYPE_2D وتريد إنشاء VkImageView نوعها VK_IMAGE_VIEW_TYPE_2D أو VK_IMAGE_VIEW_TYPE_2D_ARRAY لجعلها تبدو كمصفوفة ذات عنصر واحد فيمكنك في كل الحالتين وضع baseArrayLayer = 0 و layerCount = 1 أو layerCount = VK_REMAINING_ARRAY_LAYERS، أما لو أردت إنشاء VK_IMAGE_VIEW_TYPE_2D لأحد الصور التي تحتويها VkImage فيمكنك تحديد مؤشر تلك الصورة في baseArrayLayer ووضع layerCount = 1، يمكنك تطبيق نفس المبدأ على الصور الأحادية الأبعاد.

في حالة الخريطة المكعبة الواحدة VK_IMAGE_VIEW_TYPE_CUBE فيجب تحديد ستة صور من صورة VkImage تحتوي على الأقل ستة صور تبدأ من baseArrayLayer وتكون قيمة layerCount = 6، هذه الصور الستة تمثّل أوجه المكعب بهذا الترتيب [math]+X[/math] يليها [math]-X[/math] ثم [math]+Y[/math] ثم [math]-Y[/math] ثم [math]+Z[/math] ثم [math]-Z[/math]، في حال كانت مصفوفات خرائط مكعب VK_IMAGE_VIEW_TYPE_CUBE_ARRAY فيجب تحديد صورة VkImage تحتوي على الأقل عدد صور بمضاعفات الستة -تذكر أنه يمكنك مصفوفة بمكعب واحد-، مثلاً لإنشاء VkImageView تحتوي على مكعبين VK_IMAGE_VIEW_TYPE_CUBE_ARRAY من VkImage تحتوي على 13 صورة -تريد تجاهل الأخيرة- فضع baseArrayLayer = 0 و layerCount = 12.

الصور الثلاثية الأبعاد كما أشرنا عند VK_IMAGE_TYPE_3D و VK_IMAGE_VIEW_TYPE_3D، فيجب أن تضع viewType = VK_IMAGE_VIEW_TYPE_3D و baseArrayLayer = 0 و layerCount = 1.

الصور الخطية VK_IMAGE_TILING_LINEAR كما أشرنا لاتصلح للتصيير أو قد لايمكن استخدامها مع المظللات، بل تصلح فقط للنقل، لذا قد لاتريد أو لن تستطيع إنشاء كائن VkImageView منها، بل من صورة VK_IMAGE_TILING_OPTIMAL محجوزة على ذاكرة الجهاز، قد تؤجل استخدام VkImageView إلى أن نتحدث عن الطوابير.
  1. البكسلات في الصور المستخدمة لإكساء المجسمات في الرسوميات تسمى عادةً تكسلات، جمع تكسل texel وهي اختصار لعبارة texture element والتي تعني عنصر إكساء مثلما أن كلمة pixel اختصار محرّف لعبارة picture element التي تعني عنصر صورة، تستخدم تكسل للتمييز بين نقطة على صور وبين بكسل المستخدم للإشارة إلى نقطة على الشاشة.
  2. أغلب الواجهات البرمجية لاترسم سوى أشكال أساسية محددة، وهي النقاط والخطوط والمثلثات، لذا لرسم مربع فإننا نستسخدم مثلثين متقابلين، ستلاحظ أن هذين المثلثين يتشاركان نقطتين، لذا إما علينا أن نرسل 6 نقاط، أو يمكننا باستخدام مؤشرات النقاط أن نرسل فقط أربعة نقاط مع إرسال ستة مؤشرات كل منها تشير لأحد هذه النقاط، يمكننا هكذا توفير الذاكرة في المجسمات ذات النقاط الكبيرة، وكذلك لفوائدها على الأداء عن طريق إجراء الحسابات على كل نقطة مرة واحدة فقط.
  3. الرسم غير المباشر يعني أن أحد مظللاتك سيوفر معلومات الرسم لاحقاً بدلاً من تحديدها عند إرسال أمر الرسم، فمثلاً عند رسم الجسيمات مثل الشرار فيمكن رسم عدةّ نسخ من شرارة واحدة لتشكّل مجموعة شرارات بأمر واحد، تسمى هذه الطريقة بالرسم بالنسخ instanced drawing -وهي ليست موضوعنا لكن للتوضيح-، حتى تحدد عدد الشرارات ومواقعها يمكنك إجراء هذه الحسابات على المعالج الرئيسي ثم إرسال معلومات الرسم مع أمر الرسم، لكن يمكنك باستخدام الرسم الغير مباشرة indirect drawing يمكنك أن تنقل هذه الحسابات للمعالج الرسومي بحيث تستفيد من قدرته على المعالجة المتوازية وتجعل مظللك هو من يوفر معلومات الرسم.
  4. بعض التنسيقات يشار إلى أن أكثر من 94% من الأجهزة تدعمها، النسبة المتبقية غالباً ماتكون عائدة لمشغلات قديمة، لذا ابحث عن نفس الجهاز الذي لايدعمها، فستجد أن هناك إصدار حديث من المشغّل وفّر الدعم.
  5. قد لاتحتوي هذه المرّكبات على ألوان بمعنى ألوان، فقد تحتوي على معلومات، يمكنك مثلاً تخزن مركبات متجه ثلاثي الأبعاد في أحد هذه المركبات، وقتها لن تعني RGB لون بل مركبات متجه.
  6. في مظلل البكسلات التي تكتب نتيجتها النهائية على الشاشة مركبات RGB الخارجة ستعبر عن مركبات لونية، لعمل تدرج رمادي ستأخذ قيمة R المدخلة وتضعها في كل المركبات الخارجة ليظهر التدرج الرمادي.