Android 上的 Vulkan 验证层

大多数显式图形 API 不会执行错误检查,因为这样做会导致性能下降。Vulkan 具有 *验证层*,可在开发过程中提供错误检查,从而避免在应用的发布版本中出现性能损失。验证层依赖于拦截 API 入口点的通用分层机制。

单个 Khronos 验证层

以前,Vulkan 提供了多个需要按特定顺序启用的验证层。从 1.1.106.0 Vulkan SDK 版本开始,您的应用只需启用一个 单个验证层VK_LAYER_KHRONOS_validation,即可获得以前验证层的所有功能。

使用 APK 中打包的验证层

将验证层打包到 APK 中可确保最佳兼容性。验证层可作为预构建二进制文件提供,也可从源代码构建。

使用预构建二进制文件

GitHub 发布页面下载最新的 Android Vulkan 验证层二进制文件。

将图层添加到 APK 的最简单方法是将预构建的图层二进制文件提取到模块的 src/main/jniLibs/ 目录中,并保持 ABI 目录(例如 arm64-v8ax86-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

在实例创建期间启用验证层

Vulkan API 允许应用在实例创建期间启用图层。图层拦截的入口点必须具有以下对象之一作为第一个参数

  • VkInstance
  • VkPhysicalDevice
  • VkDevice
  • VkCommandBuffer
  • VkQueue

调用 vkEnumerateInstanceLayerProperties() 列出可用的图层及其属性。当 vkCreateInstance() 执行时,Vulkan 会启用图层。

以下代码片段显示了应用如何使用 Vulkan API 以编程方式查询和启用图层

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

启用调试回调

调试实用程序扩展 VK_EXT_debug_utils 允许您的应用程序创建一个调试信使,该信使将验证层消息传递给应用程序提供的回调。您的设备可能未实现此扩展,但它在最新的验证层中已实现。还有一个名为 VK_EXT_debug_report 的已弃用扩展,如果 VK_EXT_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 访问权限的操作系统的userdebug版本上运行。

  • 面向 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 调试桥 (adb) 将二进制文件推送到该目录,如下所示

  1. 使用 adb push 命令将图层二进制文件加载到设备上的应用数据存储中

    $ adb push libVkLayer_khronos_validation.so /data/local/tmp
    
  2. 使用 adb shellrun-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 shell settings 启用图层

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