طبقات التحقق من Vulkan على Android

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

مرحلة التحقّق الفردية من Khronos

في السابق، كانت Vulkan توفّر مراحل تحقّق متعددة كان يجب تفعيلها بترتيب معيّن. اعتبارًا من الإصدار 1.1.106.0 من حزمة Vulkan SDK، ما عليك سوى تفعيل مرحلة تحقّق واحدة، هي VK_LAYER_KHRONOS_validation، للحصول على جميع الميزات من مراحل التحقّق السابقة.VK_LAYER_KHRONOS_validation

استخدام مراحل التحقّق المضمّنة في حزمة APK

يضمن تضمين مراحل التحقّق في حزمة APK التوافق الأمثل. تتوفّر مراحل التحقّق كملفات ثنائية تم إنشاؤها مسبقًا أو يمكن إنشاؤها من رمز المصدر.

استخدام الملفات الثنائية التي تم إنشاؤها مسبقًا

نزِّل أحدث الملفات الثنائية لمرحلة التحقّق من Android Vulkan من صفحة الإصدار على GitHub.

أسهل طريقة لإضافة المراحل إلى حزمة APK هي استخراج الملفات الثنائية للمراحل التي تم إنشاؤها مسبقًا إلى دليل src/main/jniLibs/ في الوحدة، مع الإبقاء على أدلة واجهات ABI (مثل arm64-v8a أو x86-64)، على النحو التالي:

src/main/jniLibs/
  arm64-v8a/
    libVkLayer_khronos_validation.so
  armeabi-v7a/
    libVkLayer_khronos_validation.so
  x86/
    libVkLayer_khronos_validation.so
  x86-64/
    libVkLayer_khronos_validation.so

إنشاء مرحلة التحقّق من رمز المصدر

لتصحيح أخطاء رمز المصدر لمرحلة التحقّق، اسحب أحدث رمز مصدر من مستودع GitHub Khronos Groupواتّبِع تعليمات الإنشاء هناك.

التأكّد من تضمين مرحلة التحقّق بشكل صحيح

بغض النظر عمّا إذا كنت تنشئ باستخدام المراحل التي تم إنشاؤها مسبقًا من Khronos أو المراحل التي تم إنشاؤها من رمز المصدر، تؤدي عملية الإنشاء إلى إنشاء بنية ملف نهائية في حزمة APK على النحو التالي:

lib/
  arm64-v8a/
    libVkLayer_khronos_validation.so
  armeabi-v7a/
    libVkLayer_khronos_validation.so
  x86/
    libVkLayer_khronos_validation.so
  x86-64/
    libVkLayer_khronos_validation.so

يوضّح الأمر التالي كيفية التأكّد من أنّ حزمة APK تتضمّن مرحلة التحقّق على النحو المتوقّع:

$ jar -tf project.apk | grep libVkLayer
lib/x86_64/libVkLayer_khronos_validation.so
lib/armeabi-v7a/libVkLayer_khronos_validation.so
lib/arm64-v8a/libVkLayer_khronos_validation.so
lib/x86/libVkLayer_khronos_validation.so

تفعيل مرحلة التحقّق أثناء إنشاء مثيل

تتيح Vulkan API للتطبيق تفعيل المراحل أثناء إنشاء مثيل. يجب أن تتضمّن نقاط الدخول التي تعترضها إحدى المراحل أحد الكائنات التالية كالمَعلمة الأولى:

  • VkInstance
  • VkPhysicalDevice
  • VkDevice
  • VkCommandBuffer
  • VkQueue

استخدِم vkEnumerateInstanceLayerProperties() لعرض المراحل المتاحة وخصائصها. تفعِّل Vulkan المراحل عند تنفيذ vkCreateInstance().

يوضّح مقتطف الرمز التالي كيف يمكن لتطبيق استخدام Vulkan API للاستعلام عن المراحل وتفعيلها آليًا:

// Enable just the Khronos validation layer.
static const char *layers[] = {"VK_LAYER_KHRONOS_validation"};

// Get the layer count using a null pointer as the last parameter.
uint32_t instance_layer_present_count = 0;
vkEnumerateInstanceLayerProperties(&instance_layer_present_count, nullptr);

// Enumerate layers with a valid pointer in the last parameter.
VkLayerProperties layer_props[instance_layer_present_count];
vkEnumerateInstanceLayerProperties(&instance_layer_present_count, layer_props);

// Make sure selected validation layers are available.
VkLayerProperties *layer_props_end = layer_props + instance_layer_present_count;
for (const char* layer:layers) {
  assert(layer_props_end !=
  std::find_if(layer_props, layer_props_end, [layer](VkLayerProperties layerProperties) {
    return strcmp(layerProperties.layerName, layer) == 0;
  }));
}

