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 的支持,将验证层添加到您的项目。

要利用 Andorid 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
    

    提示:您还能通过设备上开发者选项启用这些设置。 启用开发者选项后,打开您测试设备上的 Settings 应用,导航到 Developer options > Debugging,并确保启用 Enable GPU debug layers 选项。

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