Capas de validación de Vulkan en Android

La mayoría de las API de gráficos explícitos no realizan comprobación de errores, porque hacerlo puede generar una penalidad de rendimiento. Vulkan proporciona comprobación de errores de modo que puedas usar esta función durante el desarrollo y excluirla de la compilación de actualización de la app, lo que evita la penalidad cuando resultaría más perjudicial. Para eso, habilita la capa de validación de Vulkan. Esta capa de validación intercepta o atrapa los puntos de entrada de Vulkan para diversos propósitos de depuración y validación.

La capa de validación intercepta los puntos de entrada para los que contiene definiciones. Un punto de entrada que no se definió en la capa llega al controlador, el nivel de base, sin validación.

Los ejemplos del NDK de Android y Vulkan incluyen la capa de validación de Vulkan para usar durante el desarrollo. Puedes incluir la capa de validación en la pila de gráficos, lo que le permite informar errores de validación. Esta instrumentación te brinda la posibilidad de detectar y solucionar usos inadecuados durante el desarrollo.

La capa de validación única de Khronos

El cargador puede insertar capas de Vulkan en una pila de modo que las de mayor nivel llamen a la capa inferior y la pila de capas finalice en el controlador del dispositivo. En el pasado, varias capas de validación se habilitaban en un orden específico en Android. Sin embargo, ahora hay una sola capa, VK_LAYER_KHRONOS_validation, que abarca todo el comportamiento de las capas de validación anteriores. Para la validación de Vulkan, todas las apps deben habilitar la capa de validación única, VK_LAYER_KHRONOS_validation.

Cómo empaquetar la capa de validación

El NDK incluye un objeto binario de capa de validación compilado previamente que puedes enviar al dispositivo de prueba, empaquetándolo en el APK. Encontrarás este objeto binario en el siguiente directorio: ndk-dir/sources/third_party/vulkan/src/build-android/jniLibs/abi/. Cuando la app lo solicita, el cargador Vulkan busca y carga la capa desde el APK de la app.

Cómo empaquetar la capa de validación en tu APK con Gradle

Puedes agregar la capa de validación al proyecto mediante el complemento de Gradle para Android y la compatibilidad de Android Studio con CMake y ndk-build. Para incorporar las bibliotecas usando la compatibilidad de Android Studio con CMake y ndk-build, agrega lo siguiente al archivo build.gradle del módulo de tu app:
sourceSets {
  main {
    jniLibs {
      // Gradle includes libraries in the following path as dependencies
      // of your CMake or ndk-build project so that they are packaged in
      // your app’s APK.
      srcDir "ndk-path/sources/third_party/vulkan/src/build-android/jniLibs"
    }
  }
}
Para obtener más información sobre la compatibilidad de Android Studio con CMake y ndk-build, consulta Cómo agregar código C y C++ a tu proyecto.

Cómo empaquetar la capa de validación en bibliotecas JNI

Puedes agregar manualmente los objetos binarios de la capa de validación al directorio de bibliotecas JNI de tu proyecto usando las siguientes opciones de líneas de comandos:
$ cd project-root
$ mkdir -p app/src/main
$ cp -fr ndk-path/sources/third_party/vulkan/src/build-android/jniLibs app/src/main/

Cómo compilar objetos binarios de capas desde un código fuente

Si tu app necesita la última capa de validación, puedes obtener el código fuente más reciente desde el repositorio de GitHub de Khronos Group y seguir las instrucciones de compilación que allí se detallan.

Cómo verificar la compilación de las capas

Independientemente de que realices compilaciones con capas del NDK compiladas previamente o a partir del código fuente más reciente, el proceso de compilación genera una estructura de archivo final como la siguiente:

src/main/jniLibs/
  arm64-v8a/
    libVkLayer_khronos_validation.so
  armeabi-v7a/
    libVkLayer_khronos_validation.so

En el siguiente ejemplo, se muestra la manera de verificar que tu APK contenga las capas de validación según lo previsto:

$ jar -xvf project.apk
 ...
 inflated: lib/arm64-v8a/libVkLayer_khronos_validation.so
 ...

Cómo habilitar las capas

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

  • VkInstance
  • VkPhysicalDevice
  • VkDevice
  • VkCommandBuffer
  • VkQueue

