רוב ממשקי ה-API הגרפיים המפורשים לא מבצעים בדיקת שגיאות, כי זה עלול לפגוע בביצועים. ל-Vulkan יש שכבות אימות שמספקות בדיקת שגיאות במהלך הפיתוח, וכך נמנעת פגיעה בביצועים בגרסת ה-build להפצה של האפליקציה. שכבות האימות מסתמכות על מנגנון שכבות למטרות כלליות שמיירט נקודות כניסה של API.
שכבת אימות אחת של Khronos
בעבר, Vulkan סיפק כמה שכבות אימות שהיה צריך להפעיל בסדר מסוים. החל מגרסה 1.1.106.0 של Vulkan SDK, צריך להפעיל באפליקציה רק שכבת אימות אחת, VK_LAYER_KHRONOS_validation, כדי לקבל את כל התכונות משכבות האימות הקודמות.
שימוש בשכבות אימות שנארזו ב-APK
כדי להבטיח תאימות אופטימלית, כדאי לארוז את שכבות האימות בתוך ה-APK. שכבות האימות זמינות כקובצי בינאריים שנבנו מראש או שניתן לבנות אותן מקוד מקור.
שימוש בקובצי בינארי מוכנים מראש
מורידים את הקבצים הבינאריים של שכבת האימות של Vulkan ל-Android מדף הגרסאות ב-GitHub.
הדרך הכי קלה להוסיף את השכבות ל-APK היא לחלץ את קובצי ה-binary של השכבות שנוצרו מראש לספרייה 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 ולפעול לפי הוראות ה-build שמופיעות שם.
מוודאים ששכבת האימות ארוזה בצורה נכונה
לא משנה אם אתם משתמשים בשכבות שנוצרו מראש של Khronos או בשכבות שנוצרו מקוד המקור, תהליך ה-build יוצר מבנה קובץ סופי ב-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 מאפשר לאפליקציה להפעיל שכבות במהלך יצירת המכונה. נקודות כניסה ששכבה מיירטת חייבות לכלול את אחד מהאובייקטים הבאים בתור הפרמטר הראשון:
VkInstanceVkPhysicalDeviceVkDeviceVkCommandBufferVkQueue
מפעילים את 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)
הפעלת קריאה חוזרת (callback) לניפוי באגים
התוסף Debug Utils VK_EXT_debug_utils מאפשר לאפליקציה ליצור שליח לניפוי באגים שמעביר הודעות של שכבת האימות לקריאה חוזרת (callback) שסופקה על ידי האפליקציה. יכול להיות שהתוסף הזה לא מוטמע במכשיר שלכם, אבל הוא מוטמע בשכבות האימות העדכניות ביותר. יש גם תוסף שהוצא משימוש בשם VK_EXT_debug_report, שמספק יכולות דומות אם VK_EXT_debug_utils לא זמין.
לפני שמשתמשים בתוסף Debug Utils, צריך לוודא שהמכשיר או שכבת האימות שנטענה תומכים בו. בדוגמה הבאה אפשר לראות איך בודקים אם יש תמיכה בתוסף debug utils ואיך רושמים קריאה חוזרת (callback) אם יש תמיכה בתוסף במכשיר או בשכבת האימות.
// 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); }
אחרי שהאפליקציה רושמת ומפעילה את פונקציית ה-callback, המערכת מנתבת אליה הודעות לניפוי באגים.
#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 (רמת API 28) ומעלה יכולים להשתמש בשכבות אימות חיצוניות לקובץ הבינארי, ולהפעיל או להשבית אותן באופן דינמי. כדי להעביר שכבות אימות למכשיר הבדיקה, פועלים לפי השלבים שמפורטים בקטע הזה:
איך מאפשרים לאפליקציה להשתמש בשכבות אימות חיצוניות
מודל האבטחה והמדיניות של Android שונים באופן משמעותי מפלטפורמות אחרות. כדי לטעון שכבות אימות חיצוניות, אחד מהתנאים הבאים צריך להתקיים:
אפליקציית היעד ניתנת לניפוי באגים. האפשרות הזו מספקת יותר מידע על תוצאות ניפוי הבאגים, אבל היא עלולה לפגוע בביצועים של האפליקציה.
אפליקציית היעד פועלת בגרסת build של מערכת ההפעלה מסוג userdebug שמעניקה גישת בסיס.
אפליקציות שמטרגטות ל-Android 11 (רמת API 30) ומעלה בלבד: קובץ המניפסט של Android שאתם מטרגטים כולל את הרכיב
meta-dataהבא:<meta-data android:name="com.android.graphics.injectLayers.enable" android:value="true"/>
טעינת שכבת אימות חיצונית
במכשירים עם Android 9 (רמת API 28) ומעלה, Vulkan יכולה לטעון את שכבת האימות מהאחסון המקומי של האפליקציה. החל מ-Android 10 (רמת API 29), Vulkan יכול גם לטעון את שכבת האימות מ-APK נפרד. אתם יכולים לבחור כל שיטה שתרצו, כל עוד גרסת Android שלכם תומכת בה.
טעינת קובץ בינארי של שכבת אימות מהאחסון המקומי של המכשיר
מכיוון ש-Vulkan מחפש את הקובץ הבינארי בספריית אחסון הנתונים הזמנית של המכשיר, קודם צריך להעביר את הקובץ הבינארי לספרייה הזו באמצעות Android Debug Bridge (adb), באופן הבא:
משתמשים בפקודה
adb pushכדי לטעון את הקובץ הבינארי של השכבה למאגר הנתונים של האפליקציה במכשיר:$ adb push libVkLayer_khronos_validation.so /data/local/tmp
משתמשים בפקודות
adb shellו-run-asכדי לטעון את השכבה דרך תהליך האפליקציה. כלומר, לקובץ הבינארי יש את אותה גישה למכשיר שיש לאפליקציה, בלי צורך בגישת root.$ 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
טעינת קובץ בינארי של שכבת אימות מ-APK אחר
אתם יכולים להשתמש ב-adb כדי להתקין APK שמכיל את השכבה, ואז להפעיל את השכבה.
adb install --abi abi path_to_apk
הפעלת שכבות מחוץ לאפליקציה
אפשר להפעיל שכבות Vulkan לכל אפליקציה בנפרד או באופן גלובלי. הגדרות לכל אפליקציה נשמרות אחרי הפעלה מחדש, אבל מאפיינים גלובליים נמחקים בהפעלה מחדש.
הפעלת שכבות לכל אפליקציה בנפרד
כדי להפעיל שכבות לכל אפליקציה בנפרד:
משתמשים בהגדרות של adb shell כדי להפעיל את השכבות:
$ adb shell settings put global enable_gpu_debug_layers 1מציינים את אפליקציית היעד שבה רוצים להפעיל את השכבות:
$ adb shell settings put global gpu_debug_app <package_name>
מציינים את רשימת השכבות שרוצים להפעיל (מלמעלה למטה), ומפרידים בין השכבות בנקודתיים:
$ adb shell settings put global gpu_debug_layers <layer1:layer2:layerN>
מכיוון שיש לנו שכבת אימות אחת של Khronos, הפקודה תיראה בערך כך:
$ adb shell settings put global gpu_debug_layers VK_LAYER_KHRONOS_validationמציינים חבילה אחת או יותר כדי לחפש שכבות בתוכן:
$ 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>