Android の Vulkan 検証レイヤ

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

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

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

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

  • 検証レイヤをテストデバイスに読み込む
  • 検証レイヤのソースコードを取得する
  • レイヤのビルドを確認する
  • Vulkan アプリでレイヤを有効にする

検証レイヤをテストデバイスに読み込む

NDK には検証レイヤのビルド済みバイナリが組み込まれており、これを APK にパッケージ化する、または Android Debug Bridge(ADB)を使用して読み込むことにより、テストデバイスにプッシュできます。バイナリは次のディレクトリにあります。 ndk-dir/sources/third_party/vulkan/src/build-android/jniLibs/abi/

アプリから要求されると、Vulkan ローダはアプリの APK またはローカルのデータ ディレクトリからレイヤを見つけて読み込みます。このセクションでは、レイヤのバイナリをテストデバイスにプッシュする方法をいくつか説明します。Vulkan ローダはデバイス上の複数のソースからレイヤのバイナリを検出できますが、下記に説明されている方法のいずれか 1 つのみを使用する必要があります。

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 Debug Bridge(ADB)を使用してバイナリをそのディレクトリにプッシュする必要があります。

  1. adb push コマンドを使用して、必要なレイヤバイナリをデバイス上のアプリのデータ ストレージに読み込みます。次の例では、libVkLayer_unique_objects.so をデバイスの /data/local/tmp ディレクトリにプッシュします。
        $ adb push libVkLayer_unique_objects.so /data/local/tmp
        
  2. adb shell コマンドと run-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 グループの 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;
    ...
    

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

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