Android の Vulkan 検証レイヤ

パフォーマンスが低下する可能性があるため、明示的なグラフィック API の多くは、エラーチェックを実行しません。 Vulkan は、開発時にエラーチェックを実行できるようにしながらも、エラーチェック機能をアプリのリリースビルドからは除外します。このため、最も重要なときにパフォーマンスが低下するのを回避できます。 このためには、検証レイヤを有効にします。検証レイヤは、さまざまなデバッグと検証を目的として、Vulkan エントリ ポイントをインターセプトまたはフックします。

各検証レイヤは、これらの 1 つ以上のエントリ ポイントの定義を含むことができます。そして、含んでいる定義に対応するエントリ ポイントをインターセプトします。 検証レイヤがエントリ ポイントを定義しない場合、システムは次のレイヤにエントリ ポイントを渡します。 最終的には、どのレイヤでも定義されないエントリ ポイントが、ベースレベル未検証のドライバに到達します。

Android SDK、NDK、および Vulkan サンプルには、開発中に使用する Vulkan 検証レイヤが含まれます。 これらの検証レイヤをグラフィック スタックにフックすれば、レイヤが検証の問題を報告できるようになります。 このインストルメンテーションにより、開発中の誤用を検出して修正することが可能になります。

このページでは、以下の方法について説明します。

  • NDK のレイヤのバイナリを統合する。
  • 検証レイヤのソースコードを取得する。
  • レイヤのビルドを検証する。
  • Vulkan アプリケーションのレイヤを有効にする。

プロジェクトに検証レイヤを追加する

NDK バージョン 12 以降には、検証レイヤのバイナリが組み込まれています。インスタンスと端末の作成時、アプリから要求されると、Vulkan ローダーは APK のインストール場所から検証レイヤのバイナリを見つけて読み込みます。

組み込みの検証レイヤのバイナリを使用するには、プロジェクトの Gradle ビルド構成を変更するか、プロジェクトの JNI ライブラリ ディレクトリにバイナリを手動で追加します。

Gradle で検証レイヤを追加する

プロジェクトに検証レイヤを追加するには、Android Studio の CMake と Ndk-build のサポートを使用します。または、Android Studio における、Gradle の試験的なプラグインを使用します。 普通は CMake と Ndk-build 構成を使用します。

Android Studio の CMake と Ndk-build のサポートを使用してライブラリを追加するには、プロジェクトの Gradle 構成に以下を追加します。

sourceSets {
  main {
    jniLibs {
      srcDir "${your-ndk-dir}/sources/third_party/vulkan/src/build-android/jniLibs"
    }
  }
}

Android Studio における、Gradle の試験的なプラグインを使用してライブラリを追加するには、プロジェクトの Gradle 構成に以下を追加します。

sources {
  main {
    jniLibs {
      source.srcDir "${your-ndk-dir}/sources/third_party/vulkan/src/build-android/jniLibs"
    }
  }
}

JNI ライブラリに検証レイヤを追加する

プロジェクトの Gradle ビルド ファイルの構成が機能しない場合、以下のコマンドライン オプションを使用してプロジェクトの JNI ライブラリ ディレクトリに検証レイヤのバイナリを手動で追加します。

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

レイヤのソースを取得する

アプリで最新の検証レイヤが必要な場合は、Khronosグループの GitHub レポジトリから最新のソースを取得して、ビルドの手順に従います。

レイヤのビルドを検証する

NDK の組み込みレイヤでビルドする場合も、最新のソースコードからビルドする場合も、ビルドプロセスは以下のような最終的なファイル構造を生成します。

src/main/jniLibs/
  arm64-v8a/
    libVkLayer_core_validation.so
    libVkLayer_device_limits.so
    libVkLayer_image.so
    libVkLayer_object_tracker.so
    libVkLayer_parameter_validation.so
    libVkLayer_swapchain.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_swapchain.so
 inflated: lib/arm64-v8a/libVkLayer_unique_objects.so
 inflated: lib/arm64-v8a/libVkLayer_parameter_validation.so
 inflated: lib/arm64-v8a/libVkLayer_image.so
 inflated: lib/arm64-v8a/libVkLayer_core_validation.so
 inflated: lib/arm64-v8a/libVkLayer_device_limits.so
 ...

