Android 上的 Vulkan 验证层

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

每个验证层都可以包含这些入口点中一个或多个的定义,并截获其包含相应定义的入口点。如果验证层未定义入口点,系统便会将入口点传递到下一层。最终,未在任何层中定义的入口点都会到达基础级别的驱动程序,并保持未验证状态。

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

本页将介绍如何执行以下操作:

  • 将验证层加载到您的测试设备
  • 获取验证层的源代码
  • 验证层编译
  • 在 Vulkan 应用中启用层

将验证层加载到您的测试设备

NDK 包含预编译的验证层二进制文件,您可以将这类文件推送到测试设备,只需将其封装到 APK 中或使用 Android 调试桥 (ABD) 加载文件即可。您可在以下目录中找到这些二进制文件:ndk-dir/sources/third_party/vulkan/src/build-android/jniLibs/abi/

收到应用的请求后,Vulkan 加载程序将从您应用的 APK 或本地数据目录中寻找并加载相应的层。本部分将介绍多种可让您将层二进制文件推送到测试设备的方法。虽然 Vulkan 加载程序可从设备上的多个源中找到层二进制文件,但是您只能选择使用下述方法中的一种。

使用 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/
    

使用 ADB 将层二进制文件推送到您的测试设备

运行 Android 9(API 级别 28)或更高版本的设备允许 Vulkan 从设备本地存储空间中加载层二进制文件。也就是说,从设备加载二进制文件时,您已不再需要将这些二进制文件与您应用的 APK 绑定在一起。不过,已安装的应用必须可供调试。Vulkan 会在设备的临时数据存储目录中寻找二进制文件,因此,您必须首先使用 Android 调试桥 (ADB) 将二进制文件推送到该目录,方法如下:

  1. 使用 adb push 命令将所需层二进制文件加载到您的应用在设备上的数据存储空间。以下示例会将 libVkLayer_unique_objects.so 推送到设备的 /data/local/tmp 目录:
        $ adb push libVkLayer_unique_objects.so /data/local/tmp
        
  2. 使用 adb shellrun-as 命令,通过应用进程加载层。也就是说,二进制文件拥有该应用所具有的设备访问权限,无需请求 root 权限。
        $ adb shell run-as com.example.myapp cp /data/local/tmp/libVkLayer_unique_objects.so
        $ adb shell run-as com.example.myapp ls libVkLayer_unique_objects.so
        
  3. 使用 adb shell settings 命令让 Vulkan 从设备存储空间加载层:
        $ adb shell settings put global enable_gpu_debug_layers 1
        $ adb shell settings put global gpu_debug_app com.example.myapp
        $ adb shell settings put global gpu_debug_layers VK_LAYER_GOOGLE_unique_objects
        

    提示:您还能通过设备上的开发者选项启用这些设置。启用开发者选项后,请打开测试设备上的设置应用,然后依次转到开发者选项 > 调试,并请务必开启启用 GPU 调试层选项。

  4. 要查看第 3 步中的设置是否已启用,您可以使用以下命令来查看:
        $ adb shell settings list global | grep gpu
        enable_gpu_debug_layers=1
        gpu_debug_app=com.example.myapp
        gpu_debug_layers=VK_LAYER_GOOGLE_unique_objects
        
  5. 由于第 3 步中应用的设置不会随设备重新启动而重置,因此建议您在验证层加载完毕后清除设置:
        $ 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
        

从源代码中编译层二进制文件

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

验证层编译

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

    src/main/jniLibs/
      arm64-v8a/
        libVkLayer_core_validation.so
        libVkLayer_object_tracker.so
        libVkLayer_parameter_validation.so
        libVkLayer_threading.so
        libVkLayer_unique_objects.so
      armeabi-v7a/
        libVkLayer_core_validation.so
        ...
    

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

    $ jar -xvf project.apk
     ...
     inflated: lib/arm64-v8a/libVkLayer_threading.so
     inflated: lib/arm64-v8a/libVkLayer_object_tracker.so
     inflated: lib/arm64-v8a/libVkLayer_unique_objects.so
     inflated: lib/arm64-v8a/libVkLayer_parameter_validation.so
     inflated: lib/arm64-v8a/libVkLayer_core_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 layers are available
    // NOTE:  These are not listed in an arbitrary order.  Threading must be
    //        first, and unique_objects must be last.  This is the order they
    //        will be inserted by the loader.
    const char *instance_layers[] = {
        "VK_LAYER_GOOGLE_threading",
        "VK_LAYER_LUNARG_parameter_validation",
        "VK_LAYER_LUNARG_object_tracker",
        "VK_LAYER_LUNARG_core_validation",
        "VK_LAYER_GOOGLE_unique_objects"
    };

    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 layers 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_report 可让您的应用在事件发生时控制层行为。

