バリデーションレイヤ (Validation layers)
バリデーションレイヤとは何か? (What are validation layers?)
Vulkan API はドライバのオーバーヘッドを最小化するという考えに基づいて設計されているので、その結果として、デフォルトではAPIでのエラーチェックは非常に限られています。設定するenum値を間違えたり、必須の引数にNULLポインタを渡したりといったような単純な間違いでさえ、一般的には明示的に対処されず、単純にクラッシュするか未定義の動作を引き起こします。Vulkanは、行っていることについて全て明示的にすることを要求するので、新しいGPUの機能を使うときに、論理デバイス作成時にそれを要求するのを忘れるなどのような小さなミスを起こしやすいです。
しかし、それらのチェックをAPIに追加することができないということではありません。Vulkanはバリデーションレイヤ(validation layers)として知られる、エレガントなシステムを導入します。バリデーションレイヤはオプションのコンポーネントで、Vulkanの関数呼び出しをフックして、追加の操作を適用します。バリデーションレイヤでの一般的な操作は:
- 仕様に対して間違った使い方をしていないか、引数の値をチェックする
- オブジェクトの作成と破棄を追跡して、リソースリークを発見する
- スレッドの呼び出し元を追跡して、スレッドセーフかどうかチェックする
- 全ての呼び出しをその引数を、標準出力に出力する
- Vulkanの呼び出しをプロファイリングとリプレイのためにトレースする
診断バリデーションレイヤでの関数の実装がどのようなものか、以下がその例です:
VkResult vkCreateInstance(
const VkInstanceCreateInfo* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkInstance* instance) {
if (pCreateInfo == nullptr || instance == nullptr) {
log("Null pointer passed to required parameter!");
return VK_ERROR_INITIALIZATION_FAILED;
}
return real_vkCreateInstance(pCreateInfo, pAllocator, instance);
}
これらのバリデーションレイヤは、あなたが興味あるデバッグ機能を全て含むように、自由に積み重ねることができます。あなたは単純に、デバッグビルドではバリデーションレイヤを有効にし、リリースビルドでは完全に無効にすることができ、これによって両方のいいとこ取りをすることができます。
Vulkanには組み込みのバリデーションレイヤはありませんが、LunarG Vulkan SDK は一般的なエラーをチェックするレイヤーの素晴らしいセットを提供しています。また、それらは完全にオープンソースなので、あなたはどのような種類の間違いがチェックされているか調べて、コントリビュートすることができます。バリデーションレイヤを使うことは、あなたのアプリケーションが未定義動作に依存してたまたま動いていたものが、別のドライバだとクラッシュするといったことを避けるのに最良の方法です。
バリデーションレイヤはシステムにインストールされたものだけ使うことができます。例えば、LunarGのバリデーションレイヤは Vulkan SDK がインストールされたPCでだけ有効です。
Vulkanのバリデーションレイヤは、以前はインスタンスレイヤとデバイス固有レイヤの2種類が存在しました。最初のアイデアは、インスタンスレイヤはインスタンスのようなグローバルなVulkanオブジェクトに関してのみチェックし、デバイス固有レイヤは特定のGPUに関する呼び出しだけをチェックする、といったものでした。デバイス固有レイヤは今は非推奨になっており、インスタンスレイヤが全てのVulkan呼び出しに適用されるようになっています。仕様書は互換性のためにデバイス固有レイヤを有効にすることを推奨しており、いくつかの実装ではそれが要求されます。私たちは後で見るように、単純にインスタンスとデバイスで同じレイヤを指定するようにします。
バリデーションレイヤの使用 (Using validation layers)
この節では、Vulkan SDK で提供されている標準的な診断レイヤをどうやって有効にするか見ていきます。拡張機能と同じように、バリデーションレイヤも名前を指定することによって有効にする必要があります。全ての有用な標準の検証機能は、VK_LAYER_KHRONOS_validation
として知られる、SDKに含まれるレイヤにバンドルされています。
最初に、有効にするレイヤを指定する変数と、それらを有効にするかどうかの変数をプログラムに追加しましよう。私はこの値が、プログラムがデバッグモードでコンパイルされているかどうかに基づくようにしました。NDEBUG
マクロはC++標準の一部で、「デバッグでない(not debug)」ことを意味します。
const uint32_t WIDTH = 800;
const uint32_t HEIGHT = 600;
const std::vector<const char*> validationLayers = {
"VK_LAYER_KHRONOS_validation"
};
#ifdef NDEBUG
const bool enableValidationLayers = false;
#else
const bool enableValidationLayers = true;
#endif
私たちは、リクエストされたレイヤが全て利用可能かどうかチェックする、checkValidationLayerSupport
関数を新しく追加します。最初に、vkEnumerateInstanceLayerProperties
関数を使って、全ての利用可能なレイヤをリストアップします。この関数の使い方は、インスタンス作成の章で説明した、vkEnumerateInstanceExtensionProperties
と同じです。
bool checkValidationLayerSupport() {
uint32_t layerCount;
vkEnumerateInstanceLayerProperties(&layerCount, nullptr);
std::vector<VkLayerProperties> availableLayers(layerCount);
vkEnumerateInstanceLayerProperties(&layerCount, availableLayers.data());
return false;
}
次に、validationLayers
内のレイヤが全てavailableLayers
リストの中に存在するかどうかチェックします。strcmp
を使うために<cstring>
をインクルードする必要があるでしょう。
for (const char* layerName : validationLayers) {
bool layerFound = false;
for (const auto& layerProperties : availableLayers) {
if (strcmp(layerName, layerProperties.layerName) == 0) {
layerFound = true;
break;
}
}
if (!layerFound) {
return false;
}
}
return true;
これでcreateInstance
の中でこの関数をつかえます:
void createInstance() {
if (enableValidationLayers && !checkValidationLayerSupport()) {
throw std::runtime_error("validation layers requested, but not available!");
}
...
}
ここで、デバッグモードでプログラムを実行し、エラーが起こらないか確認してください。もしエラーが起こったら、FAQを参照してください。
最後に、バリデーションレイヤが有効なときはその名前を含むように、VkInstanceCreateInfo
構造体の初期化を変更します。
if (enableValidationLayers) {
createInfo.enabledLayerCount = static_cast<uint32_t>(validationLayers.size());
createInfo.ppEnabledLayerNames = validationLayers.data();
} else {
createInfo.enabledLayerCount = 0;
}
もしチェックが成功していたら、vkCreateInstance
は決してVK_ERROR_LAYER_NOT_PRESENT
エラーを返さないはずですが、確認のためにプログラムを走らせてください。
メッセージコールバック (Message callback)
バリデーションレイヤはデフォルトでデバッグメッセージを標準出力に表示しますが、プログラムで明示的にコールバックを提供することで、その動作を自分自身で処理することもできます。全てのエラーが必要な(致命的な)エラーというわけではないので、これによってどの種類のメッセージを表示するか決定することができます。もしこれが今すぐ必要でないなら、この章の最後の節まで飛ばしてもらってかまいません。
メッセージとそれに関する詳細を処理するためにコールバックをセットアップするには、VK_EXT_debug_utils
拡張を使ってデバッグメッセンジャーのコールバックを設定する必要があります。
私達は最初に、バリデーションレイヤが有効になっているかどうかに応じて必要な拡張機能のリストを返す、getRequiredExtensions
関数を作成します。
std::vector<const char*> getRequiredExtensions() {
uint32_t glfwExtensionCount = 0;
const char** glfwExtensions;
glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);
std::vector<const char*> extensions(glfwExtensions, glfwExtensions + glfwExtensionCount);
if (enableValidationLayers) {
extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
}
return extensions;
}
GLFWで指定された拡張機能は常に必要ですが、デバッグメッセージの拡張機能は条件付きで追加されます。ここで、文字列リテラル"VK_EXT_debug_utils"と等しいVK_EXT_DEBUG_UTILS_EXTENSION_NAME
マクロを使っていることに注意してください。このマクロを使うことによってタイポを避けることができます。
これでこの関数をcreateInstance
内で使うことができます。
auto extensions = getRequiredExtensions();
createInfo.enabledExtensionCount = static_cast<uint32_t>(extensions.size());
createInfo.ppEnabledExtensionNames = extensions.data();
VK_ERROR_EXTENSION_NOT_PRESENT
エラーが帰ってこないことを確認するために、プログラムを実行してください。バリデーションレイヤが利用可能であれば、暗黙のうちにこの拡張機能が存在するので、本当はチェックする必要はありません。
では、デバッグコールバック関数がどのようなものか見てみましょう。PFN_vkDebugUtilsMessengerCallbackEXT
プロトタイプに適合した、debugCallback
というスタティックメンバ関数を追加してください。VKAPI_ATTR
とVKAPI_CALL
は、この関数がVulkanから呼び出されるのに正しいシグネチャであることを保証します。
static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback(
VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,
VkDebugUtilsMessageTypeFlagsEXT messageType,
const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData,
void* pUserData) {
std::cerr << "validation layer: " << pCallbackData->pMessage << std::endl;
return VK_FALSE;
}
最初の引数はメッセージの重要度を表し、次のフラグのうちどれかです:
-
VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT
:診断メッセージ -
VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT
:リソースを作成したといったような情報メッセージ -
VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT
:必ずしもエラーではないが、アプリケーションのバグである可能性が高い動作についてのメッセージ -
VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT
:無効でクラッシュにつながる動作についてのメッセージ
これらのenum値は、メッセージがある重要度レベル以上かどうか、比較演算子を使ってチェックできるように設定されています。例えば:
if (messageSeverity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) {
// Message is important enough to show
// メッセージは表示する必要があるほど重要
}
引数messageType
は以下の値をとります:
-
VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT
:仕様にもパフォーマンスにも関係の無いイベントが発生した -
VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT
:仕様に違反した、もしくは間違いの可能性がある何かが起こった -
VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT
:Vulkanの最適でない使い方をしている可能性
引数pCallbackData
は、メッセージの詳細を格納しているVkDebugUtilsMessengerCallbackDataEXT
構造体を参照しています。この構造体で重要なメンバは:
-
pMessage
:ヌル終端されたデバッグメッセージ -
pObjects
:メッセージに関連するVulkanオブジェクトハンドルの配列 -
objectCount
:オブジェクトの配列の要素数1
最後に、pUserData
はコールバックをセットアップするときに指定したポインタ値で、これによってあなた自身のデータを渡すことができます。
コールバックは、メッセージの引き金となったVulkan関数呼び出しを中止するかどうかを表す、ブール値を返します。もしコールバックがtrueを返すと、関数は中止され、VK_ERROR_VALIDATION_FAILED_EXT
エラーが返ります。これは通常は、バリデーションレイヤ自身のテストのためにのみ使われるので、あなたは常にVK_FALSE
を返すと良いでしょう。
残るは、コールバック関数についてVulkanに伝えるだけです。驚くかもしれませんが、Vulkanではデバッグコールバックでさえ、明示的な生成と破棄が必要なハンドルによって管理されます。このようなコールバックはデバッグメッセンジャの一部で、あなたが必要なだけ持つことができます。instance
のすぐ下に、このハンドルをメンバ変数として追加してください。
VkDebugUtilsMessengerEXT debugMessenger;
initVulkan
関数の中でcreateInstance
の直後に呼び出される、setupDebugMessenger
関数を追加してください。
void initVulkan() {
createInstance();
setupDebugMessenger();
}
void setupDebugMessenger() {
if (!enableValidationLayers) return;
}
メッセンジャとそのコールバックについての詳細を、構造体に設定する必要があります。
VkDebugUtilsMessengerCreateInfoEXT createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
createInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT;
createInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;
createInfo.pfnUserCallback = debugCallback;
createInfo.pUserData = nullptr; // Optional
messageSeverity
メンバで、コールバックが呼び出されて欲しい重要度を指定することができます。ここで、VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT
以外の全てのタイプを指定しているので、冗長で一般的なデバッグ情報を除いた、問題の可能性がある通知を受け取ることができます。
同じように、messageType
でコールバックが呼び出されるメッセージの種類をフィルタリングできます。ここでは単純に全ての種類を有効にしています。あなたは、必要がないものについては常に無効にすることができます。
最後に、pfnUserCallback
にコールバック関数へのポインタを設定します。あなたはpUserData
にポインタを渡すこともでき、それはコールバック関数のpUserData
引数に渡されます。例えば、これを使ってHelloTriangleApplication
クラスへのポインタを渡す、といったことができます。
バリデーションレイヤメッセージとデバッグコールバックの設定にはもっとたくさんの方法がありますが、このチュートリアルを始めるにはこの設定が適しているでしょう。可能な設定についてのさらなる情報は拡張機能の仕様を参照してください。
この構造体はVkDebugUtilsMessengerEXT
オブジェクトを作るためにvkCreateDebugUtilsMessengerEXT
関数に渡されます。残念なことに、この関数は拡張関数なので、自動的にはロードされません。vkGetInstanceProcAddr
を使って、自分自身でそのアドレスを調べる必要があります。私達は、これをバックグラウンドで処理する、独自のプロキシ関数を作成します。HelloTriangleApplication
クラス定義のすぐ上に、これを追加します。
VkResult CreateDebugUtilsMessengerEXT(VkInstance instance, const VkDebugUtilsMessengerCreateInfoEXT* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDebugUtilsMessengerEXT* pDebugMessenger) {
auto func = (PFN_vkCreateDebugUtilsMessengerEXT) vkGetInstanceProcAddr(instance, "vkCreateDebugUtilsMessengerEXT");
if (func != nullptr) {
return func(instance, pCreateInfo, pAllocator, pDebugMessenger);
} else {
return VK_ERROR_EXTENSION_NOT_PRESENT;
}
}
vkGetInstanceProcAddr
関数は、指定された関数がロードできないときはnullptr
を返します。これで、拡張オブジェクトが利用可能であれば、この関数を呼び出して作成することができます。
if (CreateDebugUtilsMessengerEXT(instance, &createInfo, nullptr, &debugMessenger) != VK_SUCCESS) {
throw std::runtime_error("failed to set up debug messenger!");
}
最後から2番めの引数は、再びnullptr
に設定したオプションのアロケータコールバックで、その他の引数は見たままです。デバッグメッセンジャはVulkanインスタンスとそのレイヤに固有のものなので、最初の引数で明示的に指定する必要があります。このパターンは、のちのち、他の子オブジェクトでも見られます。
VkDebugUtilsMessengerEXT
オブジェクトもまた、vkDestroyDebugUtilsMessengerEXT
を呼び出してクリーンアップする必要があります。vkCreateDebugUtilsMessengerEXT
と同様、この関数も明示的にロードする必要があります。
CreateDebugUtilsMessengerEXT
のすぐ下に、別のプロキシ関数を作成します:
void DestroyDebugUtilsMessengerEXT(VkInstance instance, VkDebugUtilsMessengerEXT debugMessenger, const VkAllocationCallbacks* pAllocator) {
auto func = (PFN_vkDestroyDebugUtilsMessengerEXT) vkGetInstanceProcAddr(instance, "vkDestroyDebugUtilsMessengerEXT");
if (func != nullptr) {
func(instance, debugMessenger, pAllocator);
}
}
この関数はクラスのスタティック関数か、クラス外の関数にしてください。そして、cleanup
関数内でこの関数を呼び出すことができます。
void cleanup() {
if (enableValidationLayers) {
DestroyDebugUtilsMessengerEXT(instance, debugMessenger, nullptr);
}
vkDestroyInstance(instance, nullptr);
glfwDestroyWindow(window);
glfwTerminate();
}
インスタンス作成時と破棄時のデバッグ (Debugging instance creation and destruction)
プログラムにバリデーションレイヤとデバッグ機能を追加しましたが、まだ全てをカバーしたわけではありません。vkCreateDebugUtilsMessengerEXT
呼び出しは有効なインスタンスが作成されていることを要求しますし、vkDestroyDebugUtilsMessengerEXT
はインスタンスが破棄される前に呼び出されなければいけません。このため、現状では vkCreateInstance
および vkDestroyInstance
呼び出しの問題をデバッグすることができないままです。
しかし、注意深く拡張機能のドキュメントを読んだのなら、これら2つの関数のために、別のデバッグメッセンジャを作成する方法があることに気づきます。これは、VkInstanceCreateInfo
のメンバpNext
に、VkDebugUtilsMessengerCreateInfoEXT
構造体へのポインタを渡すだけです。最初に、メッセンジャの作成情報を関数に分割します。
void populateDebugMessengerCreateInfo(VkDebugUtilsMessengerCreateInfoEXT& createInfo) {
createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
createInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT;
createInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;
createInfo.pfnUserCallback = debugCallback;
}
...
void setupDebugMessenger() {
if (!enableValidationLayers) return;
VkDebugUtilsMessengerCreateInfoEXT createInfo;
populateDebugMessengerCreateInfo(createInfo);
if (CreateDebugUtilsMessengerEXT(instance, &createInfo, nullptr, &debugMessenger) != VK_SUCCESS) {
throw std::runtime_error("failed to set up debug messenger!");
}
}
これをcreateInstance
関数の中でも再利用することができます。
void createInstance() {
...
VkInstanceCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
createInfo.pApplicationInfo = &appInfo;
...
VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo{};
if (enableValidationLayers) {
createInfo.enabledLayerCount = static_cast<uint32_t>(validationLayers.size());
createInfo.ppEnabledLayerNames = validationLayers.data();
populateDebugMessengerCreateInfo(debugCreateInfo);
createInfo.pNext = (VkDebugUtilsMessengerCreateInfoEXT*) &debugCreateInfo;
} else {
createInfo.enabledLayerCount = 0;
createInfo.pNext = nullptr;
}
if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) {
throw std::runtime_error("failed to create instance!");
}
}
debugCreateInfo
変数は、vkCreateInstance
呼び出しより前に破棄されないように、if文の外側に定義しています。こうやって追加のデバッグメッセンジャを作成することによって、自動的にvkCreateInstance
とvkDestroyInstance
呼び出し時に使われ、その後に破棄されるようになります。
テスト (Testing)
バリデーションレイヤが動作しているところを見るために、意図的に間違いを作り出してみましょう。一時的にcleanup
関数からDestroyDebugUtilsMessengerEXT
呼び出しを取り除いて、プログラムを実行してください。終了したら、次のような表示がされるはずです:
もしこのようなメッセージが表示されない場合は、インストールをチェックしてください。
どの関数呼び出しがメッセージの引き金となったのかを知りたければ、メッセージコールバックにブレイクポイントを追加して、スタックトレースを見ることができます。
Configuration
バリデーションレイヤの動作には、VkDebugUtilsMessengerCreateInfoEXT
構造体にセットするフラグ以外にもたくさんの設定があります。Vulkan SDK のConfig
ディレクトリを見てください。レイヤの設定方法について説明した、vk_layer_settings.txt
ファイルが見つかるでしょう。
あなたのアプリケーションでレイヤの設定を変更するには、このファイルをプロジェクトのDebug
とRelease
ディレクトリにコピーし、設定したい動作の指示に従ってください。しかし、このチュートリアルでは、デフォルトの設定を使っていると仮定します。
このチュートリアルを通して、私はいくつかの間違いをわざと入れ、バリデーションレイヤーがそれを捕まえるのにいかに役立つかを見せます。そして、Vulkanで何をしているか正確に知ることがどれだけ重要であるかを教えます。さて、システム内のVulkanデバイスについて見るときがきました。
-
訳注:
pObjects
の要素数 ↩