Vulkan validation layers on Android

Most explicit graphics APIs do not perform error-checking, because doing so can result in a performance penalty. Vulkan provides error-checking in a manner that lets you use this feature at development time, but exclude it from the release build of your app, thus avoiding the penalty when it matters most. You do this by enabling validation layers. Validation layers intercept or hook Vulkan entry points for various debug and validation purposes.

Each validation layer can contain definitions for one or more of these entry points, and intercepts the entry points for which it contains definitions. When a validation layer does not define an entry point, the system passes the entry point on to the next layer. Ultimately, an entry point not defined in any layer reaches the driver, the base level, unvalidated.

The Android SDK, NDK, and Vulkan samples include Vulkan validation layers for use during development. You can hook these validation layers into the graphics stack, allowing them to report validation issues. This instrumentation allows you to catch and fix misuses during development.

This page explains how to:

  • Load validation layers onto your test device
  • Get source code for validation layers
  • Verify layer build
  • Enable layers in the Vulkan application

Load validation layers onto your test device

The NDK includes pre-built validation layer binaries that you can push to your test device by either packaging them in your APK or loading them using the Android Debug Bridge (ADB). You can find these binaries in the following directory: ndk-dir/sources/third_party/vulkan/src/build-android/jniLibs/abi/

When requested by your app, the Vulkan loader finds and loads the layers from either your app’s APK or local data directory. This section explores multiple ways you can push the layer binaries to your test device. Although the Vulkan loader can discover layer binaries from multiple sources on the device, you need to use only one of the methods described below.

Package validation layers into your APK with Gradle

You can add the validation layer to your project using the Android Gradle Plugin and Android Studio's support for CMake and ndk-build.

To add the libraries using Android Studio's support for CMake and ndk-build, add the following to your app module's build.gradle file:

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

To learn more about Android Studio’s support for CMake and ndk-build, read Add C and C++ code to your project.

Package validation layers into JNI libraries

You can manually add the validation layer binaries to your project's JNI libraries directory by using the following command line options:

$ cd project-root
$ mkdir -p app/src/main
$ cp -fr ndk-path/sources/third_party/vulkan/src/build-android/jniLibs app/src/main/

Push layer binaries to your test device using ADB

Devices running Android 9 (API level 28) and higher allow Vulkan load layer binaries from your device’s local storage. That is, when loading the binaries from the device, you no longer need to bundle them with your app’s APK. However, the installed app must be debuggable. Vulkan looks for the binaries in your device’s temporary data storage directory, so you must first push the binaries to that directory using Android Debug Bridge (ADB), as follows:

  1. Use the adb push command to load the desired layer binaries into your app’s data storage on the device. The following example pushes libVkLayer_unique_objects.so to the device’s /data/local/tmp directory:
    $ adb push libVkLayer_unique_objects.so /data/local/tmp
    
  2. Use the adb shell and run-as commands to load the layers through your app process. That is, the binaries have the same device access that the app has without requiring root access.
    $ 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. Use the adb shell settings command to enable Vulkan to load the layers from device storage:
    $ 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
    

    Tip: You can also enable these settings through the on-device developer options. After you enable developer options, open the Settings app on your test device, navigate to Developer options > Debugging and make sure the option to Enable GPU debug layers is turned on.

  4. If want to check whether the settings from step 3 are enabled, you can do so using the following commands:
    $ 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. Because the settings you apply in step 3 persist across device reboots, you may want to clear the settings after the layers are loaded:
    $ 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
    

Build layer binaries from source

If your app needs the latest validation layer, you can pull the latest source from the Khronos Group GitHub repository and follow the build instructions there.

Verify layer build

Regardless of whether you build with NDK's prebuilt layers or you build from the latest source code, the build process produces final file structure like the following:

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

The following example shows how to verify that your APK contains the validation layers as expected:

$ 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
 ...

Enable layers

The Vulkan API allows an app to enable layers. Layers are enabled during instance creation. Entry points that a layer intercepts must have one of these objects as the first parameter:

  • VkInstance
  • VkPhysicalDevice
  • VkDevice
  • VkCommandBuffer
  • VkQueue

You can call vkEnumerateInstanceLayerProperties() to list the available layers and their properties. The system enables layers when vkCreateInstance() executes.

The following code snippet shows how an app can use the Vulkan API to programmatically enable and query a layer:

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

Enable the debug callback

The Debug Report extension VK_EXT_debug_report allows your application to control layer behavior when an event occurs.

Before using this extension, you must first make sure that the platform supports it. The following example shows how to check for debug extension support and register a callback if the extension is supported.

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

Once your app has registered and enabled the debug callback, the system routes debugging messages to a callback that you register. An example of such a callback appears below:

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