Android の Vulkan 検証レイヤ

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

検証レイヤは、そこに含まれる定義に対応するエントリ ポイントをインターセプトします。レイヤに定義されていないエントリ ポイントは、検証されないままベースレベル、つまりドライバに到達します。

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

単一の Khronos 検証レイヤ

Vulkan レイヤはローダーによってスタックに挿入され、レイヤスタックで上位のレイヤが下位のレイヤを呼び出し、最終的にデバイス ドライバで終了します。以前は、Android で特定の順序で複数の検証レイヤが有効になっていましたが、現在は、レイヤ VK_LAYER_KHRONOS_validation 1 つで、前述の検証レイヤの動作をすべて含むようになりました。Vulkan 検証では、すべてのアプリで単一の検証レイヤ VK_LAYER_KHRONOS_validation を有効にする必要があります。

検証レイヤをパッケージ化する

NDK には検証レイヤのビルド済みバイナリが組み込まれており、これを APK にパッケージ化することにより、テストデバイスにプッシュできます。このバイナリはディレクトリ ndk-dir/sources/third_party/vulkan/src/build-android/jniLibs/abi/ にあります。アプリがリクエストすると、Vulkan ローダーはアプリの APK からレイヤを見つけて読み込みます。

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/
    

ソースからレイヤバイナリをビルドする

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

レイヤのビルドを確認する

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

    src/main/jniLibs/
      arm64-v8a/
        libVkLayer_khronos_validation.so
      armeabi-v7a/
        libVkLayer_khronos_validation.so
    

次の例は、APK に想定どおりに検証レイヤが含まれているかどうかを検証する方法を示します。

    $ jar -xvf project.apk
     ...
     inflated: lib/arm64-v8a/libVkLayer_khronos_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 layer is available
    const char *instance_layers[] = {
        "VK_LAYER_KHRONOS_validation"
    };

    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 layer 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 Utils 拡張機能 VK_EXT_debug_utils により、アプリが提供するコールバックに検証レイヤのメッセージを渡すデバッグ メッセンジャーをアプリで作成できます。また、サポートを終了する拡張機能 VK_EXT_debug_report もあります。これは、VK_EXT_debug_utils が使用できない場合に同様の機能を提供します。

