طبقات التحقق من 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 للتطبيق بتفعيل الطبقات أثناء إنشاء المثيل. يجب أن تحتوي نقاط العبور التي تعترضها الطبقة على أحد الكائنات التالية كمَعلمة أولاً:

  • 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 بشكل كبير عن منصّات التشغيل الأخرى. لتحميل طبقات التحقّق الخارجية، يجب أن يكون أحد الشروط التالية صحيحًا:

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

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

  • التطبيقات التي تستهدف الإصدار 11 من نظام التشغيل Android (المستوى 30 لواجهة برمجة التطبيقات) أو الإصدارات الأحدث فقط: يتضمّن ملف Android manifest المستهدَف العنصر التالي 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 يبحث عن الملف الثنائي في ملف directory تخزين البيانات المؤقتة على جهازك، عليك أولاً دفع الملف الثنائي إلى هذا الدليل باستخدام 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>