Слои проверки Vulkan на Android

Большинство явных графических API не выполняют проверку ошибок, поскольку это может привести к снижению производительности. В Vulkan предусмотрены уровни валидации , которые обеспечивают проверку ошибок во время разработки, предотвращая снижение производительности в финальной сборке приложения. Уровни валидации основаны на универсальном механизме наложения слоёв, который перехватывает точки входа API.

Единый уровень проверки Khronos

Ранее Vulkan предоставлял несколько уровней валидации, которые требовалось включать в определённом порядке. Начиная с версии Vulkan SDK 1.1.106.0, вашему приложению достаточно включить только один уровень валидацииVK_LAYER_KHRONOS_validation , чтобы получить доступ ко всем функциям предыдущих уровней валидации.

Используйте слои проверки, упакованные в ваш APK

Упаковка слоёв валидации в APK-файл обеспечивает оптимальную совместимость. Слои валидации доступны в виде готовых двоичных файлов или могут быть собраны из исходного кода.

Использовать готовые двоичные файлы

Загрузите последние двоичные файлы уровня проверки Android Vulkan со страницы релиза GitHub .

Самый простой способ добавить слои в ваш APK — извлечь готовые двоичные файлы слоев в каталог src/main/jniLibs/ вашего модуля, не затрагивая каталоги ABI (например, arm64-v8a или x86-64 ), например:

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

Создайте слой проверки из исходного кода

Для отладки исходного кода уровня проверки извлеките последнюю версию исходного кода из репозитория Khronos Group GitHub и следуйте приведенным там инструкциям по сборке.

Убедитесь, что слой проверки упакован правильно.

Независимо от того, выполняете ли вы сборку с использованием готовых слоев Khronos или слоев, созданных из исходного кода, процесс сборки создает в вашем APK окончательную структуру файла, подобную следующей:

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

Следующая команда показывает, как проверить, что ваш APK содержит слой проверки, как и ожидалось:

$ 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

Включить уровень проверки во время создания экземпляра

API Vulkan позволяет приложению включать слои при создании экземпляра. Точки входа, перехватываемые слоем, должны иметь один из следующих объектов в качестве первого параметра:

  • VkInstance
  • VkPhysicalDevice
  • VkDevice
  • VkCommandBuffer
  • VkQueue

Вызовите vkEnumerateInstanceLayerProperties() чтобы получить список доступных слоёв и их свойств. Vulkan включает слои при выполнении vkCreateInstance() .

В следующем фрагменте кода показано, как приложение может использовать API Vulkan для программного запроса и включения слоев:

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

Вывод logcat по умолчанию

Уровень валидации выводит предупреждения и сообщения об ошибках в LogCat, помеченные тегом VALIDATION . Сообщение уровня валидации выглядит следующим образом (с добавлением переносов строк для удобства прокрутки):

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)

Включить отладочный обратный вызов

Расширение Debug Utils VK_EXT_debug_utils позволяет вашему приложению создать отладочный мессенджер, который передаёт сообщения уровня валидации обратному вызову, предоставляемому приложением. Это расширение может быть не реализовано на вашем устройстве, но оно реализовано в последних уровнях валидации. Существует также устаревшее расширение VK_EXT_debug_report , которое предоставляет аналогичные возможности, если VK_EXT_debug_utils недоступно.

Перед использованием расширения Debug Utils убедитесь, что ваше устройство или загруженный уровень проверки поддерживают его. В следующем примере показано, как проверить, поддерживается ли расширение Debug Utils, и зарегистрировать обратный вызов, если расширение поддерживается устройством или уровнем проверки.

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

После того как ваше приложение зарегистрируется и включит обратный вызов, система направит ему отладочные сообщения.

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

Используйте внешние уровни проверки

Вам не обязательно упаковывать слои валидации в APK; устройства под управлением Android 9 (уровень API 28) и выше могут использовать внешние слои валидации по отношению к вашему исполняемому файлу и динамически включать и выключать их. Следуйте инструкциям в этом разделе, чтобы добавить слои валидации на тестовое устройство:

Разрешите вашему приложению использовать внешние уровни проверки

Модель безопасности и политики Android существенно отличаются от других платформ. Для загрузки внешних уровней проверки должно быть выполнено одно из следующих условий:

  • Целевое приложение можно отлаживать . Этот параметр предоставляет больше отладочной информации, но может негативно повлиять на производительность вашего приложения.

  • Целевое приложение запускается на отладочной сборке операционной системы, которая предоставляет права root.

  • Только приложения, предназначенные для Android 11 (уровень API 30) или выше: ваш целевой файл манифеста Android включает следующий элемент meta-data :

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

Загрузить внешний слой проверки

Устройства под управлением Android 9 (API уровня 28) и выше позволяют Vulkan загружать слой валидации из локального хранилища вашего приложения . Начиная с Android 10 (API уровня 29), Vulkan также может загружать слой валидации из отдельного APK-файла . Вы можете выбрать любой удобный для вас метод, если ваша версия Android его поддерживает.

Загрузите двоичный файл проверочного слоя из локального хранилища вашего устройства.

Поскольку Vulkan ищет двоичный файл во временном каталоге хранения данных вашего устройства, сначала необходимо поместить двоичный файл в этот каталог с помощью Android Debug Bridge (adb) , как показано ниже:

  1. Используйте команду adb push для загрузки двоичного файла слоя в хранилище данных вашего приложения на устройстве:

    $ adb push libVkLayer_khronos_validation.so /data/local/tmp
    
  2. Используйте adb shell и команды run-as для загрузки слоя через процесс вашего приложения. Таким образом, исполняемый файл будет иметь тот же доступ к устройству, что и приложение, но без необходимости использования root-доступа.

    $ 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. Включить слой .

Загрузить двоичный файл слоя проверки из другого APK

Вы можете использовать adb для установки APK , содержащего слой, а затем включить слой .

adb install --abi abi path_to_apk

Включить слои вне приложения

Вы можете включить слои Vulkan как для отдельного приложения, так и глобально. Настройки отдельного приложения сохраняются после перезагрузки, а глобальные настройки сбрасываются при перезагрузке.

Включить слои для каждого приложения отдельно

Следующие шаги описывают, как включить слои для каждого приложения:

  1. Используйте настройки оболочки adb для включения слоев:

    $ adb shell settings put global enable_gpu_debug_layers 1
    
  2. Укажите целевое приложение, для которого необходимо включить слои:

    $ adb shell settings put global gpu_debug_app <package_name>
    
  3. Укажите список включаемых слоев (сверху вниз), разделяя каждый слой двоеточием:

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

    Поскольку у нас есть один уровень проверки Khronos, команда, скорее всего, будет выглядеть так:

    $ adb shell settings put global gpu_debug_layers VK_LAYER_KHRONOS_validation
    
  4. Укажите один или несколько пакетов для поиска слоев внутри:

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

Проверить, включены ли настройки, можно с помощью следующих команд:

$ 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

Поскольку применяемые вами настройки сохраняются при перезагрузке устройства, вам может потребоваться очистить настройки после загрузки слоев:

$ 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

Включить слои глобально

Вы можете включить один или несколько слоёв глобально до следующей перезагрузки. При этом будет предпринята попытка загрузить слои для всех приложений, включая собственные исполняемые файлы.

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