2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Vulkan Tutorial (Drawing a triangle/Setup/Validation layers) 日本語訳

Last updated at Posted at 2022-07-04

バリデーションレイヤ (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_ATTRVKAPI_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文の外側に定義しています。こうやって追加のデバッグメッセンジャを作成することによって、自動的にvkCreateInstancevkDestroyInstance呼び出し時に使われ、その後に破棄されるようになります。

テスト (Testing)

バリデーションレイヤが動作しているところを見るために、意図的に間違いを作り出してみましょう。一時的にcleanup関数からDestroyDebugUtilsMessengerEXT呼び出しを取り除いて、プログラムを実行してください。終了したら、次のような表示がされるはずです:

もしこのようなメッセージが表示されない場合は、インストールをチェックしてください

どの関数呼び出しがメッセージの引き金となったのかを知りたければ、メッセージコールバックにブレイクポイントを追加して、スタックトレースを見ることができます。

Configuration

バリデーションレイヤの動作には、VkDebugUtilsMessengerCreateInfoEXT構造体にセットするフラグ以外にもたくさんの設定があります。Vulkan SDK のConfigディレクトリを見てください。レイヤの設定方法について説明した、vk_layer_settings.txtファイルが見つかるでしょう。

あなたのアプリケーションでレイヤの設定を変更するには、このファイルをプロジェクトのDebugReleaseディレクトリにコピーし、設定したい動作の指示に従ってください。しかし、このチュートリアルでは、デフォルトの設定を使っていると仮定します。

このチュートリアルを通して、私はいくつかの間違いをわざと入れ、バリデーションレイヤーがそれを捕まえるのにいかに役立つかを見せます。そして、Vulkanで何をしているか正確に知ることがどれだけ重要であるかを教えます。さて、システム内のVulkanデバイスについて見るときがきました。

C++ code

前の記事
次の記事

  1. 訳注:pObjectsの要素数

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?