使用する前に、Debug Utils 拡張機能がプラットフォームでサポートされているかどうかを確認する必要があります。次の例は、デバッグ拡張機能がサポートされているかどうかを確認し、サポートされている場合はコールバックを登録する方法を示します。

    // 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 utils extension is available
    for (uint32_t i = 0; i < inst_ext_count; i++) {
        if (strcmp(inst_exts[i].extensionName,
        VK_EXT_DEBUG_UTILS_EXTENSION_NAME) == 0) {
            enabled_inst_exts[enabled_inst_ext_count++] =
                VK_EXT_DEBUG_UTILS_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_vkCreateDebugUtilsMessengerEXT pfnCreateDebugUtilsMessengerEXT;
    PFN_vkDestroyDebugUtilsMessengerEXT pfnDestroyDebugUtilsMessengerEXT;

    pfnCreateDebugUtilsMessengerEXT = (PFN_vkCreateDebugUtilsMessengerEXT)
         vkGetDeviceProcAddr(device, "vkCreateDebugUtilsMessengerEXT");
    pfnDestroyDebugUtilsMessengerEXT = (PFN_vkDestroyDebugUtilsMessengerEXT)
         vkGetDeviceProcAddr(device, "vkDestroyDebugUtilsMessengerEXT");

    assert(pfnCreateDebugUtilsMessengerEXT);
    assert(pfnDestroyDebugUtilsMessengerEXT);

    // Create the debug messenger callback with desired settings
    VkDebugUtilsMessengerEXT debugUtilsMessenger;
    if (pfnCreateDebugUtilsMessengerEXT) {
        VkDebugUtilsMessengerCreateInfoEXT messengerInfo;
        constexpr VkDebugUtilsMessageSeverityFlagsEXT kSeveritiesToLog =
                VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT |
                VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT;

        constexpr VkDebugUtilsMessageTypeFlagsEXT kMessagesToLog =
                VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT |
                VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT |
                VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;

        messengerInfo.sType           = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
        messengerInfo.pNext           = nullptr;
        messengerInfo.flags           = 0;
        messengerInfo.messageSeverity = kSeveritiesToLog;
        messengerInfo.messageType     = kMessagesToLog;
        messengerInfo.pfnUserCallback = &DebugUtilsMessenger; // Callback example below
        messengerInfo.pUserData       = nullptr; // Custom user data passed to callback

        pfnCreateDebugUtilsMessengerEXT(instance, &messengerInfo, nullptr, &debugUtilsMessenger);
    }

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

    

アプリがデバッグ コールバックを登録して有効にすると、登録したコールバックにシステムがデバッグ メッセージを渡します。このようなコールバックの例を下記に示します。

    #include <android/log.h>

    VKAPI_ATTR VkBool32 VKAPI_CALL DebugUtilsMessenger(
                            VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,
                            VkDebugUtilsMessageTypeFlagsEXT messageTypes,
                            const VkDebugUtilsMessengerCallbackDataEXT *callbackData,
                            void *userData)
    {
       const char validation[]  = "Validation";
       const char performance[] = "Performance";
       const char error[]       = "ERROR";
       const char warning[]     = "WARNING";
       const char unknownType[] = "UNKNOWN_TYPE";
       const char unknownSeverity[] = "UNKNOWN_SEVERITY";
       const char* typeString      = unknownType;
       const char* severityString  = unknownSeverity;
       const char* messageIdName   = callbackData->pMessageIdName;
       int32_t messageIdNumber     = callbackData->messageIdNumber;
       const char* message         = callbackData->pMessage;
       android_LogPriority priority = ANDROID_LOG_UNKNOWN;

       if (messageSeverity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) {
           severityString = error;
           priority = ANDROID_LOG_ERROR;
       }
       else if (messageSeverity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) {
           severityString = warning;
           priority = ANDROID_LOG_WARN;
       }
       if (messageTypes & VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT) {
           typeString = validation;
       }
       else if (messageTypes & VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT) {
           typeString = performance;
       }

       __android_log_print(priority,
                           "AppName",
                           "%s %s: [%s] Code %i : %s",
                           typeString,
                           severityString,
                           messageIdName,
                           messageIdNumber,
                           message);

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

ADB を使用してレイヤをテストデバイスにプッシュする

Android 9(API レベル 28)以上を搭載するデバイスでは、Vulkan はアプリのローカル ストレージからレイヤを読み込むことができます。Android 10(API レベル 29)では、別の APK からレイヤを読み込むことができます。

デバイスのローカル ストレージにあるレイヤバイナリ

Vulkan がデバイスの一時データ ストレージ ディレクトリでバイナリを検索するため、まず以下のように Android Debug Bridge(ADB)を使用してそのディレクトリにバイナリをプッシュしておく必要があります。

  1. adb push コマンドを使用して、必要なレイヤバイナリをデバイス上のアプリのデータ ストレージに読み込みます。次の例では、libVkLayer_khronos_validation.so をデバイスの /data/local/tmp ディレクトリにプッシュします。
        $ adb push libVkLayer_khronos_validation.so /data/local/tmp
        
  2. adb shell コマンドと run-as コマンドを使用して、アプリプロセスを介してレイヤを読み込みます。これにより、バイナリはルートアクセス権を必要とせずに、アプリと同じデバイス アクセス権を持つことになります。
        $ adb shell run-as com.example.myapp cp /data/local/tmp/libVkLayer_khronos_validation.so .
        $ adb shell run-as com.example.myapp ls libVkLayer_khronos_validation.so
        
  3. レイヤを有効にします

レイヤを含む APK

adb を使って、APK をインストールしてから、レイヤを有効にします

    adb install --abi abi path_to_apk
    

アプリの外部のレイヤを有効にする

レイヤは、アプリレベルかグローバル レベルで有効にできます。アプリレベルの設定は再起動後も保持されますが、グローバル プロパティは再起動時にクリアされます。

アプリレベルでレイヤを有効にするには:

    # Enable layers
    adb shell settings put global enable_gpu_debug_layers 1

    # Specify target application
    adb shell settings put global gpu_debug_app <package_name>

    # Specify layer list (from top to bottom)
    adb shell settings put global gpu_debug_layers <layer1:layer2:layerN>

    # Specify packages to search for layers
    adb shell settings put global gpu_debug_layer_app <package1:package2:packageN>
    

設定が有効になっているかどうかを確認するには、次のコマンドを使用します。

    $ adb shell settings list global | grep gpu
    enable_gpu_debug_layers=1
    gpu_debug_app=com.example.myapp
    gpu_debug_layers=VK_LAYER_KHRONOS_validation
    

適用した設定はデバイスの再起動後も保持されるため、レイヤを読み込んだ後はこの設定を削除することをおすすめします。

    $ 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
    $ adb shell settings delete global gpu_debug_layer_app
    

アプリレベルでレイヤを無効にするには:

    # Delete the global setting that enables layers
    adb shell settings delete global enable_gpu_debug_layers

    # Delete the global setting that selects target application
    adb shell settings delete global gpu_debug_app

    # Delete the global setting that specifies layer list
    adb shell settings delete global gpu_debug_layers

    # Delete the global setting that specifies layer packages
    adb shell settings delete global gpu_debug_layer_app
    

グローバル レベルでレイヤを有効にするには:

    # This attempts to load layers for all applications, including native
    # executables
    adb shell setprop debug.vulkan.layers <layer1:layer2:layerN>