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 hacerlo, debes habilitar las capas de validación. Estas capas interceptan o enlazan puntos de entrada de Vulkan para diferentes fines de depuración y validación.

Cada capa de validación puede contener definiciones para uno o más de esos puntos de entrada e intercepta los puntos de entrada para los cuales contiene definiciones. Cuando una capa de validación no define un punto de entrada, el sistema lo pasa a la capa siguiente. En última instancia, un punto de entrada que no se definió en ninguna capa llega al controlador, el nivel de base, sin validación.

Los ejemplos del SDK de Android, del NDK y de Vulkan incluyen capas de validación de Vulkan para usar durante el desarrollo. Puedes incluir esas capas en la pila de gráficos. Esto les permite comunicar problemas de validación. Esta instrumentación te brinda la posibilidad de detectar y solucionar usos inadecuados durante el desarrollo.

En esta página se explica la manera de:

  • Cargar capas de validación en tu dispositivo de prueba
  • Obtener el código fuente para las capas de validación
  • Verificar la compilación de las capas
  • Habilitar capas en la aplicación Vulkan

Carga capas de validación en tu dispositivo de prueba

El NDK incluye objetos binarios de capa de validación compilados previamente que puedes enviar al dispositivo de prueba ya sea empaquetándolos en el APK o cargándolos mediante Android Debug Bridge (ADB). Estos objetos binarios se encuentran en el siguiente directorio: ndk-dir/sources/third_party/vulkan/src/build-android/jniLibs/abi/

Cuando lo solicita la aplicación, el cargador de Vulkan busca y carga las capas desde el APK de la aplicación o desde el directorio de datos locales. En esta sección, se exploran varias formas de enviar los objetos binarios de capas al dispositivo de prueba. Si bien el cargador de Vulkan puede detectar objetos binarios de capas en varias fuentes del dispositivo, debes usar solo uno de los métodos que se describen a continuación.

Empaqueta capas de validación en el APK con Gradle

Puedes agregar las capas de validación al proyecto mediante el complemento Gradle para Android y la compatibilidad de Android Studio con CMake y ndk-build.

Para agregar las bibliotecas mediante la compatibilidad de Android Studio con CMake y ndk-build, agrega lo siguiente al archivo build.gradle del módulo de tu aplicación:

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

Envía los objetos binarios de capas al dispositivo de prueba mediante ADB

Los dispositivos que ejecutan Android 9 (nivel 28 de la API) y versiones posteriores permiten que Vulkan cargue objetos binarios de capas desde el almacenamiento local del dispositivo. Es decir, al cargar los objetos binarios desde el dispositivo, ya no necesitas agruparlos con el APK de la aplicación. No obstante, la aplicación instalada se debe poder depurar. 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 desees en el almacenamiento de datos de la aplicación en el dispositivo. El siguiente ejemplo envía libVkLayer_unique_objects.so al directorio /data/local/tmp del dispositivo:
        $ adb push libVkLayer_unique_objects.so /data/local/tmp
        
  2. Usa los comandos adb shell y run-as para cargar las capas mediante el proceso de tu aplicación. 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_unique_objects.so
        $ adb shell run-as com.example.myapp ls libVkLayer_unique_objects.so
        
  3. Usa el comando adb shell settings para autorizar a Vulkan a cargar las capas desde el almacenamiento del dispositivo:
        $ adb shell settings put global enable_gpu_debug_layers 1
        $ adb shell settings put global gpu_debug_app com.example.myapp
        $ adb shell settings put global gpu_debug_layers VK_LAYER_GOOGLE_unique_objects
        

    Sugerencia: También puedes habilitar esta configuración desde las opciones para el desarrollador en el dispositivo. Después de habilitar las opciones para desarrolladores, abre la app de Configuración en el dispositivo de prueba, desplázate hasta Opciones para programadores > Depuración y asegúrate de que la opción Habilitar depuración GPU esté activada.

  4. Para comprobar si la configuración del paso 3 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_GOOGLE_unique_objects
        
  5. Dado que las opciones que aplicas en el paso 3 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
        

Compila objetos binarios de capas 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_core_validation.so
        libVkLayer_object_tracker.so
        libVkLayer_parameter_validation.so
        libVkLayer_threading.so
        libVkLayer_unique_objects.so
      armeabi-v7a/
        libVkLayer_core_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_threading.so
     inflated: lib/arm64-v8a/libVkLayer_object_tracker.so
     inflated: lib/arm64-v8a/libVkLayer_unique_objects.so
     inflated: lib/arm64-v8a/libVkLayer_parameter_validation.so
     inflated: lib/arm64-v8a/libVkLayer_core_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 aplicación puede usar la API de Vulkan API para habilitar y consultar programáticamente una capa:

    // 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 layers are available
    // NOTE:  These are not listed in an arbitrary order.  Threading must be
    //        first, and unique_objects must be last.  This is the order they
    //        will be inserted by the loader.
    const char *instance_layers[] = {
        "VK_LAYER_GOOGLE_threading",
        "VK_LAYER_LUNARG_parameter_validation",
        "VK_LAYER_LUNARG_object_tracker",
        "VK_LAYER_LUNARG_core_validation",
        "VK_LAYER_GOOGLE_unique_objects"
    };

    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 layers 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_report del informe de depuración permite que tu aplicación controle el comportamiento de la capa cuando se produce un evento.

