Android 上的 Vulkan 验证层

大多数显式图形 API 都不会执行错误检查,因为执行错误检查会降低性能。Vulkan 提供可让您在开发时使用的错误检查功能,但该功能会从您应用的发布版本中排除,这样可以避免在关键时刻性能出现下降。您可以通过启用 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;
    ...
    

启用调试回调

调试实用工具扩展程序 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 访问权限的操作系统的用户调试版本上运行。

加载层

搭载 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>