Couches de validation Vulkan sur Android

La plupart des API de graphiques explicites n'effectuent pas de vérification des erreurs, car cela peut compromettre les performances. Vulkan dispose de couches de validation qui permettent de vérifier les erreurs lors du développement sans nuire aux performances dans le build de votre application. Les couches de validation reposent sur un mécanisme de superposition à usage général qui intercepte les points d'entrée des API.

Couche de validation Khronos unique

Auparavant, Vulkan proposait plusieurs couches de validation qui devaient être activées dans un ordre spécifique. À partir de la version 1.1.106.0 du SDK Vulkan, votre application ne doit activer qu'une seule couche de validation ,VK_LAYER_KHRONOS_validation, pour obtenir toutes les fonctionnalités des couches de validation précédentes.

Utiliser les couches de validation empaquetées dans le fichier APK

Empaqueter les couches de validation dans le fichier APK garantit une compatibilité optimale. Les couches de validation sont disponibles en tant que binaires prédéfinis ou peuvent être compilées à partir du code source.

Utiliser des binaires prédéfinis

Téléchargez les derniers binaires de la couche de validation Android Vulkan sur la page de versions de GitHub.

Le moyen le plus simple d'ajouter des couches au fichier APK consiste à extraire les binaires de la couche prédéfinie dans le répertoire src/main/jniLibs/ du module, sans toucher aux répertoires ABI (tels que arm64-v8a ou x86-64), comme indiqué ici :

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

Créer la couche de validation à partir du code source

Pour déboguer le code source de la couche de validation, extrayez la dernière source à partir du dépôt GitHub du groupe Khronos, puis suivez les instructions de compilation.

Vérifier que la couche de validation est empaquetée correctement

Que vous utilisiez des couches Khronos prédéfinies ou des couches créées à partir de la source, le processus de compilation génère une structure de fichier finale dans le package APK, comme suit :

lib/
  arm64-v8a/
    libVkLayer_khronos_validation.so
  armeabi-v7a/
    libVkLayer_khronos_validation.so
  x86/
    libVkLayer_khronos_validation.so
  x86-64/
    libVkLayer_khronos_validation.so

La commande suivante montre comment vérifier que le fichier APK contient la couche de validation comme prévu :

$ 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

Activer une couche de validation lors de la création de l'instance

L'API Vulkan permet à une application d'activer les couches lors de la création d'une instance. Les points d'entrée interceptés par une couche doivent comporter l'un des objets suivants comme premier paramètre :

  • VkInstance
  • VkPhysicalDevice
  • VkDevice
  • VkCommandBuffer
  • VkQueue

Appelez vkEnumerateInstanceLayerProperties() pour répertorier les couches disponibles et leurs propriétés. Vulkan active les couches lors de l'exécution de vkCreateInstance().

L'extrait de code suivant montre comment une application peut utiliser l'API Vulkan pour interroger et activer des couches par programmation :

// 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,

Sortie logcat par défaut

La couche de validation émet des messages d'avertissement et d'erreur dans logcat, accompagnés d'une balise VALIDATION. Voici ce à quoi ressemble un message de couche de validation (notez que nous avons ici ajouté des sauts de ligne pour faciliter le défilement) :

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)

Activer le rappel de débogage

L'extension Debug Utils VK_EXT_debug_utils permet à votre application de créer un utilitaire de messagerie de débogage qui transmet les messages de la couche de validation à un rappel fourni par l'application. Votre appareil peut ne pas implémenter cette extension, mais elle est intégrée dans les couches de validation les plus récentes. Il existe également une extension obsolète appelée VK_EXT_debug_report, qui offre des fonctionnalités similaires si VK_EXT_debug_utils n'est pas disponible.

Avant d'utiliser l'extension Debug Utils, assurez-vous qu'elle est compatible avec votre appareil ou une couche de validation chargée. L'exemple suivant montre comment vérifier si cette extension est compatible avec l'appareil ou la couche de validation et, si tel est le cas, comment enregistrer un rappel.

// 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);
}

Une fois l'application enregistrée et activée, le système y achemine les messages de débogage.

#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;
}

