שכבות אימות של Vulkan ב-Android

רוב ממשקי ה-API של הגרפיקה הבוטה לא מבצעים בדיקת שגיאות, מפני שהפעולה הזו עלולה עלולות להוביל לפגיעה בביצועים. ב-Vulkan יש שכבות אימות שמספקות לבדיקת שגיאות במהלך הפיתוח, כדי להימנע מפגיעה בביצועים של גרסת ה-build של האפליקציה שלכם. שכבות אימות מסתמכות על מטרה כללית במנגנון שכבות שמיירט נקודות כניסה ל-API.

שכבת אימות יחידה של כרונו

בעבר, Vulkan סיפק כמה שכבות אימות שצריך להפעיל בסדר מסוים. החל מגרסה 1.1.106.0 Vulkan SDK, האפליקציה שלך צריך להפעיל רק אימות אחד בשכבת זרימת הנתונים, VK_LAYER_KHRONOS_validation, כדי לקבל את כל התכונות מהגרסה הקודמת ארבע שכבות אימות.

שימוש בשכבות אימות שנארזות ב-APK

אריזת שכבות אימות בתוך ה-APK מבטיחה תאימות אופטימלית. שכבות האימות זמינות כקבצים בינאריים מוכנים מראש או שניתן ליצור אותן מ- קוד המקור.

שימוש בקבצים בינאריים מוכנים מראש

מורידים את הקבצים הבינאריים העדכניים של השכבה של Android Vulkan Validation מ-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

בניית שכבת האימות מקוד המקור

כדי לנפות באגים בקוד המקור של שכבת האימות, מאחזרים את המקור העדכני ביותר קבוצת Khronos GitHub מאגר ולעקוב אחרי את ההוראות ל-build.

מוודאים ששכבת האימות ארוזה כראוי

ולא משנה אם אתם בונים באמצעות השכבות המוכנות מראש של חררון או השכבות שנוצרו מהמקור, תהליך ה-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 מאפשר לאפליקציה ליצור תוכנת ניפוי באגים שמעבירה הודעות של שכבת אימות לאפליקציה שסופקה קריאה חוזרת. יכול להיות שהמכשיר לא מטמיעים את התוסף הזה, אבל הוא מוטמע ב את שכבות האימות האחרונות. יש גם תוסף שהוצא משימוש בשם 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 שונים באופן משמעותי מאלה של אחרים פלטפורמות שונות. כדי לטעון שכבות אימות חיצוניות, צריך אחד מהתנאים הבאים חייב להיות True:

  • אפליקציית היעד ניתנת לניפוי באגים. אם בוחרים באפשרות הזו, יש יותר ניפוי באגים אבל עשוי להשפיע לרעה על ביצועי האפליקציה שלכם.

  • אפליקציית היעד רצה ב-build של 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), באופן הבא:

  1. משתמשים בפקודה adb push כדי לטעון את של שכבת הבינארית באחסון הנתונים של האפליקציה במכשיר:

    $ adb push libVkLayer_khronos_validation.so /data/local/tmp
    
  2. שימוש ב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
    
  3. מפעילים את השכבה.

יש לטעון קובץ בינארי של שכבת אימות מ-APK אחר

ניתן להשתמש ב-adb כדי להתקין APK מכיל את השכבה ולאחר מכן הפעלת השכבה.

adb install --abi abi path_to_apk

הפעלת שכבות מחוץ לאפליקציה

אפשר להפעיל שכבות Vulkan לכל אפליקציה או באופן גלובלי. הגדרות לפי אפליקציה להישמר בהפעלות מחדש, בזמן שהנכסים הגלובליים נמחקים מהפעילות להפעיל מחדש.

הפעלת שכבות לכל אפליקציה בנפרד

השלבים הבאים מתארים כיצד להפעיל שכבות בכל אפליקציה בנפרד:

  1. משתמשים בהגדרות המעטפת של adb כדי להפעיל את השכבות:

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

    מכיוון שיש לנו שכבת אימות אחת של חררון, סביר להניח שהפקודה נראים כך:

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