Capas de validación de Vulkan en Android

La mayoría de las APIs de gráficos explícitos no realizan comprobación de errores, porque hacerlo puede generar una penalidad de rendimiento. Vulkan tiene capas de validación que proporcionan comprobación de errores durante el desarrollo, lo que evita la penalidad de rendimiento en la compilación de lanzamiento de la app. Las capas de validación se basan en un mecanismo de capas de uso general que intercepta los puntos de entrada de la API.

Capa de validación única de Khronos

Anteriormente, Vulkan proporcionaba varias capas de validación que debían habilitarse en un orden específico. A partir de la versión 1.1.106.0 del SDK de Vulkan, tu app solo debe habilitar una capa de validación, VK_LAYER_KHRONOS_validation, para obtener todas las funciones de las capas de validación anteriores.

Usa capas de validación empaquetadas en el APK

Empaquetar capas de validación en el APK garantiza una compatibilidad óptima. Las capas de validación están disponibles como objetos binarios compilados previamente o que se pueden compilar a partir del código fuente.

Usa objetos binarios compilados previamente

Descarga los objetos binarios más recientes de la capa de validación de Android Vulkan en la página de lanzamientos de GitHub.

La manera más fácil de agregar las capas al APK es extraer los objetos binarios de capas compilados previamente en el directorio src/main/jniLibs/ del módulo, con directorios de ABI (como arm64-v8a o x86-64) intactos, como se muestra a continuación:

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

Compila la capa de validación a partir del código fuente

Para depurar en el código fuente de la capa de validación, extrae la fuente más reciente del repositorio de GitHub de Khronos Group y sigue las instrucciones de compilación que allí se detallan.

Verifica que la capa de validación esté empaquetada correctamente

Independientemente de que realices compilaciones con capas compiladas previamente de Khronos o compiladas a partir del código fuente, el proceso de compilación producirá una estructura de archivo final en tu APK similar a la siguiente:

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

En el siguiente comando, se muestra cómo verificar que tu APK contenga la capa de validación según lo 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

Habilita una capa de validación durante la creación de instancias

La API de Vulkan permite que una app habilite capas durante la creación de instancias. Los puntos de entrada que intercepta una capa deben tener alguno de los siguientes objetos como primer parámetro:

  • VkInstance
  • VkPhysicalDevice
  • VkDevice
  • VkCommandBuffer
  • VkQueue

Llama a vkEnumerateInstanceLayerProperties() para que detalle las capas disponibles y sus propiedades. Vulkan habilita las capas cuando se ejecuta vkCreateInstance().

En el siguiente fragmento de código, se muestra cómo una app puede usar la API de Vulkan para buscar y habilitar las capas de manera programática:

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

Salida predeterminada de logcat

La capa de validación emite mensajes de advertencia y error en logcat, con una etiqueta VALIDATION. Un mensaje de la capa de validación se ve de la siguiente manera (agregamos saltos de línea para facilitar el desplazamiento):

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)

Habilita la devolución de llamada de depuración

La extensión VK_EXT_debug_utils de Debug Utils permite que tu aplicación cree un sistema de mensajería de depuración que pasa los mensajes de la capa de validación a una devolución de llamada proporcionada por la aplicación. Es posible que esta extensión no se implemente en el dispositivo, pero sí lo hará en las capas de validación más recientes. También hay una extensión obsoleta llamada VK_EXT_debug_report, que proporciona capacidades similares si VK_EXT_debug_utils no está disponible.

Antes de usar la extensión de Debug Utils, debes asegurarte de que sea compatible con tu dispositivo o una capa de validación cargada. En el siguiente ejemplo, se muestra cómo comprobar si la extensión de Debug Utils es compatible con el dispositivo o la capa de validación y, en ese caso, registrar una devolución de llamada.

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

Una vez que la app registre y habilite la devolución de llamada, el sistema enrutará los mensajes de depuración hacia ella.

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

Usa capas de validación externas

No es necesario que empaquetes capas de validación en tu APK; los dispositivos que ejecutan Android 9 (nivel de API 28) y versiones posteriores pueden usar capas de validación externas al objeto binario y activarlas y desactivarlas de forma dinámica. Sigue los pasos de esta sección para enviar capas de validación a tu dispositivo de prueba:

Habilita el uso de capas de validación externas en la app

El modelo de seguridad y las políticas de Android difieren significativamente de los de otras plataformas. Para cargar capas de validación externa, se debe cumplir con una de las siguientes condiciones:

  • La app de destino es depurable. Esta opción da como resultado más información de depuración, pero puede afectar negativamente el rendimiento de la app.

  • La app de destino se ejecuta en una compilación userdebug del sistema operativo que otorga acceso con permisos de administrador.

  • Para las apps orientadas a Android 11 (nivel de API 30) o versiones posteriores: El archivo de manifiesto de Android de destino incluye el siguiente elemento meta-data:

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

Carga una capa de validación externa

Los dispositivos que ejecutan Android 9 (nivel de API 28) y versiones posteriores permiten que Vulkan cargue la capa de validación del almacenamiento local de la app. A partir de Android 10 (nivel de API 29), Vulkan también puede cargar la capa de validación desde un APK independiente. Puedes elegir el método que prefieras, siempre y cuando sea compatible con tu versión de Android.

Carga un objeto binario de capa de validación del almacenamiento local del dispositivo

Como Vulkan busca el objeto binario en el directorio de almacenamiento de datos temporales del dispositivo, primero debes enviarlo a ese directorio mediante Android Debug Bridge (adb) de la siguiente manera:

  1. Usa el comando adb push para cargar el objeto binario de capa en el almacenamiento de datos de tu app, en el dispositivo:

    $ adb push libVkLayer_khronos_validation.so /data/local/tmp
    
  2. Usa los comandos adb shell y run-as para cargar las capas en el proceso de tu app. De esta manera, el objeto binario tiene el mismo acceso al dispositivo que la app, pero no requiere acceso con permisos de administrador.

    $ 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. Habilita la capa.

Carga un objeto binario de capa de validación desde otro APK

Puedes usar adb para instalar un APK que contenga la capa y, luego, habilitarla.

adb install --abi abi path_to_apk

Habilita las capas fuera de la app

Puedes habilitar capas de Vulkan por app, o bien de forma global. La configuración por app persiste después de los reinicios, mientras que las propiedades globales se borran.

Cómo habilitar las capas por app

En los siguientes pasos, se describe cómo habilitar las capas por app:

  1. Usa la configuración de shell de adb para habilitar las capas:

    $ adb shell settings put global enable_gpu_debug_layers 1
    
  2. Especifica la aplicación de destino para habilitar las capas:

    $ adb shell settings put global gpu_debug_app <package_name>
    
  3. Especifica la lista de capas que deseas habilitar (de arriba abajo), separándolas con dos puntos:

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

    Como tenemos una sola capa de validación de Khronos, es probable que el comando se vea así:

    $ adb shell settings put global gpu_debug_layers VK_LAYER_KHRONOS_validation
    
  4. Especifica uno o más paquetes para buscar capas dentro de lo siguiente:

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

Puedes comprobar si los parámetros de configuración están habilitados con los siguientes comandos:

$ 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

Dado que los parámetros de configuración que aplicas persisten luego de todos los reinicios del dispositivo, puedes borrarlos una vez que se carguen las capas:

$ 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

Cómo habilitar las capas de manera global

Puedes habilitar una o más capas de manera global hasta el próximo reinicio. Esta acción genera un intento de cargar las capas para todas las aplicaciones, incluidas las nativas ejecutables.

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