Camadas de validação da 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. A 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 da Vulkan. As camadas de validação interceptam ou capturam pontos de entrada da Vulkan para diversos fins de depuração e validação.

A camada de validação intercepta os pontos de entrada para os quais ela contém definições. Um ponto de entrada não definido na camada alcança o driver (o nível de base) invalidado.

As amostras do Android NDK e da Vulkan incluem camadas de validação da Vulkan para uso durante o desenvolvimento. É possível vincular a camada de validação à pilha de gráficos, permitindo que ela relate problemas de validação. Com essa instrumentação, é possível identificar e consertar usos indevidos durante o desenvolvimento.

A camada de validação única do Khronos

As camadas da Vulkan podem ser inseridas pelo carregador em uma pilha para que camadas de nível superior chamem a camada abaixo e, assim, a pilha de camadas terminará no driver do dispositivo. Anteriormente, existiam várias camadas de validação que eram ativadas em uma ordem específica no Android. No entanto, agora existe uma única camada, VK_LAYER_KHRONOS_validation, que engloba todo o comportamento da camada de validação anterior. Para a validação da Vulkan, todos os apps precisam ativar a única camada de validação, VK_LAYER_KHRONOS_validation.

Empacotar a camada de validação

O NDK inclui binários de camada de validação pré-compilados que podem ser enviados ao dispositivo de teste agrupando-os ao APK. É possível encontrar esse binário no seguinte diretório: ndk-dir/sources/third_party/vulkan/src/build-android/jniLibs/abi/ Quando solicitado pelo app, o carregador da Vulkan encontra e carrega a camada do APK do app.

Empacotar a camada de validação no APK com o Gradle

Para adicionar a camada de validação ao seu projeto, use o Plug-in do Android para Gradle e a compatibilidade do Android Studio com o CMake e o ndk-build. Para adicionar as bibliotecas usando o suporte do Android Studio para o CMake e o ndk-build, adicione 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 a camada 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/

Compilar binário de camada da fonte

Caso seu app precise da camada de validação mais recente, pesquise a fonte mais recente no repositório GitHub (link em inglês) do Khronos Group e siga as instruções de compilação.

Verificar a criação de camadas

Independentemente de você usar camadas pré-criadas 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_khronos_validation.so
  armeabi-v7a/
    libVkLayer_khronos_validation.so

O exemplo a seguir mostra como verificar se o APK contém a camada de validação esperada:

$ jar -xvf project.apk
 ...
 inflated: lib/arm64-v8a/libVkLayer_khronos_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 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;
...

Saída padrão do logcat

A camada de validação emite mensagens de aviso e erro no logcat rotuladas com uma tag VALIDATION. Uma mensagem de camada de validação tem esta aparência:
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

Ativar o callback de depuração

A extensão Debug Utils VK_EXT_debug_utils permite que o app crie um mensageiro de depuração que transmitirá as mensagens da camada de validação para um callback fornecido pelo app. Também existe uma extensão obsoleta, VK_EXT_debug_report, que oferece recursos semelhantes se VK_EXT_debug_utils não está disponível.

Antes de usar a extensão Debug Utils, é necessário primeiro 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 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);
}

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

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

Enviar camadas ao seu dispositivo de teste usando o ADB

Siga as etapas desta seção para enviar camadas para seu dispositivo de teste:

Ativar depuração

As políticas e o modelo de segurança do Android são muito diferentes dos que são usados por outras plataformas. Para carregar camadas externas, uma das seguintes condições precisa ser verdadeira:

  • O arquivo de manifesto do app de destino inclui o seguinte elemento de metadados (aplica-se apenas a apps direcionados para Android 11 nível de API "R" ou mais recente):
    <meta-data android:name="com.android.graphics.injectLayers.enable" android:value="true" /> Use essa opção para criar o perfil do seu aplicativo.
  • O app de destino é depurável. Essa opção fornece mais informações de depuração, mas pode afetar negativamente o desempenho do app.
  • O aplicativo de destino é executado em um build userbug do sistema operacional que concede acesso raiz.

Carregar as camadas

Dispositivos com o Android 9 (nível da API 28) e versões mais recentes permitem que a Vulkan carregue camadas do armazenamento local do app. O Android 10 (nível de API 29) é compatível com o carregamento de camadas a partir de um APK separado.

Binários de camada no armazenamento local do dispositivo

A 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 de camada desejados no armazenamento de dados do seu app no dispositivo. O exemplo a seguir envia libVkLayer_khronos_validation.so para o diretório /data/local/tmp do dispositivo.
    $ adb push libVkLayer_khronos_validation.so /data/local/tmp
    
  2. Use os comandos adb shell e run-as para carregar as camadas por meio do 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_khronos_validation.so .
    $ adb shell run-as com.example.myapp ls libVkLayer_khronos_validation.so
    
  3. Ative as camadas.

APK que contém as camadas

Você pode usar adb para instalar o APK e, em seguida, ativar as camadas.

adb install --abi abi path_to_apk

Ativar camadas fora do app

Você pode ativar as camadas por app ou de forma global. As configurações por app persistem em reinicializações, enquanto as propriedades globais são apagadas na reinicialização.

Para ativar camadas por app:

# 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 verificar se as configurações estão ativadas, você pode usar os seguintes 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

Como as configurações aplicadas 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
$ adb shell settings delete global gpu_debug_layer_app

Para desativar camadas por app:

# 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 ativar camadas de forma global:

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