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

רוב ממשקי ה-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 של Kohronos Group ופועלים לפי ההוראות ל-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, צריך לוודא שהמכשיר או שכבת האימות שמופעלת נתמכים בו. בדוגמה הבאה מוסבר איך לבדוק אם יש תמיכה בתוסף 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);
}

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

#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 של מערכת ההפעלה שמעניקה גישה לרמה הבסיסית (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 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

הפעלת שכבות ברמת הארגון

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

$ adb shell setprop debug.vulkan.layers <layer1:layer2:layerN>