רוב ממשקי ה-API הגרפיים המפורשים לא מבצעים בדיקת שגיאות, כי הפעולה הזו עלולה להוביל לפגיעה בביצועים. ל-Vulkan יש שכבות אימות שמאפשרות לבצע בדיקת שגיאות במהלך הפיתוח, וכך להימנע מהפגיעה בביצועים בגרסה המהדורה של האפליקציה. שכבות האימות מסתמכות על מנגנון שכבות למטרות כלליות שמנתב את נקודות הכניסה ל-API.
שכבת אימות יחידה של Khronos
בעבר, Vulkan סיפק כמה שכבות אימות שצריך להפעיל בסדר מסוים. החל מגרסה 1.1.106.0 Vulkan SDK, האפליקציה צריכה להפעיל רק שכבת אימות אחת, VK_LAYER_KHRONOS_validation
, כדי לקבל את כל התכונות משכבות האימות הקודמות.
שימוש בשכבות אימות שארוזות ב-APK
שכבות אימות האריזה ב-APK מבטיחות תאימות אופטימלית. שכבות האימות זמינות כקבצים בינאריים מוכנים מראש או שניתן ליצור אותן מקוד מקור.
שימוש בקובצי בינארי שנוצרו מראש
מורידים את קובצי ה-binaries העדכניים ביותר של שכבת האימות של 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 ולפעול לפי הוראות ה-build שמפורטות שם.
מוודאים ששכבת האימות ארוזת בצורה נכונה
לא משנה אם אתם מבצעים 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 מאפשר לאפליקציה להפעיל שכבות במהלך יצירת המכונה. בנקודות הכניסה ששכבת ניתוב מנתבת, הפרמטר הראשון צריך להיות אחד מהאובייקטים הבאים:
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)
הפעלת הקריאה החוזרת (callback) של ניפוי באגים
התוסף Debug Utils VK_EXT_debug_utils
מאפשר לאפליקציה ליצור שליח לניפוי באגים שמעביר הודעות של שכבת האימות ל-callback שסופק על ידי האפליקציה. יכול להיות שהמכשיר שלכם לא יישם את התוסף הזה, אבל הוא מיושם בשכבות האימות העדכניות ביותר. יש גם תוסף שהוצא משימוש שנקרא 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 ואילך (API ברמה 28 ואילך) אפשר להשתמש בשכבות אימות חיצוניות לקובץ הבינארי ולהפעיל אותן או להשבית אותן באופן דינמי. בצעו את השלבים המפורטים בקטע הזה כדי לדחוף שכבות אימות למכשיר הבדיקה:
איך מאפשרים לאפליקציה להשתמש בשכבות אימות חיצוניות
המודל והמדיניות של אבטחת Android שונים באופן משמעותי מפלטפורמות אחרות. כדי לטעון שכבות אימות חיצוניות, אחד מהתנאים הבאים צריך להתקיים:
אפשר לאתר באפליקציית היעד באגים. האפשרות הזו מאפשרת לקבל יותר מידע על ניפוי הבאגים, אבל היא עלולה להשפיע לרעה על ביצועי האפליקציה.
אפליקציית היעד פועלת בגרסה userdebug של מערכת ההפעלה שמעניקה הרשאת root.
אפליקציות שמטרגטות ל-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 כדי להפעיל את השכבות:
$ 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
הפעלת שכבות ברמת הארגון
אפשר להפעיל שכבה אחת או יותר באופן גלובלי עד להפעלה מחדש הבאה. הפעולה הזו מנסה לטעון את השכבות לכל האפליקציות, כולל קובצי הפעלה (executables) מקומיים.
$ adb shell setprop debug.vulkan.layers <layer1:layer2:layerN>