Android 上的 Vulkan 验证层

大多数显式图形 API 都不会执行错误检查,因为执行错误检查会降低性能。Vulkan 提供可让您在开发时使用的错误检查功能,但该功能会从您应用的发布 build 中排除,这样可以避免在关键时刻性能出现下降。您可以通过启用 Vulkan 验证层来执行此操作。验证层会出于各种调试和验证目的截获或挂接 Vulkan 入口点。

验证层会截获其包含定义的入口点。未在层中定义的入口点都会到达基础级别的驱动程序,并保持未验证状态。

Android NDK 和 Vulkan 示例包括 Vulkan 验证层(可在开发期间使用)。您可以将验证层挂接到图形堆栈中,从而允许其报告验证问题。借助此插桩测试,您可以捕捉和修复开发期间出现的误用问题。

单个 Khronos 验证层

Vulkan 层可以通过加载程序插入到堆栈中,以便高级层调用底下的层,层堆栈最终终止于设备驱动程序。过去,在 Android 上按特定顺序启用了多个验证层。但是,现在使用单个层 VK_LAYER_KHRONOS_validation,它包含了之前所有的验证层行为。对于 Vulkan 验证,所有应用都应启用单个验证层 VK_LAYER_KHRONOS_validation

封装验证层

NDK 包含预构建的验证层二进制文件,您可以将这类文件推送到测试设备,只需将其封装到 APK 中即可。您可在以下目录中找到此二进制文件: ndk-dir/sources/third_party/vulkan/src/build-android/jniLibs/abi/ 收到应用的请求后,Vulkan 加载程序将从您应用的 APK 寻找并加载相应的层。

使用 Gradle 将验证层封装到您的 APK 中

您可以利用 Android Gradle 插件以及 Android Studio 对 CMake 和 ndk-build 的支持,将验证层添加到您的项目。如需使用 Android Studio 对 CMake 和 ndk-build 的支持来添加库,请将以下内容添加到应用模块的 build.gradle 文件中:
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"
    }
  }
}
如需详细了解 Android Studio 对 CMake 和 ndk-build 的支持,请参阅向您的项目添加 C 和 C++ 代码

将验证层封装到 JNI 库中

您可以使用以下命令行选项,将验证层二进制文件手动添加到项目的 JNI 库目录中:
$ cd project-root
$ mkdir -p app/src/main
$ cp -fr ndk-path/sources/third_party/vulkan/src/build-android/jniLibs app/src/main/

从源代码构建层二进制文件

如果您的应用需要最新的验证层,您可以从 Khronos Group 的 GitHub 代码库中获取最新的源代码,并按照其中的构建说明操作。

验证层构建

无论您是使用 NDK 的预构建层进行构建,还是从最新的源代码进行构建,构建流程都会生成如下所示的最终文件结构:

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

下面的示例显示了如何验证您的 APK 是否包含预期的验证层:

$ jar -xvf project.apk
 ...
 inflated: lib/arm64-v8a/libVkLayer_khronos_validation.so
 ...

启用层

Vulkan API 可让应用启用层。层是在实例创建过程中启用的。层截获的入口点必须将下列对象之一作为第一个参数:

  • VkInstance
  • VkPhysicalDevice
  • VkDevice
  • VkCommandBuffer
  • VkQueue

您可以调用 vkEnumerateInstanceLayerProperties() 来列出可用层及其属性。系统会在 vkCreateInstance() 执行时启用层。

以下代码段显示了应用如何使用 Vulkan API 以程序化方式启用和查询层:

// 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;
...

默认 logcat 输出

验证层会在带有 VALIDATION 标记的 logcat 中发出警告和错误消息。验证层消息如下所示:
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

启用调试回调

调试实用工具扩展程序 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 =
    (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);
}

在您的应用注册并启用调试回调后,系统会将调试消息路由到您注册的回调。这类回调的一个示例显示如下:

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

使用 ADB 将层推送到您的测试设备

按照本部分中的步骤将层推送到测试设备:

启用调试功能

Android 的安全模型和政策与其他平台有很大不同。若要加载外部层,必须满足以下条件之一:

  • 目标应用的清单文件包含以下元数据元素(仅适用于以 Android 11(API 级别“R”)或更高版本为目标的应用):
    <meta-data android:name="com.android.graphics.injectLayers.enable" android:value="true" /> 您应使用此选项对应用进行性能剖析。
  • 目标应用是可调试的。此选项可为您提供更多调试信息,但可能会降低应用性能。
  • 目标应用在授予 root 访问权限的操作系统的 userdebug build 上运行。

加载层

搭载 Android 9(API 级别 28)和更高版本的设备允许 Vulkan 从应用的本地存储空间加载层。Android 10(API 级别 29)支持从单独的 APK 加载层。

设备的本地存储空间中的层二进制文件

Vulkan 会在设备的临时数据存储目录中寻找二进制文件,因此,您必须首先使用 Android 调试桥 (ADB) 将二进制文件推送到该目录,方法如下:

  1. 使用 adb push 命令将所需层二进制文件加载到您的应用在设备上的数据存储空间。以下示例将 libVkLayer_khronos_validation.so 推送到设备的 /data/local/tmp 目录:
    $ 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

在应用外启用层

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

按应用启用层:

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

如需查看设置是否已启用,您可以使用以下命令:

$ 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

按应用停用层:

# 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

全局启用层:

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