Android 上的 Vulkan 验证层

大多数显式图形 API 都不会执行错误检查,因为执行错误检查会降低性能。Vulkan 具有可以在开发期间提供错误检查功能的验证层,能够避免应用的发布 build 出现性能下降。验证层依赖于可以截获 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() 列出可用层及其属性。Vulkan 会在 vkCreateInstance() 执行时启用层。

以下代码段展示了应用如何使用 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 输出

验证层会在带有 VALIDATION 标记的 logcat 中发出警告和错误消息。验证层消息如下所示(此处添加了换行符以便于滚动):

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 允许应用创建调试 messenger,将验证层消息传递给应用提供的回调。您的设备可能未实现该扩展,但最新的验证层中都已实现。此外,还有一个已弃用的扩展 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 build 上运行。

  • 仅限以 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

在应用外启用层

您可以按应用启用层,也可全局启用层。针对应用的设置会在重启后保留,而全局属性则会在重启时被清除。

按应用启用层

以下步骤说明了如何按应用启用层:

  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>