Android の Vulkan 検証レイヤ

パフォーマンスが低下する可能性があるため、明示的なグラフィック API のほとんどは、エラーチェックを行いません。Vulkan には、検証レイヤが用意されており、開発中にエラーチェックを行うことで、アプリのリリースビルドでのパフォーマンスの低下を防ぎます。検証レイヤは、API のエントリ ポイントをインターセプトする汎用のレイヤ化メカニズムを使用します。

単一の Khronos 検証レイヤ

これまで、Vulkan は複数の検証レイヤを提供しており、これらの検証レイヤは特定の順序で有効にする必要がありました。1.1.106.0 Vulkan SDK リリース以降では、アプリで単一の検証レイヤ VK_LAYER_KHRONOS_validation を有効にするだけで、旧バージョンの検証レイヤの機能をすべて取得できます。

APK にパッケージ化された検証レイヤを使用する

検証レイヤを APK 内にパッケージ化すると、最適な互換性を確保できます。検証レイヤは、ビルド済みのバイナリとして入手することも、ソースコードからビルドすることもできます。

ビルド済みのバイナリを使用する

注: GitHub リリースページから最新の Android Vulkan 検証レイヤのバイナリをダウンロードしてください。

APK にレイヤを追加する最も簡単な方法は、ビルド済みのレイヤバイナリをモジュールの src/main/jniLibs/ ディレクトリに抽出し、ABI ディレクトリ(arm64-v8ax86-64 など)変更せずにそのままにしておくことです。以下の例をご覧ください。

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

ソースコードから検証レイヤをビルドする

検証レイヤのソースコードに対してデバッグを行うには、Khronos グループの GitHub リポジトリから最新のソースを取得し、そこに記載のビルドの手順を行います。

検証レイヤが正しくパッケージ化されていることを確認する

Khronos のビルド済みのレイヤでビルドする場合も、ソースからビルドしたレイヤを使用する場合も、ビルドプロセスは以下に示すように APK の最終的なファイル構造を生成します。

lib/
  arm64-v8a/
    libVkLayer_khronos_validation.so
  armeabi-v7a/
    libVkLayer_khronos_validation.so
  x86/
    libVkLayer_khronos_validation.so
  x86-64/
    libVkLayer_khronos_validation.so

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

$ jar -tf project.apk | grep libVkLayer
lib/x86_64/libVkLayer_khronos_validation.so
lib/armeabi-v7a/libVkLayer_khronos_validation.so
lib/arm64-v8a/libVkLayer_khronos_validation.so
lib/x86/libVkLayer_khronos_validation.so

インスタンスの作成時に検証レイヤを有効にする

Vulkan API を使用すると、インスタンスの作成時にアプリでレイヤを有効にできます。レイヤがインターセプトするエントリ ポイントには、最初のパラメータとして以下のいずれかのオブジェクトが必要です。

  • VkInstance
  • VkPhysicalDevice
  • VkDevice
  • VkCommandBuffer
  • VkQueue

vkEnumerateInstanceLayerProperties() を呼び出すと、使用可能なレイヤとそのプロパティを一覧表示できます。Vulkan は、vkCreateInstance() の実行時にレイヤを有効にします。

次のコード スニペットは、アプリから Vulkan API を使用して、プログラムでレイヤに対してクエリや有効化を行う方法を示しています。

// Enable just the Khronos validation layer.
static const char *layers[] = {"VK_LAYER_KHRONOS_validation"};

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

// Enumerate layers with a valid pointer in the last parameter.
VkLayerProperties layer_props[instance_layer_present_count];
vkEnumerateInstanceLayerProperties(&instance_layer_present_count, layer_props);

// Make sure selected validation layers are available.
VkLayerProperties *layer_props_end = layer_props + instance_layer_present_count;
for (const char* layer:layers) {
  assert(layer_props_end !=
  std::find_if(layer_props, layer_props_end, [layer](VkLayerProperties layerProperties) {
    return strcmp(layerProperties.layerName, layer) == 0;
  }));
}

