Livelli di convalida Vulkan su Android

La maggior parte delle API grafiche esplicite non esegue il controllo degli errori perché ciò può comportare una riduzione delle prestazioni. Vulkan dispone di livelli di convalida che forniscono il controllo degli errori durante lo sviluppo, evitando penalità nelle prestazioni nella build di release dell'app. I livelli di convalida si basano su un meccanismo di livelli per uso generico che intercetta i punti di ingresso dell'API.

Singolo livello di convalida Khronos

In precedenza, Vulkan forniva più livelli di convalida che dovevano essere attivati in un ordine specifico. A partire dalla release 1.1.106.0 dell'SDK Vulkan, la tua app deve solo attivare un singolo livello di convalida, VK_LAYER_KHRONOS_validation, per ottenere tutte le funzionalità dei livelli di convalida precedenti.

Utilizzo di livelli di convalida pacchettizzati nell'APK

La pacchettizzazione dei livelli di convalida all'interno dell'APK garantisce una compatibilità ottimale. I livelli di convalida sono disponibili come programmi binari predefiniti o possono essere creati dal codice sorgente.

Utilizza programmi binari predefiniti

Scarica gli ultimi programmi binari dei livelli di Android Vulkan Validation dalla pagina di rilascio di GitHub.

Il modo più semplice per aggiungere i livelli al tuo APK è estrarre i file binari dei livelli predefiniti nella directory src/main/jniLibs/ del modulo, con le directory ABI (come arm64-v8a o x86-64) intatte, come segue:

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

Crea il livello di convalida dal codice sorgente

Per eseguire il debug nel codice sorgente del livello di convalida, estrai l'origine più recente dal repository GitHub di Khronos Group e segui le istruzioni di creazione presenti.

Verificare che il livello di convalida sia pacchettizzato correttamente

Indipendentemente dal fatto che tu abbia creato con i livelli predefiniti Khronos o con i livelli creati dal codice sorgente, il processo di compilazione produce una struttura di file finale nell'APK come la seguente:

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

Il seguente comando mostra come verificare che l'APK contenga il livello di convalida come previsto:

$ 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

Abilita un livello di convalida durante la creazione dell'istanza

L'API Vulkan consente a un'app di abilitare i livelli durante la creazione dell'istanza. I punti di ingresso intercettati da un livello devono avere uno dei seguenti oggetti come primo parametro:

  • VkInstance
  • VkPhysicalDevice
  • VkDevice
  • VkCommandBuffer
  • VkQueue

Chiama vkEnumerateInstanceLayerProperties() per elencare i livelli disponibili e le relative proprietà. Vulkan abilita i livelli quando viene eseguito vkCreateInstance().

Il seguente snippet di codice mostra come un'app può utilizzare l'API Vulkan per eseguire query e attivare i livelli a livello di programmazione:

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

Output logcat predefinito

Il livello di convalida emette messaggi di avviso ed errore in logcat etichettati con un tag VALIDATION. Un messaggio del livello di convalida ha il seguente aspetto (con interruzioni di riga aggiunte per facilitare lo scorrimento):

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)

Attiva il callback di debug

L'estensione Debug Utils VK_EXT_debug_utils consente all'applicazione di creare un programma di messaggistica di debug che trasmette i messaggi del livello di convalida a un callback fornito dall'applicazione. Il dispositivo potrebbe non implementare questa estensione, ma è implementata nei livelli di convalida più recenti. C'è anche un'estensione deprecata denominata VK_EXT_debug_report, che offre funzionalità simili se VK_EXT_debug_utils non è disponibile.

Prima di utilizzare l'estensione Debug Utils, assicurati che sia supportata dal tuo dispositivo o da un livello di convalida caricato. L'esempio seguente mostra come verificare se l'estensione utils di debug è supportata e registrare un callback se l'estensione è supportata dal livello di dispositivo o di convalida.

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

Dopo che l'app è stata registrata e attiva il callback, il sistema vi indirizza i messaggi di debug.

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

