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

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

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

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

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

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

استخدام البرامج الثنائية المُنشأة مسبقًا

نزِّل أحدث ملفات ثنائية لمرحلة التحقّق من Vulkan على Android من صفحة الإصدارات على 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 واتّباع تعليمات الإنشاء الواردة فيه.

التحقّق من حزم مرحلة التحقّق بشكل صحيح

بغض النظر عمّا إذا كنت تستخدم طبقات 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، عليك التأكّد من أنّ جهازك أو مرحلة التحقّق المحمَّلة متوافقان معها. يوضّح المثال التالي كيفية التحقّق من توفّر إضافة أدوات تصحيح الأخطاء وتسجيل دالة ردّ اتصال إذا كانت الإضافة متوافقة مع الجهاز أو مرحلة التحقّق.

// 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 من نظام التشغيل الذي يمنح إذن الوصول إلى الجذر.

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

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

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

تسمح الأجهزة التي تعمل بنظام التشغيل Android 9 (مستوى واجهة برمجة التطبيقات 28) والإصدارات الأحدث لواجهة Vulkan بتحميل مرحلة التحقّق من مساحة التخزين المحلية لتطبيقك. بدءًا من الإصدار 10 من نظام التشغيل Android (مستوى واجهة برمجة التطبيقات 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 لتفعيل الطبقات:

    $ 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>