Utiliser des couches de validation externes

Vous n'avez pas besoin d'empaqueter les couches de validation dans le fichier APK. Les appareils équipés d'Android 9 (niveau d'API 28) ou version ultérieure peuvent utiliser des couches de validation externes au binaire, et les désactiver, puis les activer de manière dynamique. Pour transmettre des couches de validation à votre appareil de test, suivez les étapes ci-dessous :

Autoriser votre application à utiliser des couches de validation externes

Le modèle de sécurité et les règles d'Android sont très différents de ceux des autres plates-formes. Pour charger des couches de validation externes, vous devez remplir l'une des conditions suivantes :

  • L'application cible est débogable. Cette option vous fournit davantage d'informations de débogage, mais peut affecter les performances de votre application.

  • L'application cible est exécutée sur un build userdebug du système d'exploitation qui accorde un accès racine.

  • Applications ciblant uniquement Android 11 (niveau d'API 30) ou version ultérieure : le fichier manifeste Android cible doit inclure l'élément meta-data suivant :

    <meta-data android:name="com.android.graphics.injectLayers.enable"
      android:value="true"/>

Charger une couche de validation externe

Les appareils équipés d'Android 9 (niveau d'API 28) ou version ultérieure permettent à Vulkan de charger la couche de validation à partir du stockage local de votre application. À partir d'Android 10 (niveau d'API 29), Vulkan peut également charger la couche de validation à partir d'un fichier APK distinct. Vous pouvez sélectionner la méthode de votre choix, dans la mesure où votre version d'Android le permet.

Charger un binaire de couche de validation à partir du stockage local de votre appareil

Comme Vulkan recherche le binaire dans le répertoire de stockage temporaire des données de votre appareil, vous devez d'abord transférer le binaire vers ce répertoire à l'aide d'Android Debug Bridge (adb), comme suit :

  1. Utilisez la commande adb push pour charger le binaire de la couche dans le stockage de données de votre application sur l'appareil :

    $ adb push libVkLayer_khronos_validation.so /data/local/tmp
    
  2. Utilisez les commandes adb shell et run-as pour charger la couche tout au long du processus de votre application. Autrement dit, le binaire disposera du même accès d'appareil que l'application sans avoir besoin d'un accès racine.

    $ 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. Activez la couche.

Charger un binaire de couche de validation à partir d'un autre fichier APK

Vous pouvez utiliser adb pour installer un fichier APK contenant la couche, puis activer celle-ci.

adb install --abi abi path_to_apk

Activer les couches en dehors de l'application

Vous pouvez activer les couches Vulkan pour une application ou de manière globale. Au redémarrage, les paramètres par application persistent, tandis que les propriétés globales sont effacées.

Activer les couches en fonction de chaque application

Pour activer des couches par application, procédez comme suit :

  1. Utilisez les paramètres de l'interface système adb pour activer les couches :

    $ adb shell settings put global enable_gpu_debug_layers 1
    
  2. Spécifiez l'application cible sur laquelle les activer :

    $ adb shell settings put global gpu_debug_app <package_name>
    
  3. Spécifiez la liste des couches à activer (de haut en bas), en les séparant par le signe deux-points :

    $ adb shell settings put global gpu_debug_layers <layer1:layer2:layerN>
    

    Comme nous n'avons qu'une seule couche de validation Khronos, la commande ressemblera probablement à ce qui suit :

    $ adb shell settings put global gpu_debug_layers VK_LAYER_KHRONOS_validation
    
  4. Spécifiez un ou plusieurs packages dans lesquels rechercher les couches :

    $ adb shell settings put global
      gpu_debug_layer_app <package1:package2:packageN>
    

Vous pouvez vérifier si les paramètres sont activés à l'aide des commandes suivantes :

$ 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

Étant donné que les paramètres que vous appliquez sont conservés au redémarrage de l'appareil, vous pouvez les effacer une fois les couches chargées :

$ 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

Activer les couches de manière globale

Vous pouvez activer une ou plusieurs couches de manière globale jusqu'au prochain redémarrage. Cette opération tente de charger les couches pour toutes les applications, y compris les exécutables natifs.

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