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 resultar em uma queda de performance. A Vulkan tem camadas de validação que oferecem verificação de erros durante o desenvolvimento, evitando a queda de performance no build de lançamento do app. As camadas de validação dependem de um mecanismo de camadas de uso geral que intercepta os pontos de entrada da API.

Camada de validação única do Khronos

Antes, a Vulkan fornecia várias camadas de validação que precisavam ser ativadas em uma ordem específica. Na versão 1.1.106.0 e mais recentes do SDK da Vulkan, seu app só precisa ativar uma camada de validação única (link em inglês), a VK_LAYER_KHRONOS_validation, para poder usar todos os recursos das camadas de validação anteriores.

Usar camadas de validação empacotadas no APK

O empacotamento de camadas de validação no APK garante compatibilidade ideal. As camadas estão disponíveis como binários pré-criados ou podem ser criadas usando o código-fonte.

Usar binários pré-criados

Faça o download dos binários mais recentes da camada de validação da Vulkan no Android na página de versão do GitHub (link em inglês).

A maneira mais fácil de adicionar as camadas ao seu APK é extrair os binários de camada pré-criados para o diretório src/main/jniLibs/ do módulo, com os diretórios de ABI (como arm64-v8a ou x86-64) intactos, desta forma:

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

Criar a camada de validação usando o código-fonte

Para depurar o código-fonte para a camada de validação, extraia a fonte mais recente do repositório GitHub (link em inglês) do Khronos Group e siga as instruções de criação.

Verificar se a camada de validação está empacotada corretamente

Independentemente de você usar camadas pré-criadas do Khronos ou camadas criadas usando o código-fonte, o processo de compilação produzirá uma estrutura de arquivo final no APK semelhante a esta:

lib/
  arm64-v8a/
    libVkLayer_khronos_validation.so
  armeabi-v7a/
    libVkLayer_khronos_validation.so
  x86/
    libVkLayer_khronos_validation.so
  x86-64/
    libVkLayer_khronos_validation.so

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

$ jar -tf project.apk | grep libVkLayer
lib/x86_64/libVkLayer_khronos_validation.so
lib/armeabi-v7a/libVkLayer_khronos_validation.so
lib/arm64-v8a/libVkLayer_khronos_validation.so
lib/x86/libVkLayer_khronos_validation.so

Ativar uma camada de validação durante a criação da instância

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

  • VkInstance
  • VkPhysicalDevice
  • VkDevice
  • VkCommandBuffer
  • VkQueue

Chame vkEnumerateInstanceLayerProperties() para listar as camadas disponíveis e as propriedades delas. A Vulkan ativa camadas quando o método vkCreateInstance() é executado (links em inglês).

O snippet de código abaixo mostra como um app pode usar a API Vulkan para consultar e ativar camadas de forma programática:

// Enable just the Khronos validation layer.
static const char *layers[] = {"VK_LAYER_KHRONOS_validation"};

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

// Enumerate layers with a valid pointer in the last parameter.
VkLayerProperties layer_props[instance_layer_present_count];
vkEnumerateInstanceLayerProperties(&instance_layer_present_count, layer_props);

// Make sure selected validation layers are available.
VkLayerProperties *layer_props_end = layer_props + instance_layer_present_count;
for (const char* layer:layers) {
  assert(layer_props_end !=
  std::find_if(layer_props, layer_props_end, [layer](VkLayerProperties layerProperties) {
    return strcmp(layerProperties.layerName, layer) == 0;
  }));
}