Puedes llamar a vkEnumerateInstanceLayerProperties() para que enumere las capas disponibles y sus propiedades. El sistema habilita las capas cuando se ejecuta vkCreateInstance().

En el siguiente fragmento de código, se muestra la manera en que una app puede usar la API de Vulkan para habilitar y consultar una capa de forma programática:

// Get layer count using null pointer as last parameter
uint32_t instance_layer_present_count = 0;
vkEnumerateInstanceLayerProperties(&instance_layer_present_count, nullptr);

// Enumerate layers with valid pointer in last parameter
VkLayerProperties* layer_props =
    (VkLayerProperties*)malloc(instance_layer_present_count * sizeof(VkLayerProperties));
vkEnumerateInstanceLayerProperties(&instance_layer_present_count, layer_props));

// Make sure the desired validation layer is available
const char *instance_layers[] = {
    "VK_LAYER_KHRONOS_validation"
};

uint32_t instance_layer_request_count =
    sizeof(instance_layers) / sizeof(instance_layers[0]);
for (uint32_t i = 0; i < instance_layer_request_count; i++) {
    bool found = false;
    for (uint32_t j = 0; j < instance_layer_present_count; j++) {
        if (strcmp(instance_layers[i], layer_props[j].layerName) == 0) {
            found = true;
        }
    }
    if (!found) {
        error();
    }
}

// Pass desired layer into vkCreateInstance
VkInstanceCreateInfo instance_info = {};
instance_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
instance_info.enabledLayerCount = instance_layer_request_count;
instance_info.ppEnabledLayerNames = instance_layers;
...

Salida predeterminada de logcat

La capa de validación emite mensajes de advertencia y error en logcat, etiquetados con una etiqueta VALIDATION. Un mensaje de la capa de validación se ve como lo siguiente:
VALIDATION: UNASSIGNED-CoreValidation-DrawState-QueueForwardProgress(ERROR / SPEC):
            msgNum: 0 - VkQueue 0x7714c92dc0[] is waiting on VkSemaphore 0x192e[]
            that has no way to be signaled.
VALIDATION:     Objects: 1
VALIDATION:         [0] 0x192e, type: 5, name: NULL

Cómo habilitar la devolución de llamada de depuración

La extensión VK_EXT_debug_utils de Debug Utils permite que tu app cree un mensajero de depuración que pasará los mensajes de la capa de validación a una devolución de llamada proporcionada por la aplicación. Ten en cuenta que también hay una extensión obsoleta, VK_EXT_debug_report, que proporciona una capacidad similar si VK_EXT_debug_utils no está disponible.

Antes de usar la extensión de Debug Utils, debes asegurarte de que sea compatible con la plataforma. En el siguiente ejemplo, se muestra la manera de comprobar la compatibilidad con la extensión de depuración y de registrar una devolución de llamada si se admite la extensión.

// Get the instance extension count
uint32_t inst_ext_count = 0;
vkEnumerateInstanceExtensionProperties(nullptr, &inst_ext_count, nullptr);

// Enumerate the instance extensions
VkExtensionProperties* inst_exts =
    (VkExtensionProperties *)malloc(inst_ext_count * sizeof(VkExtensionProperties));
vkEnumerateInstanceExtensionProperties(nullptr, &inst_ext_count, inst_exts);

const char * enabled_inst_exts[16] = {};
uint32_t enabled_inst_ext_count = 0;

// Make sure the debug utils extension is available
for (uint32_t i = 0; i < inst_ext_count; i++) {
    if (strcmp(inst_exts[i].extensionName,
    VK_EXT_DEBUG_UTILS_EXTENSION_NAME) == 0) {
        enabled_inst_exts[enabled_inst_ext_count++] =
            VK_EXT_DEBUG_UTILS_EXTENSION_NAME;
    }
}

if (enabled_inst_ext_count == 0)
    return;

// Pass the instance extensions into vkCreateInstance
VkInstanceCreateInfo instance_info = {};
instance_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
instance_info.enabledExtensionCount = enabled_inst_ext_count;
instance_info.ppEnabledExtensionNames = enabled_inst_exts;

PFN_vkCreateDebugUtilsMessengerEXT pfnCreateDebugUtilsMessengerEXT;
PFN_vkDestroyDebugUtilsMessengerEXT pfnDestroyDebugUtilsMessengerEXT;