使用此扩展之前,您必须先确保平台可为其提供支持。下面的示例显示了如何检查是否支持调试扩展,以及在支持扩展时注册回调。

    // 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 report extension is available
    for (uint32_t i = 0; i < inst_ext_count; i++) {
        if (strcmp(inst_exts[i].extensionName,
        VK_EXT_DEBUG_REPORT_EXTENSION_NAME) == 0) {
            enabled_inst_exts[enabled_inst_ext_count++] =
                VK_EXT_DEBUG_REPORT_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_vkCreateDebugReportCallbackEXT vkCreateDebugReportCallbackEXT;
    PFN_vkDestroyDebugReportCallbackEXT vkDestroyDebugReportCallbackEXT;

    vkCreateDebugReportCallbackEXT = (PFN_vkCreateDebugReportCallbackEXT)
        vkGetInstanceProcAddr(instance, "vkCreateDebugReportCallbackEXT");
    vkDestroyDebugReportCallbackEXT = (PFN_vkDestroyDebugReportCallbackEXT)
        vkGetInstanceProcAddr(instance, "vkDestroyDebugReportCallbackEXT");

    assert(vkCreateDebugReportCallbackEXT);
    assert(vkDestroyDebugReportCallbackEXT);

    // Create the debug callback with desired settings
    VkDebugReportCallbackEXT debugReportCallback;
    if (vkCreateDebugReportCallbackEXT) {
        VkDebugReportCallbackCreateInfoEXT debugReportCallbackCreateInfo;
        debugReportCallbackCreateInfo.sType =
            VK_STRUCTURE_TYPE_DEBUG_REPORT_CREATE_INFO_EXT;
        debugReportCallbackCreateInfo.pNext = NULL;
        debugReportCallbackCreateInfo.flags = VK_DEBUG_REPORT_ERROR_BIT_EXT |
                                              VK_DEBUG_REPORT_WARNING_BIT_EXT |
                                              VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT;
        debugReportCallbackCreateInfo.pfnCallback = DebugReportCallback;
        debugReportCallbackCreateInfo.pUserData = NULL;

        vkCreateDebugReportCallbackEXT(instance, &debugReportCallbackCreateInfo,
                                       nullptr, &debugReportCallback);
    }

    // Later, when shutting down Vulkan, call the following
    if (vkDestroyDebugReportCallbackEXT) {
       vkDestroyDebugReportCallbackEXT(instance, debugReportCallback, nullptr);
    }

    

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

    #include <android/log.h>

    static VKAPI_ATTR VkBool32 VKAPI_CALL DebugReportCallback(
                                       VkDebugReportFlagsEXT msgFlags,
                                       VkDebugReportObjectTypeEXT objType,
                                       uint64_t srcObject, size_t location,
                                       int32_t msgCode, const char * pLayerPrefix,
                                       const char * pMsg, void * pUserData )
    {
       if (msgFlags & VK_DEBUG_REPORT_ERROR_BIT_EXT) {
           __android_log_print(ANDROID_LOG_ERROR,
                               "AppName",
                               "ERROR: [%s] Code %i : %s",
                               pLayerPrefix, msgCode, pMsg);
       } else if (msgFlags & VK_DEBUG_REPORT_WARNING_BIT_EXT) {
           __android_log_print(ANDROID_LOG_WARN,
                               "AppName",
                               "WARNING: [%s] Code %i : %s",
                               pLayerPrefix, msgCode, pMsg);
       } else if (msgFlags & VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT) {
           __android_log_print(ANDROID_LOG_WARN,
                               "AppName",
                               "PERFORMANCE WARNING: [%s] Code %i : %s",
                               pLayerPrefix, msgCode, pMsg);
       } else if (msgFlags & VK_DEBUG_REPORT_INFORMATION_BIT_EXT) {
           __android_log_print(ANDROID_LOG_INFO,
                               "AppName", "INFO: [%s] Code %i : %s",
                               pLayerPrefix, msgCode, pMsg);
       } else if (msgFlags & VK_DEBUG_REPORT_DEBUG_BIT_EXT) {
           __android_log_print(ANDROID_LOG_VERBOSE,
                               "AppName", "DEBUG: [%s] Code %i : %s",
                               pLayerPrefix, msgCode, pMsg);
       }

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