// Create a Vulkan instance, requesting all enabled layers or extensions
// available on the system
VkInstanceCreateInfo instanceCreateInfo{
  .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
  .pNext = nullptr,
  .pApplicationInfo = &appInfo,
  .enabledLayerCount = sizeof(layers) / sizeof(layers[0]),
  .ppEnabledLayerNames = 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 a aparência a seguir, aqui com a adição de quebras de linha para facilitar a rolagem:

Validation -- Validation Error:
  [ VUID-VkDeviceQueueCreateInfo-pQueuePriorities-parameter ]
Object 0: VK_NULL_HANDLE, type = VK_OBJECT_TYPE_DEVICE; | MessageID = 0xd6d720c6 |
vkCreateDevice: required parameter
  pCreateInfo->pQueueCreateInfos[0].pQueuePriorities specified as NULL.
The Vulkan spec states: pQueuePriorities must be a valid pointer to an array of
  queueCount float values
  (https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html
  #VUID-VkDeviceQueueCreateInfo-pQueuePriorities-parameter)

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 transmite mensagens da camada de validação para um callback fornecido pelo aplicativo. Seu dispositivo pode não implementar essa extensão, mas ela é implementada nas camadas de validação mais recentes. Há também uma extensão descontinuada chamada VK_EXT_debug_report, que oferece recursos semelhantes se VK_EXT_debug_utils não estiver disponível.

Antes de usar a extensão Debug Utils, confira se o dispositivo ou uma camada de validação carregada é compatível. O exemplo a seguir mostra como verificar isso e registrar um callback se a extensão for compatível com o dispositivo ou com a camada de validação.

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

// Enumerate the instance extensions.
VkExtensionProperties inst_exts[inst_ext_count];
vkEnumerateInstanceExtensionProperties(nullptr, &inst_ext_count, inst_exts);

// Check for debug utils extension within the system driver or loader.
// Check if the debug utils extension is available (in the driver).
VkExtensionProperties *inst_exts_end = inst_exts + inst_ext_count;
bool debugUtilsExtAvailable = inst_exts_end !=
  std::find_if(inst_exts, inst_exts_end, [](VkExtensionProperties
    extensionProperties) {
    return strcmp(extensionProperties.extensionName,
      VK_EXT_DEBUG_UTILS_EXTENSION_NAME) == 0;
  });

if ( !debugUtilsExtAvailable ) {
  // Also check the layers for the debug utils extension.
  for (auto layer: layer_props) {
    uint32_t layer_ext_count;
    vkEnumerateInstanceExtensionProperties(layer.layerName, &layer_ext_count,
      nullptr);
    if (layer_ext_count == 0) continue;
    VkExtensionProperties layer_exts[layer_ext_count];
    vkEnumerateInstanceExtensionProperties(layer.layerName, &layer_ext_count,
    layer_exts);

    VkExtensionProperties * layer_exts_end = layer_exts + layer_ext_count;
    debugUtilsExtAvailable = layer_exts != std::find_if(
      layer_exts, layer_exts_end,[](VkExtensionProperties extensionProperties) {
        return strcmp(extensionProperties.extensionName,
        VK_EXT_DEBUG_UTILS_EXTENSION_NAME) == 0;
      });
    if (debugUtilsExtAvailable) {
        // Add the including layer into the layer request list if necessary.
        break;
    }
  }
}

if (!debugUtilsExtAvailable) return; // since this snippet depends on debugUtils

const char * enabled_inst_exts[] = { ..., VK_EXT_DEBUG_UTILS_EXTENSION_NAME };
uint32_t enabled_extension_count =
  sizeof(enabled_inst_exts)/sizeof(enabled_inst_exts[0]);

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

// NOTE: Can still return VK_ERROR_EXTENSION_NOT_PRESENT if validation layer
// isn't loaded.
vkCreateInstance(&instance_info, nullptr, &instance);

auto pfnCreateDebugUtilsMessengerEXT =
  (PFN_vkCreateDebugUtilsMessengerEXT)vkGetInstanceProcAddr(
    tutorialInstance, "vkCreateDebugUtilsMessengerEXT");
auto pfnDestroyDebugUtilsMessengerEXT =
  (PFN_vkDestroyDebugUtilsMessengerEXT)vkGetInstanceProcAddr(
    tutorialInstance, "vkDestroyDebugUtilsMessengerEXT");

// Create the debug messenger callback with your the settings you want.
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;

  // The DebugUtilsMessenger callback is explained in the following section.
  messengerInfo.pfnUserCallback = &DebugUtilsMessenger;
  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);
}

Depois que o app registrar e ativar o callback, o sistema encaminhará mensagens de depuração para ele.

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

Usar camadas de validação externas

Não é necessário empacotar camadas de validação no APK. Os dispositivos com o Android 9 (API de nível 28) e versões mais recentes podem usar camadas de validação externas ao seu binário e desativá-las dinamicamente. Siga as etapas desta seção para enviar camadas de validação ao seu dispositivo de teste:

Permitir que o app use camadas de validação externas

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

  • O app de destino é depurável. Essa opção resulta em mais informações de depuração, mas pode afetar negativamente o desempenho do app.

  • O app de destino é executado em um build userbug do sistema operacional que concede acesso raiz.

  • Apps direcionados apenas ao Android 11 (API de nível 30) ou versões mais recentes: o arquivo de manifesto de destino do Android inclui o seguinte elemento meta-data:

    <meta-data android:name="com.android.graphics.injectLayers.enable"
      android:value="true"/>
    

Carregar uma camada de validação externa

Dispositivos com o Android 9 (API de nível 28) e versões mais recentes permitem que a Vulkan carregue a camada de validação do armazenamento local do app. A partir do Android 10 (API de nível 29), a Vulkan também pode carregar a camada de validação de um APK separado. Você pode escolher qualquer método que preferir, desde que a versão do Android seja compatível.

Carregar um binário de camada de validação do armazenamento local do dispositivo

A Vulkan busca o binário no diretório de armazenamento de dados temporário do dispositivo. Portanto, primeiro é necessário enviar o binário para esse diretório usando o Android Debug Bridge (adb), desta maneira:

  1. Use o comando adb push para carregar o binário de camada no armazenamento de dados do app no dispositivo:

    $ adb push libVkLayer_khronos_validation.so /data/local/tmp
    
  2. Use os comandos adb shell e run-as para carregar a camada pelo processo do app. O binário tem 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 a camada.

Carregar um binário da camada de validação de outro APK

Você pode usar o adb para instalar um APK que contém a camada e, em seguida, ativá-la.

adb install --abi abi path_to_apk

Ativar camadas fora do app

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

Ativar camadas por app

As etapas a seguir descrevem como ativar camadas por app:

  1. Use as configurações do shell do adb para ativar as camadas:

    $ adb shell settings put global enable_gpu_debug_layers 1
    
  2. Especifique o app de destino para ativar as camadas:

    $ adb shell settings put global gpu_debug_app <package_name>
    
  3. Especifique a lista de camadas a serem ativadas (de cima para baixo), separando cada camada com dois-pontos:

    $ adb shell settings put global gpu_debug_layers <layer1:layer2:layerN>
    

    Como temos uma única camada de validação do Khronos, o comando provavelmente terá esta aparência:

    $ adb shell settings put global gpu_debug_layers VK_LAYER_KHRONOS_validation
    
  4. Especifique um ou mais pacotes para pesquisar camadas dentro de:

    $ adb shell settings put global
      gpu_debug_layer_app <package1:package2:packageN>
    

Você pode conferir se as configurações estão ativadas usando 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_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

Ativar camadas globalmente

Você pode ativar uma ou mais camadas globalmente até a próxima reinicialização. Isso fará com que o dispositivo tente carregar as camadas para todos os aplicativos, incluindo executáveis nativos.

$ adb shell setprop debug.vulkan.layers <layer1:layer2:layerN>