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

デフォルトの logcat 出力

検証レイヤは、VALIDATION タグでラベル付けされた logcat に警告メッセージとエラー メッセージを出力します。検証レイヤのメッセージは次のようになります。
VALIDATION: UNASSIGNED-CoreValidation-DrawState-QueueForwardProgress(ERROR / SPEC):
            msgNum: 0 - VkQueue 0x7714c92dc0[] is waiting on VkSemaphore 0x192e[]
            that has no way to be signaled.
VALIDATION:     Objects: 1
VALIDATION:         [0] 0x192e, type: 5, name: NULL

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

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 のセキュリティ モデルとポリシーは他のプラットフォームと大きく異なります。外部レイヤを読み込むには、次のいずれかの条件を満たす必要があります。

  • ターゲット アプリのマニフェスト ファイルに次の meta-data 要素が含まれている(Android 11(API レベル「R」)以上をターゲットとするアプリのみ)。
    <meta-data android:name="com.android.graphics.injectLayers.enable" android:value="true" /> このオプションを使用してアプリをプロファイリングする必要があります。
  • ターゲット アプリがデバッグ可能である。このオプションを使用すると、より詳細なデバッグ情報を得られますが、アプリのパフォーマンスに悪影響を及ぼす可能性があります。
  • ルートアクセスを許可するオペレーティング システムの userdebug ビルドでターゲット アプリが実行されている。

レイヤを読み込む

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 コマンドを使用して、アプリのプロセスを介してレイヤを読み込みます。これにより、バイナリは root アクセス権を必要とせずに、アプリと同じデバイス アクセス権を持つことになります。
    $ 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>