Camadas de validação do Vulkan no Android

A maioria das APIs gráficas explícitas não faz a verificação de erros, porque isso pode gerar uma queda de desempenho. O Vulkan oferece uma verificação de erros que permite usar esse recurso durante o desenvolvimento, mas o exclui da versão de lançamento do app, evitando a queda de desempenho no momento mais importante. É possível fazer isso ativando as camadas de validação. As camadas de validação interceptam ou capturam pontos de entrada do Vulkan para diversos fins de depuração e validação.

Cada camada de validação pode conter definições de um ou mais desses pontos de entrada e intercepta os pontos para os quais ela contém definições. Quando uma camada de validação não define um ponto de entrada, o sistema passa o ponto para a próxima camada. Em última instância, um ponto de entrada não definido em uma camada chega ao driver (o nível de base) invalidado.

As amostras do Android SDK, do NDK e do Vulkan incluem camadas de validação do Vulkan para uso durante o desenvolvimento. É possível capturar essas camadas na pilha de gráfico, permitindo que relatem problemas de validação. Com essa instrumentação, é possível identificar e consertar usos indevidos durante o desenvolvimento.

Esta página explica como:

  • carregar camadas de validação em um dispositivo de teste;
  • conseguir o código-fonte de camadas de validação;
  • verificar a compilação de camadas;
  • ativar camadas no app Vulkan.

Carregar camadas de validação em um dispositivo de teste

O NDK inclui binários de camada de validação pré-compilados que podem ser enviados ao dispositivo de teste. Para fazer isso, basta incluí-los no seu APK ou carregá-los usando o Android Debug Bridge (ADB). Esses binários estão localizados no seguinte diretório: ndk-dir/sources/third_party/vulkan/src/build-android/jniLibs/abi/

Quando solicitado pelo app, o carregador do Vulkan encontra e carrega as camadas do diretório de dados local ou do APK do seu app. Esta seção indica várias maneiras de enviar binários da camada para um dispositivo de teste. Embora o carregador do Vulkan possa encontrar binários de camadas de várias fontes no dispositivo, você precisa usar apenas um dos métodos descritos abaixo.

Empacotar camadas de validação no APK com o Gradle

Para adicionar a camada de validação ao seu projeto, use o Android Gradle Plugin e a compatibilidade do Android Studio com o CMake e o ndk-build.

Para adicionar as bibliotecas usando a compatibilidade do Android Studio com o CMake e o ndk-build, acrescente o seguinte ao arquivo build.gradle do módulo do 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 saber mais sobre a compatibilidade do Android Studio com o CMake e o ndk-build, leia Adicione código C e C++ ao seu projeto.

Empacotar camadas de validação nas bibliotecas JNI

É possível adicionar os binários da camada de validação manualmente ao diretório das bibliotecas JNI do projeto usando as seguintes opções de linha de comando:

    $ cd project-root
    $ mkdir -p app/src/main
    $ cp -fr ndk-path/sources/third_party/vulkan/src/build-android/jniLibs app/src/main/
    

Enviar binários da camada para um dispositivo de teste usando ADB

Os dispositivos com Android 9 (API nível 28) e versões posteriores permitem que o Vulkan carregue binários de camada armazenados localmente. Isso significa que não é mais necessário agrupar os binários com o APK do app ao carregá-los do dispositivo. No entanto, o app instalado precisa ser depurável. O Vulkan busca os binários no diretório temporário de armazenamento de dados do seu dispositivo. Portanto, primeiro é necessário enviar os binários para esse diretório usando o Android Debug Bridge (ADB), da seguinte forma:

  1. Use o comando adb push para carregar os binários da camada desejada no armazenamento de dados do seu app no dispositivo. O exemplo a seguir envia libVkLayer_unique_objects.so ao diretório /data/local/tmp do dispositivo:
        $ adb push libVkLayer_unique_objects.so /data/local/tmp
        
  2. Use os comandos adb shell e run-as para carregar as camadas no processo do app. Os binários têm o mesmo acesso ao dispositivo que o app, sem exigir acesso à raiz.
        $ 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. Use o comando adb shell settings para que o Vulkan possa carregar as camadas do armazenamento do 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
        

    Dica: também é possível ativar essas configurações por meio das opções do desenvolvedor no dispositivo. Depois de ativar as opções do desenvolvedor, abra o app Config. no dispositivo de teste, vá para Opções do desenvolvedor > Depuração e verifique se a opção Ativar camadas de depuração de GPU está ativada.

  4. Para verificar se as configurações da etapa 3 estão ativadas, você pode usar os comandos a seguir:
        $ 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. Como as configurações aplicadas na etapa 3 persistem após a reinicialização do dispositivo, é recomendável apagar as configurações depois que as camadas forem carregadas:
        $ 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
        

Compilar binários de camada da fonte

Se seu app precisar da camada de validação mais recente, consiga a última fonte do repositório GitHub do Khronos Group e siga as instruções de compilação.

Verificar a compilação de camadas

Independentemente de você usar camadas pré-compiladas do NDK ou o código-fonte mais recente, o processo de compilação produzirá a seguinte estrutura de arquivo final:

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

O exemplo a seguir mostra como verificar se o APK contém as camadas de validação esperadas:

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

Ativar camadas

A API Vulkan permite que um app ative camadas. Essas camadas são ativadas durante a criação de uma instância. O primeiro parâmetro dos pontos de entrada que uma camada intercepta precisa ser um destes objetos:

  • VkInstance
  • VkPhysicalDevice
  • VkDevice
  • VkCommandBuffer
  • VkQueue

Chame vkEnumerateInstanceLayerProperties() para listar as camadas disponíveis e as propriedades delas. O sistema ativa as camadas quando vkCreateInstance() é executado.

O snippet de código a seguir mostra como um app pode usar a API Vulkan para ativar e consultar programaticamente uma camada:

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

Ativar o callback de depuração

A extensão do Relatório de depuração VK_EXT_debug_report permite que seu app controle o comportamento da camada quando um evento ocorre.

Antes de usar essa extensão, primeiro é preciso verificar se a plataforma é compatível com ela. O exemplo a seguir mostra como verificar se há compatibilidade com a extensão de depuração e registrar um callback se ela for compatível.

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

    

Quando o app registra e ativa o callback de depuração, o sistema encaminha mensagens de depuração a um callback registrado. Veja um exemplo desse callback abaixo:

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