// Create a Vulkan instance, requesting all enabled layers or extensions
// available on the system
VkInstanceCreateInfo instanceCreateInfo{
  .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
  .pNext = nullptr,
  .pApplicationInfo = &appInfo,
  .enabledLayerCount = sizeof(layers) / sizeof(layers[0]),
  .ppEnabledLayerNames = layers,

إخراج logcat التلقائي

تُصدر مرحلة التحقّق رسائل تحذير وأخطاء في logcat مصنّفة بعلامة VALIDATION. تبدو رسالة مرحلة التحقّق على النحو التالي (مع إضافة فواصل أسطر هنا لتسهيل التمرير):

Validation -- Validation Error:
  [ VUID-VkDeviceQueueCreateInfo-pQueuePriorities-parameter ]
Object 0: VK_NULL_HANDLE, type = VK_OBJECT_TYPE_DEVICE; | MessageID = 0xd6d720c6 |
vkCreateDevice: required parameter
  pCreateInfo->pQueueCreateInfos[0].pQueuePriorities specified as NULL.
The Vulkan spec states: pQueuePriorities must be a valid pointer to an array of
  queueCount float values
  (https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html
  #VUID-VkDeviceQueueCreateInfo-pQueuePriorities-parameter)

تفعيل معاودة الاتصال لتصحيح الأخطاء

تتيح إضافة Debug Utils VK_EXT_debug_utils لتطبيقك إنشاء أداة مراسلة لتصحيح الأخطاء تنقل رسائل مرحلة التحقّق إلى معاودة اتصال يوفّرها التطبيق. قد لا يطبّق جهازك هذه الإضافة، ولكن يتم تطبيقها في أحدث مراحل التحقّق. تتوفّر أيضًا إضافة مهملة تُعرف باسم VK_EXT_debug_report توفّر إمكانات مشابهة إذا لم تكن VK_EXT_debug_utils متاحة.

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

// Get the instance extension count.
uint32_t inst_ext_count = 0;
vkEnumerateInstanceExtensionProperties(nullptr, &inst_ext_count, nullptr);

// Enumerate the instance extensions.
VkExtensionProperties inst_exts[inst_ext_count];
vkEnumerateInstanceExtensionProperties(nullptr, &inst_ext_count, inst_exts);

// Check for debug utils extension within the system driver or loader.
// Check if the debug utils extension is available (in the driver).
VkExtensionProperties *inst_exts_end = inst_exts + inst_ext_count;
bool debugUtilsExtAvailable = inst_exts_end !=
  std::find_if(inst_exts, inst_exts_end, [](VkExtensionProperties
    extensionProperties) {
    return strcmp(extensionProperties.extensionName,
      VK_EXT_DEBUG_UTILS_EXTENSION_NAME) == 0;
  });

if ( !debugUtilsExtAvailable ) {
  // Also check the layers for the debug utils extension.
  for (auto layer: layer_props) {
    uint32_t layer_ext_count;
    vkEnumerateInstanceExtensionProperties(layer.layerName, &layer_ext_count,
      nullptr);
    if (layer_ext_count == 0) continue;
    VkExtensionProperties layer_exts[layer_ext_count];
    vkEnumerateInstanceExtensionProperties(layer.layerName, &layer_ext_count,
    layer_exts);

    VkExtensionProperties * layer_exts_end = layer_exts + layer_ext_count;
    debugUtilsExtAvailable = layer_exts != std::find_if(
      layer_exts, layer_exts_end,[](VkExtensionProperties extensionProperties) {
        return strcmp(extensionProperties.extensionName,
        VK_EXT_DEBUG_UTILS_EXTENSION_NAME) == 0;
      });
    if (debugUtilsExtAvailable) {
        // Add the including layer into the layer request list if necessary.
        break;
    }
  }
}

if (!debugUtilsExtAvailable) return; // since this snippet depends on debugUtils

const char * enabled_inst_exts[] = { ..., VK_EXT_DEBUG_UTILS_EXTENSION_NAME };
uint32_t enabled_extension_count =
  sizeof(enabled_inst_exts)/sizeof(enabled_inst_exts[0]);

// Pass the instance extensions into vkCreateInstance.
VkInstanceCreateInfo instance_info = {};
instance_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
instance_info.enabledExtensionCount = enabled_extension_count;
instance_info.ppEnabledExtensionNames = enabled_inst_exts;

// NOTE: Can still return VK_ERROR_EXTENSION_NOT_PRESENT if validation layer
// isn't loaded.
vkCreateInstance(&instance_info, nullptr, &instance);

auto pfnCreateDebugUtilsMessengerEXT =
  (PFN_vkCreateDebugUtilsMessengerEXT)vkGetInstanceProcAddr(
    tutorialInstance, "vkCreateDebugUtilsMessengerEXT");
auto pfnDestroyDebugUtilsMessengerEXT =
  (PFN_vkDestroyDebugUtilsMessengerEXT)vkGetInstanceProcAddr(
    tutorialInstance, "vkDestroyDebugUtilsMessengerEXT");

// Create the debug messenger callback with your the settings you want.
VkDebugUtilsMessengerEXT debugUtilsMessenger;
if (pfnCreateDebugUtilsMessengerEXT) {
  VkDebugUtilsMessengerCreateInfoEXT messengerInfo;
  constexpr VkDebugUtilsMessageSeverityFlagsEXT kSeveritiesToLog =
    VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT |
    VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT;

constexpr VkDebugUtilsMessageTypeFlagsEXT kMessagesToLog =
  VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT |
  VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT |
  VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;

  messengerInfo.sType           = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
  messengerInfo.pNext           = nullptr;
  messengerInfo.flags           = 0;
  messengerInfo.messageSeverity = kSeveritiesToLog;
  messengerInfo.messageType     = kMessagesToLog;

  // The DebugUtilsMessenger callback is explained in the following section.
  messengerInfo.pfnUserCallback = &DebugUtilsMessenger;
  messengerInfo.pUserData       = nullptr; // Custom user data passed to callback

  pfnCreateDebugUtilsMessengerEXT(instance, &messengerInfo, nullptr,
    &debugUtilsMessenger);
}

// Later, when shutting down Vulkan, call the following:
if (pfnDestroyDebugUtilsMessengerEXT) {
    pfnDestroyDebugUtilsMessengerEXT(instance, debugUtilsMessenger, nullptr);
}

بعد أن يسجِّل تطبيقك معاودة الاتصال ويفعِّلها، يوجِّه النظام رسائل تصحيح الأخطاء إليها.

#include <android/log.h>

VKAPI_ATTR VkBool32 VKAPI_CALL DebugUtilsMessenger(
                        VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,
                        VkDebugUtilsMessageTypeFlagsEXT messageTypes,
                        const VkDebugUtilsMessengerCallbackDataEXT *callbackData,
                        void *userData)
{
  const char validation[]  = "Validation";
  const char performance[] = "Performance";
  const char error[]       = "ERROR";
  const char warning[]     = "WARNING";
  const char unknownType[] = "UNKNOWN_TYPE";
  const char unknownSeverity[] = "UNKNOWN_SEVERITY";
  const char* typeString      = unknownType;
  const char* severityString  = unknownSeverity;
  const char* messageIdName   = callbackData->pMessageIdName;
  int32_t messageIdNumber     = callbackData->messageIdNumber;
  const char* message         = callbackData->pMessage;
  android_LogPriority priority = ANDROID_LOG_UNKNOWN;

  if (messageSeverity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) {
    severityString = error;
    priority = ANDROID_LOG_ERROR;
  }
  else if (messageSeverity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) {
    severityString = warning;
    priority = ANDROID_LOG_WARN;
  }
  if (messageTypes & VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT) {
     typeString = validation;
  }
  else if (messageTypes & VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT) {
     typeString = performance;
  }

  __android_log_print(priority,
                     "AppName",
                     "%s %s: [%s] Code %i : %s",
                     typeString,
                     severityString,
                     messageIdName,
                     messageIdNumber,
                     message);

  // Returning false tells the layer not to stop when the event occurs, so
  // they see the same behavior with and without validation layers enabled.
  return VK_FALSE;
}

استخدام مراحل التحقّق الخارجية

لست بحاجة إلى تضمين مراحل التحقّق في حزمة APK، إذ يمكن للأجهزة التي تعمل بنظام التشغيل Android 9 (مستوى واجهة برمجة التطبيقات 28) والإصدارات الأحدث استخدام مراحل التحقّق الخارجية لملفك الثنائي وإيقافها وتفعيلها بشكل ديناميكي. اتّبِع الخطوات الواردة في هذا القسم لنقل مراحل التحقّق إلى جهاز الاختبار:

السماح لتطبيقك باستخدام مراحل التحقّق الخارجية

يختلف نموذج الأمان والسياسات في Android اختلافًا كبيرًا عن المنصّات الأخرى. لتحميل مراحل التحقّق الخارجية، يجب أن يتحقّق أحد الشروط التالية:

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

  • يجب تشغيل التطبيق المستهدَف على إصدار userdebug من نظام التشغيل يمنح إذن الوصول إلى الجذر.

  • التطبيقات التي تستهدف Android 11 (المستوى 30 من واجهة برمجة التطبيقات) أو الإصدارات الأحدث فقط: يجب أن يتضمّن ملف البيان المستهدَف في Android عنصر meta-data التالي:

    <meta-data android:name="com.android.graphics.injectLayers.enable"
      android:value="true"/>

تحميل مرحلة تحقّق خارجية

تتيح الأجهزة التي تعمل بنظام التشغيل Android 9 (المستوى 28 من واجهة برمجة التطبيقات) والإصدارات الأحدث لـ Vulkan تحميل مرحلة التحقّق من مساحة التخزين المحلية لتطبيقك. اعتبارًا من Android 10 (مستوى واجهة برمجة التطبيقات 29)، يمكن لـ Vulkan أيضًا تحميل مرحلة التحقّق من حزمة APK منفصلة. يمكنك اختيار أي طريقة تريدها طالما أنّ إصدار Android يتيح استخدامها.

تحميل ملف ثنائي لمرحلة التحقّق من مساحة التخزين المحلية لجهازك

بما أنّ Vulkan يبحث عن البرنامج الثنائي في دليل تخزين البيانات المؤقتة على جهازك، عليك أولاً إرسال البرنامج الثنائي إلى هذا الدليل باستخدام Android Debug Bridge (adb)، على النحو التالي:

  1. استخدِم الأمر adb push لتحميل الملف الثنائي للمرحلة إلى مساحة تخزين بيانات تطبيقك على الجهاز:

    $ adb push libVkLayer_khronos_validation.so /data/local/tmp
    
  2. استخدِم الأمرَين adb shell وrun-as لتحميل المرحلة من خلال عملية تطبيقك. أي أنّ البرنامج الثنائي يحصل على إذن الوصول إلى الجهاز نفسه الذي يحصل عليه التطبيق بدون الحاجة إلى إذن الوصول إلى الجذر.

    $ adb shell run-as com.example.myapp cp
      /data/local/tmp/libVkLayer_khronos_validation.so .
    $ adb shell run-as com.example.myapp ls libVkLayer_khronos_validation.so
    
  3. فعِّل الطبقة.

تحميل ملف ثنائي لمرحلة التحقّق من حزمة APK أخرى

يمكنك استخدام adb لـ تثبيت حزمة APK التي تحتوي على الطبقة ثم تفعيل الطبقة.

adb install --abi abi path_to_apk

تفعيل المراحل خارج التطبيق

يمكنك تفعيل مراحل Vulkan إما لكل تطبيق أو على مستوى العالم. تظل الإعدادات لكل تطبيق سارية بعد إعادة التشغيل، بينما يتم محو الخصائص العامة عند إعادة التشغيل.

تفعيل المراحل لكل تطبيق

توضّح الخطوات التالية كيفية تفعيل المراحل لكل تطبيق:

  1. استخدِم adb shell settings لتفعيل المراحل:

    $ adb shell settings put global enable_gpu_debug_layers 1
    
  2. حدِّد التطبيق المستهدَف لتفعيل المراحل عليه:

    $ adb shell settings put global gpu_debug_app <package_name>
    
  3. حدِّد قائمة بالمراحل المطلوب تفعيلها (من الأعلى إلى الأسفل)، مع الفصل بين كل مرحلة وأخرى بنقطتَين رأسيتَين:

    $ adb shell settings put global gpu_debug_layers <layer1:layer2:layerN>
    

    بما أنّ لدينا مرحلة تحقّق واحدة من Khronos، من المرجّح أن يبدو الأمر على النحو التالي:

    $ adb shell settings put global gpu_debug_layers VK_LAYER_KHRONOS_validation
    
  4. حدِّد حزمة واحدة أو أكثر للبحث عن المراحل بداخلها:

    $ adb shell settings put global
      gpu_debug_layer_app <package1:package2:packageN>
    

يمكنك التحقّق مما إذا كانت الإعدادات مفعّلة باستخدام الأوامر التالية:

$ adb shell settings list global | grep gpu
enable_gpu_debug_layers=1
gpu_debug_app=com.example.myapp
gpu_debug_layers=VK_LAYER_KHRONOS_validation

بما أنّ الإعدادات التي تطبّقها تظل سارية بعد إعادة تشغيل الجهاز، قد تحتاج إلى محو الإعدادات بعد تحميل المراحل:

$ adb shell settings delete global enable_gpu_debug_layers
$ adb shell settings delete global gpu_debug_app
$ adb shell settings delete global gpu_debug_layers
$ adb shell settings delete global gpu_debug_layer_app

تفعيل المراحل على مستوى العالم

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

$ adb shell setprop debug.vulkan.layers <layer1:layer2:layerN>