// Create a Vulkan instance, requesting all enabled layers or extensions
// available on the system
VkInstanceCreateInfo instanceCreateInfo{
  .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
  .pNext = nullptr,
  .pApplicationInfo = &appInfo,
  .enabledLayerCount = sizeof(layers) / sizeof(layers[0]),
  .ppEnabledLayerNames = layers,

デフォルトの logcat 出力

検証レイヤは、VALIDATION タグでラベル付けされた logcat に警告メッセージとエラー メッセージを出力します。検証レイヤのメッセージは次のようになります(ここではスクロールしやすいように改行を入れています)。

Validation -- Validation Error:
  [ VUID-VkDeviceQueueCreateInfo-pQueuePriorities-parameter ]
Object 0: VK_NULL_HANDLE, type = VK_OBJECT_TYPE_DEVICE; | MessageID = 0xd6d720c6 |
vkCreateDevice: required parameter
  pCreateInfo->pQueueCreateInfos[0].pQueuePriorities specified as NULL.
The Vulkan spec states: pQueuePriorities must be a valid pointer to an array of
  queueCount float values
  (https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html
  #VUID-VkDeviceQueueCreateInfo-pQueuePriorities-parameter)

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

Debug Utils 拡張機能 VK_EXT_debug_utils により、アプリが提供するコールバックに検証レイヤのメッセージを渡すデバッグ メッセンジャーをアプリで作成できます。この拡張機能は、お使いのデバイスに実装されていないかもしれませんが、最新の検証レイヤには実装されています。また、サポートを終了する拡張機能 VK_EXT_debug_report もあります。これは、VK_EXT_debug_utils が使用できない場合に同様の機能を提供します。

使用する前に、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[inst_ext_count];
vkEnumerateInstanceExtensionProperties(nullptr, &inst_ext_count, inst_exts);

// Check for debug utils extension within the system driver or loader.
// Check if the debug utils extension is available (in the driver).
VkExtensionProperties *inst_exts_end = inst_exts + inst_ext_count;
bool debugUtilsExtAvailable = inst_exts_end !=
  std::find_if(inst_exts, inst_exts_end, [](VkExtensionProperties
    extensionProperties) {
    return strcmp(extensionProperties.extensionName,
      VK_EXT_DEBUG_UTILS_EXTENSION_NAME) == 0;
  });

if ( !debugUtilsExtAvailable ) {
  // Also check the layers for the debug utils extension.
  for (auto layer: layer_props) {
    uint32_t layer_ext_count;
    vkEnumerateInstanceExtensionProperties(layer.layerName, &layer_ext_count,
      nullptr);
    if (layer_ext_count == 0) continue;
    VkExtensionProperties layer_exts[layer_ext_count];
    vkEnumerateInstanceExtensionProperties(layer.layerName, &layer_ext_count,
    layer_exts);

    VkExtensionProperties * layer_exts_end = layer_exts + layer_ext_count;
    debugUtilsExtAvailable = layer_exts != std::find_if(
      layer_exts, layer_exts_end,[](VkExtensionProperties extensionProperties) {
        return strcmp(extensionProperties.extensionName,
        VK_EXT_DEBUG_UTILS_EXTENSION_NAME) == 0;
      });
    if (debugUtilsExtAvailable) {
        // Add the including layer into the layer request list if necessary.
        break;
    }
  }
}

if (!debugUtilsExtAvailable) return; // since this snippet depends on debugUtils

const char * enabled_inst_exts[] = { ..., VK_EXT_DEBUG_UTILS_EXTENSION_NAME };
uint32_t enabled_extension_count =
  sizeof(enabled_inst_exts)/sizeof(enabled_inst_exts[0]);

// Pass the instance extensions into vkCreateInstance.
VkInstanceCreateInfo instance_info = {};
instance_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
instance_info.enabledExtensionCount = enabled_extension_count;
instance_info.ppEnabledExtensionNames = enabled_inst_exts;

// NOTE: Can still return VK_ERROR_EXTENSION_NOT_PRESENT if validation layer
// isn't loaded.
vkCreateInstance(&instance_info, nullptr, &instance);

auto pfnCreateDebugUtilsMessengerEXT =
  (PFN_vkCreateDebugUtilsMessengerEXT)vkGetInstanceProcAddr(
    tutorialInstance, "vkCreateDebugUtilsMessengerEXT");
auto pfnDestroyDebugUtilsMessengerEXT =
  (PFN_vkDestroyDebugUtilsMessengerEXT)vkGetInstanceProcAddr(
    tutorialInstance, "vkDestroyDebugUtilsMessengerEXT");

// Create the debug messenger callback with your the settings you want.
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;

  // The DebugUtilsMessenger callback is explained in the following section.
  messengerInfo.pfnUserCallback = &DebugUtilsMessenger;
  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;
}

外部の検証レイヤを使用する

検証レイヤを APK にパッケージ化する必要はありません。Android 9(API レベル 28)以降を搭載するデバイスでは、バイナリ外部の検証レイヤを使用して、これらのオンとオフを動的に切り替えられます。テストデバイスに検証レイヤをプッシュするには、このセクションの手順を実施します。

アプリで外部の検証レイヤを使用できるようにする

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

  • ターゲット アプリがデバッグ可能である。このオプションを使用すると、より詳細なデバッグ情報を得られますが、アプリのパフォーマンスに悪影響を及ぼす可能性があります。

  • root アクセス権限を付与するオペレーティング システムの userdebug ビルドでターゲット アプリが実行されている。

  • Android 11(API レベル 30)以降のみをターゲットとするアプリ: ターゲットの Android マニフェスト ファイルに次の meta-data 要素が含まれている。

    <meta-data android:name="com.android.graphics.injectLayers.enable"
      android:value="true"/>

外部の検証レイヤを読み込む

Android 9(API レベル 28)以降を搭載するデバイスでは、Vulkan はアプリのローカル ストレージから検証レイヤを読み込むことができます。Android 10(API レベル 29)以降では、Vulkan は、別の APK からも検証レイヤを読み込めるようになりました。ご利用の Android バージョンがサポートしている限り、任意の方法をお選びいただけます。

デバイスのローカル ストレージから検証レイヤバイナリを読み込む

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

  1. adb push コマンドを使用して、レイヤのバイナリをデバイス上のアプリのデータ ストレージに読み込みます。

    $ 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

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

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

アプリ単位でレイヤを有効にする

アプリレベルでレイヤを有効にする手順は次のとおりです。

  1. 次のように、adb シェル設定を使用してレイヤを有効にします。

    $ adb shell settings put global enable_gpu_debug_layers 1
    
  2. レイヤを有効にするターゲット アプリケーションを指定します。

    $ adb shell settings put global gpu_debug_app <package_name>
    
  3. 有効にするレイヤのリストを上から順に指定し、各レイヤをコロンで区切ります。

    $ adb shell settings put global gpu_debug_layers <layer1:layer2:layerN>
    

    Khronos の検証レイヤが 1 つしかないため、コマンドは次のようになります。

    $ adb shell settings put global gpu_debug_layers VK_LAYER_KHRONOS_validation
    
  4. 以下のコマンドのように、レイヤを検索するパッケージを 1 つ以上指定します。

    $ 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

レイヤをグローバルに有効にする

次回の再起動まで、1 つ以上のレイヤをグローバルに有効にできます。 これにより、すべてのアプリケーション(ネイティブ実行可能ファイルを含む)にレイヤが読み込まれます。

$ adb shell setprop debug.vulkan.layers <layer1:layer2:layerN>