pfnCreateDebugUtilsMessengerEXT = (PFN_vkCreateDebugUtilsMessengerEXT)
     vkGetDeviceProcAddr(device, "vkCreateDebugUtilsMessengerEXT");
pfnDestroyDebugUtilsMessengerEXT = (PFN_vkDestroyDebugUtilsMessengerEXT)
     vkGetDeviceProcAddr(device, "vkDestroyDebugUtilsMessengerEXT");

assert(pfnCreateDebugUtilsMessengerEXT);
assert(pfnDestroyDebugUtilsMessengerEXT);

// Create the debug messenger callback with desired settings
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;
    messengerInfo.pfnUserCallback = &DebugUtilsMessenger; // Callback example below
    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 registra y habilita la devolución de llamada de depuración, el sistema enruta los mensajes de depuración a una devolución de llamada registrada. El siguiente es un ejemplo de una devolución de llamada como esta:

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

Envía capas al dispositivo de prueba mediante ADB

Sigue los pasos de esta sección para enviar capas a tu dispositivo de prueba:

Cómo habilitar la depuración

El modelo de seguridad y las políticas de Android difieren considerablemente de los de otras plataformas. Para cargar capas externas, una de las siguientes opciones debe ser verdadera:

  • El archivo de manifiesto de la app de destino incluye el siguiente elemento de metadatos (solo se aplica para apps orientadas a Android 11 [API nivel “R”] o versiones posteriores):
    <meta-data android:name="com.android.graphics.injectLayers.enable" android:value="true" /> Debes usar esta opción para perfilar tu aplicación.
  • La app de destino es depurable. Esta opción te brinda 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 de raíz.

Cómo cargar las capas

Los dispositivos que ejecutan Android 9 (API nivel 28) y versiones posteriores permiten que Vulkan cargue capas desde el almacenamiento local de la app. Android 10 (API nivel 29) admite la carga de capas desde un APK independiente.

Objetos binarios de capas en el almacenamiento local del dispositivo

Vulkan busca los objetos binarios en el directorio de almacenamiento de datos temporales del dispositivo, así que primero debes enviar los objetos binarios a ese directorio mediante Android Debug Bridge (ADB). Sigue estos pasos:

  1. Usa el comando adb push para cargar los objetos binarios de capas que quieras en el almacenamiento de datos de la app en el dispositivo. En el siguiente ejemplo, se envía libVkLayer_khronos_validation.so al directorio /data/local/tmp del dispositivo:
    $ adb push libVkLayer_khronos_validation.so /data/local/tmp
    
  2. Usa los comandos adb shell y run-as para cargar las capas a través del proceso de tu app. Esto implica que los objetos binarios tienen el mismo acceso al dispositivo que la aplicación, pero no se requiere acceso al directorio raíz.
    $ 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 las capas.

APK que contiene las capas

Puedes usar adb para instalar el APK y, luego, habilitar las capas.

adb install --abi abi path_to_apk

Cómo habilitar las capas fuera de la aplicación

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

Para habilitar las capas por app, haz lo siguiente:

# Enable layers
adb shell settings put global enable_gpu_debug_layers 1

# Specify target application
adb shell settings put global gpu_debug_app <package_name>

# Specify layer list (from top to bottom)
adb shell settings put global gpu_debug_layers <layer1:layer2:layerN>

# Specify packages to search for layers
adb shell settings put global gpu_debug_layer_app <package1:package2:packageN>

Para comprobar si la configuración está habilitada, usa 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 ajustes que aplicas persisten luego de todos los reinicios del dispositivo, puedes borrarlos una vez que las capas se carguen:

$ 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

Para inhabilitar las capas por app, haz lo siguiente:

# Delete the global setting that enables layers
adb shell settings delete global enable_gpu_debug_layers

# Delete the global setting that selects target application
adb shell settings delete global gpu_debug_app

# Delete the global setting that specifies layer list
adb shell settings delete global gpu_debug_layers

# Delete the global setting that specifies layer packages
adb shell settings delete global gpu_debug_layer_app

Para habilitar las capas globalmente, haz lo siguiente:

# This attempts to load layers for all applications, including native
# executables
adb shell setprop debug.vulkan.layers <layer1:layer2:layerN>