レイヤを有効にする

Vulkan API は、アプリがインスタンス レイヤとデバイスレイヤの両方を有効にできるようにします。

インスタンス レイヤ

Vulkan のインスタンスレベルのエントリ ポイントをインターセプトできるレイヤを、インスタンス レイヤといいます。インスタンスレベルのエントリ ポイントとは、VkInstanceVkPhysicalDevice を最初のパラメーターとして持っているものです。

vkEnumerateInstanceLayerProperties() を呼び出すと、使用可能なインスタンス レイヤとそのプロパティを列挙することができます。 システムは、vkCreateInstace() の実行時にインスタンス レイヤを有効にします。

以下のコード スニペットでは、アプリが Vulkan API を使用して、インスタンス レイヤをプログラムで有効にしてクエリする方法を示します。

// Get instance layer count using null pointer as last parameter
uint32_t instance_layer_present_count = 0;
vkEnumerateInstanceLayerProperties(&instance_layer_present_count, nullptr);

// Enumerate instance 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 instance 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_LUNARG_device_limits",
    "VK_LAYER_LUNARG_image",
    "VK_LAYER_LUNARG_swapchain",
    "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 instance 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;
...

デバイスレイヤ

端末レベルのエントリポイントをインターセプトできるレイヤは、デバイスレイヤといいます。端末レベルのエントリポイントとは、VkDeviceVkCommandBuffer、または VkQueue が最初のパラメーターになっているものです。 有効にするデバイスレイヤの一覧は、アプリが vkCreateDevice() に渡す VkDeviceCreateInfo 構造体の ppEnabledLayerNames フィールドに含まれています。

vkEnumerateDeviceLayerProperties を呼び出すと、使用可能なレイヤとそのプロパティを列挙することができます。 システムは、vkCreateDevice() の呼び出し時にデバイスレイヤを有効にします。

以下のコード スニペットでは、アプリが Vulkan API を使用してデバイスレイヤをプログラムで有効にする方法を示します。


// Get device layer count using null as last parameter
uint32_t device_layer_present_count = 0;
vkEnumerateDeviceLayerProperties(&device_layer_present_count, nullptr);

// Enumerate device layers with valid pointer in last parameter
VkLayerProperties* layer_props =
   (VkLayerProperties *)malloc(device_layer_present_count * sizeof(VkLayerProperties));
vkEnumerateDeviceLayerProperties(physical_device, device_layer_present_count, layer_props));

// Make sure the desired device validation layers are available
// Ensure threading is first and unique_objects is last!
const char *device_layers[] = {
    "VK_LAYER_GOOGLE_threading",
    "VK_LAYER_LUNARG_parameter_validation",
    "VK_LAYER_LUNARG_object_tracker",
    "VK_LAYER_LUNARG_core_validation",
    "VK_LAYER_LUNARG_device_limits",
    "VK_LAYER_LUNARG_image",
    "VK_LAYER_LUNARG_swapchain",
    "VK_LAYER_GOOGLE_unique_objects"
};

uint32_t device_layer_request_count =
   sizeof(device_layers) / sizeof(device_layers[0]);
for (uint32_t i = 0; i < device_layer_request_count; i++) {
    bool found = false;
    for (uint32_t j = 0; j < device_layer_present_count; j++) {
        if (strcmp(device_layers[i],
           layer_props[j].layerName) == 0) {
            found = true;
        }
    }
    if (!found) {
        error();
    }
}

// Pass desired device layers into vkCreateDevice
VkDeviceCreateInfo device_info = {};
device_info.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
device_info.enabledLayerCount = device_layer_request_count;
device_info.ppEnabledLayerNames = device_layers;
...

デバッグのコールバックを有効にする

Debug Report 拡張機能 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;
}