Antes de usar esta extensión, debes asegurarte de que la plataforma la admita. 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 report extension is available
    for (uint32_t i = 0; i < inst_ext_count; i++) {
        if (strcmp(inst_exts[i].extensionName,
        VK_EXT_DEBUG_REPORT_EXTENSION_NAME) == 0) {
            enabled_inst_exts[enabled_inst_ext_count++] =
                VK_EXT_DEBUG_REPORT_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_vkCreateDebugReportCallbackEXT vkCreateDebugReportCallbackEXT;
    PFN_vkDestroyDebugReportCallbackEXT vkDestroyDebugReportCallbackEXT;

    vkCreateDebugReportCallbackEXT = (PFN_vkCreateDebugReportCallbackEXT)
        vkGetInstanceProcAddr(instance, "vkCreateDebugReportCallbackEXT");
    vkDestroyDebugReportCallbackEXT = (PFN_vkDestroyDebugReportCallbackEXT)
        vkGetInstanceProcAddr(instance, "vkDestroyDebugReportCallbackEXT");

    assert(vkCreateDebugReportCallbackEXT);
    assert(vkDestroyDebugReportCallbackEXT);

    // Create the debug callback with desired settings
    VkDebugReportCallbackEXT debugReportCallback;
    if (vkCreateDebugReportCallbackEXT) {
        VkDebugReportCallbackCreateInfoEXT debugReportCallbackCreateInfo;
        debugReportCallbackCreateInfo.sType =
            VK_STRUCTURE_TYPE_DEBUG_REPORT_CREATE_INFO_EXT;
        debugReportCallbackCreateInfo.pNext = NULL;
        debugReportCallbackCreateInfo.flags = VK_DEBUG_REPORT_ERROR_BIT_EXT |
                                              VK_DEBUG_REPORT_WARNING_BIT_EXT |
                                              VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT;
        debugReportCallbackCreateInfo.pfnCallback = DebugReportCallback;
        debugReportCallbackCreateInfo.pUserData = NULL;

        vkCreateDebugReportCallbackEXT(instance, &debugReportCallbackCreateInfo,
                                       nullptr, &debugReportCallback);
    }

    // Later, when shutting down Vulkan, call the following
    if (vkDestroyDebugReportCallbackEXT) {
       vkDestroyDebugReportCallbackEXT(instance, debugReportCallback, nullptr);
    }

    

Una vez que la aplicación 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>

    static VKAPI_ATTR VkBool32 VKAPI_CALL DebugReportCallback(
                                       VkDebugReportFlagsEXT msgFlags,
                                       VkDebugReportObjectTypeEXT objType,
                                       uint64_t srcObject, size_t location,
                                       int32_t msgCode, const char * pLayerPrefix,
                                       const char * pMsg, void * pUserData )
    {
       if (msgFlags & VK_DEBUG_REPORT_ERROR_BIT_EXT) {
           __android_log_print(ANDROID_LOG_ERROR,
                               "AppName",
                               "ERROR: [%s] Code %i : %s",
                               pLayerPrefix, msgCode, pMsg);
       } else if (msgFlags & VK_DEBUG_REPORT_WARNING_BIT_EXT) {
           __android_log_print(ANDROID_LOG_WARN,
                               "AppName",
                               "WARNING: [%s] Code %i : %s",
                               pLayerPrefix, msgCode, pMsg);
       } else if (msgFlags & VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT) {
           __android_log_print(ANDROID_LOG_WARN,
                               "AppName",
                               "PERFORMANCE WARNING: [%s] Code %i : %s",
                               pLayerPrefix, msgCode, pMsg);
       } else if (msgFlags & VK_DEBUG_REPORT_INFORMATION_BIT_EXT) {
           __android_log_print(ANDROID_LOG_INFO,
                               "AppName", "INFO: [%s] Code %i : %s",
                               pLayerPrefix, msgCode, pMsg);
       } else if (msgFlags & VK_DEBUG_REPORT_DEBUG_BIT_EXT) {
           __android_log_print(ANDROID_LOG_VERBOSE,
                               "AppName", "DEBUG: [%s] Code %i : %s",
                               pLayerPrefix, msgCode, pMsg);
       }

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