Utilizza livelli di convalida esterni

Non è necessario includere livelli di convalida dei pacchetti nell'APK; i dispositivi con Android 9 (livello API 28) e versioni successive possono utilizzare livelli di convalida esterni al file binario e disattivarli e attivarli in modo dinamico. Segui i passaggi in questa sezione per eseguire il push dei livelli di convalida al dispositivo di test:

Abilita l'app per utilizzare livelli di convalida esterni

I criteri e il modello di sicurezza di Android differiscono in modo significativo da quelli di altre piattaforme. Per caricare i livelli di convalida esterni, deve essere vera una delle seguenti condizioni:

  • È possibile eseguire il debug dell'app di destinazione. Questa opzione genera più informazioni di debug, ma potrebbe influire negativamente sulle prestazioni della tua app.

  • L'app di destinazione viene eseguita su una build userdebug del sistema operativo che concede l'accesso root.

  • App destinate solo ad Android 11 (livello API 30) o versioni successive: il file manifest Android di destinazione include il seguente elemento meta-data:

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

Carica un livello di convalida esterno

I dispositivi con Android 9 (livello API 28) e versioni successive consentono a Vulkan di caricare il livello di convalida dallo spazio di archiviazione locale dell'app. A partire da Android 10 (livello API 29), Vulkan può anche caricare il livello di convalida da un APK separato. Puoi scegliere il metodo che preferisci, a condizione che la tua versione di Android lo supporti.

Carica un programma binario del livello di convalida dallo spazio di archiviazione locale del dispositivo

Poiché Vulkan cerca il programma binario nella directory di archiviazione temporanea dei dati del dispositivo, devi prima eseguirne il push in quella directory utilizzando il Bridge di debug Android (adb), come indicato di seguito:

  1. Utilizza il comando adb push per caricare il programma binario del livello nell'archiviazione dei dati dell'app sul dispositivo:

    $ adb push libVkLayer_khronos_validation.so /data/local/tmp
    
  2. Utilizza i comandi adb shell e run-as per caricare il livello tramite il processo dell'app. In altre parole, il programma binario ha lo stesso accesso al dispositivo dell'app, senza richiedere l'accesso 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. Attiva il livello.

Carica un programma binario del livello di convalida da un altro APK

Puoi utilizzare adb per installare un APK che contiene il livello e poi abilitarlo.

adb install --abi abi path_to_apk

Abilita i livelli all'esterno dell'applicazione

Puoi attivare i livelli Vulkan a livello di app o a livello globale. Le impostazioni per app permangono tra i riavvii, mentre le proprietà globali vengono cancellate al riavvio.

Abilita i livelli in base alle singole app

I passaggi seguenti spiegano come attivare i livelli per le singole app:

  1. Utilizza le impostazioni della shell adb per abilitare i livelli:

    $ adb shell settings put global enable_gpu_debug_layers 1
    
  2. Specifica l'applicazione target per abilitare i livelli:

    $ adb shell settings put global gpu_debug_app <package_name>
    
  3. Specifica l'elenco di livelli da abilitare (dall'alto verso il basso), separando ogni livello con i due punti:

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

    Poiché abbiamo un singolo livello di convalida Khronos, il comando probabilmente sarà simile a questo:

    $ adb shell settings put global gpu_debug_layers VK_LAYER_KHRONOS_validation
    
  4. Specifica uno o più pacchetti in cui cercare i livelli all'interno di:

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

Puoi controllare se le impostazioni sono attive utilizzando i seguenti comandi:

$ 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

Poiché le impostazioni che applichi vengono mantenute su tutti i riavvii dei dispositivi, ti consigliamo di cancellarle dopo il caricamento dei livelli:

$ 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

Abilita i livelli a livello globale

Puoi abilitare uno o più livelli a livello globale fino al riavvio successivo. Questo tenta di caricare i livelli per tutte le applicazioni, compresi gli eseguibili nativi.

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