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 para la versión de tu app, y así evitar la penalidad cuando resultaría más perjudicial. Para ello, habilita la capa de validación de Vulkan. Esta capa de validación intercepta o atrae 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 permite comunicar problemas 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 de abajo y la pila de capas finalice en el controlador de 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.

Empaqueta 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 lo solicite tu app, el cargador Vulkan buscará y cargará la capa desde el APK de la app.

Empaqueta la capa de validación en tu APK con Gradle

Puedes agregar las capas de validación al proyecto mediante el complemento de Gradle para Android y la compatibilidad de Android Studio con CMake y ndk-build. Para agregar 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, lee Cómo agregar código C y C++ a tu proyecto.

Empaqueta la capa de validación en bibliotecas JNI

Puedes agregar manualmente los archivos 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/
    

Compila objetos binarios de capa desde un archivo de fuente

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

Verifica 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 la capa de validación según lo previsto:

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

Habilita las capas

La API de Vulkan permite que una app habilite capas, las cuales 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 detalle 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;
    ...
    

Habilita 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 app. 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 haya registrado y habilitado la devolución de llamada de depuración, el sistema reenvía mensajes de depuración a una devolución de llamada registrada. A continuación, se ofrece 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

Los dispositivos que ejecutan Android 9 (API nivel 28) y versiones posteriores permiten que Vulkan cargue capas desde el almacenamiento local de tu 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 del almacenamiento de datos temporales del dispositivo; por lo tanto, en primer lugar, 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 tu 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 mediante el proceso de tu app. Esto implica que los objetos binarios tienen el mismo acceso al dispositivo que la app, 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
    

Habilita las capas fuera de la app

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 las opciones que aplicas persisten en todos los reinicios del dispositivo, puedes borrar la configuración una vez que las capas se hayan cargado:

    $ 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>