Android の Vulkan 検証レイヤ

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

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

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 Plugin と 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 コマンドを使用して、アプリのプロセスを介してレイヤを読み込みます。 つまり、バイナリはルートアクセスなしで、アプリと同じ端末へのアクセス権を取得します。

    $ 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
    

    ヒント: これらの設定は、端末開発者向けオプションからも有効にできます。 開発者向けオプションを有効にしたら、テスト端末で Settings アプリを開き、[Developer options] > [Debugging] に移動して、[Enable GPU debug layers] オプションがオンになっていることを確認します。

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