Vulkan Tutorial 04 理解Validation layers


操作系統:Windows8.1

顯卡:Nivida GTX965M

開發工具:Visual Studio 2017


What are validation layers?

Vulkan API的設計核心是盡量最小化驅動程序的額外開銷,所謂額外開銷更多的是指向渲染以外的運算。其中一個具體的表現就是默認條件下,Vulkan API的錯誤檢查的支持非常有限。即使遍歷不正確的值或者將需要的參數傳遞為空指針,也不會有明確的處理邏輯,並且直接導致崩潰或者未定義的異常行為。之所以這樣,是因為Vulkan要求每一個步驟定義都非常明確,導致很容易造成小錯誤,例如使用新的GPU功能,但是忘記了邏輯設備創建時請求它。

 

但是,這並不意味着這些檢查不能添加到具體的API中。Vulkan推出了一個優化的系統,這個系統稱之為Validation layers。Validation layers是可選組件,可以掛載到Vulkan函數中調用,以回調其他的操作。Validation layers的常見操作情景有:

 

  1. 根據規范檢查參數數值,最終確認是否存與預期不符的情況
  2. 跟蹤對象的創建和銷毀,以查找是否存在資源的泄漏
  3. 跟蹤線程的調用鏈,確認線程執行過程中的安全性
  4. 將每次函數調用所使用的參數記錄到標准的輸出中,進行初步的Vulkan概要分析

以下示例代碼是一個函數中應用Validation layers的具體實現:

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

這些Validation layers可以隨意的堆疊到Vulkan驅動程序中,如果有必要,你甚至可以包含所有的debug功能。可以簡單的開啟Validation layers的debug版本,並在release版本中完全禁止,從而為您提供理想的兩個版本。

 

Vulkan沒有內置任何Validation layers,但是LunarG Vulkan SDK提供了一系列layers用於檢測常規的錯誤異常。他們是完全OpenSource的,所以你可以根據你需要的檢測需求應用具體的Validation layers。使用Validation layers是最佳的方式避免你的應用程序在發生未知的行為時收到影響,甚至中斷。

 

Vulkan只能使用已經安裝到系統上下文的Validation layers。例如,LunarG Validation layers僅在安裝了Vulkan SDK的PC上可用。

 

在之前的Vulkan版本中有兩種不同類型的Validation layers,分別應用於 instance 和 device specific。這個設計理念希望instance層只會驗證與全局Vulkan對象(例如Instance)有關的調用,而device specific層只是驗證與特定GPU相關的調用。device specific層已經被廢棄,這意味着instance層的Validation layers將應用所有的Vulkan調用。出於兼容性的考慮,規范文檔仍然建議在device specific層開啟Validation layers,這在某些情景下是有必要的。我們將在logic device層指定與instance相同的Validation layers,稍后會看到。

Using validation layers


在本節中,我們將介紹如何啟用Vulkan SDK提供的標准診斷層。就像擴展一樣,需要通過指定具體名稱來開啟validation layers。SDK通過請求VK_LAYER_LUNARG_standard_validaction層,來隱式的開啟有所關於診斷layers,從而避免明確的指定所有的明確的診斷層。

 

首先在程序中添加兩個配置變量來指定要啟用的layers以及是否開啟它們。我們選擇基於程序是否在調試模式下進行編譯。NDEBUG是C++標准宏定義,代表“不調試”。

const int WIDTH = 800;
const int HEIGHT = 600;

const std::vector<const char*> validationLayers = {
    "VK_LAYER_LUNARG_standard_validation"
};

#ifdef NDEBUG
    const bool enableValidationLayers = false;
#else
    const bool enableValidationLayers = true;
#endif

我們將添加一個新的函數checkValidationLayerSupport,檢測所有請求的layers是否可用。首先使用vkEnumerateInstanceLayerProperties函數列出所有可用的層。其用法與vkEnumerateInstanceExtensionProperties相同,在Instance小節中討論過。

bool checkValidationLayerSupport() {
    uint32_t layerCount;
    vkEnumerateInstanceLayerProperties(&layerCount, nullptr);

    std::vector<VkLayerProperties> availableLayers(layerCount);
    vkEnumerateInstanceLayerProperties(&layerCount, availableLayers.data());

    return false;
}

接下來檢查validationLayers中的所有layer是否存在於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!");
    }

    ...
}

現在以調試模式運行程序,並確保不會發生錯誤。如果發生錯誤,請確保正確安裝Vulkan SDK。如果沒有或者幾乎沒有layers上報,建議使用最新的SDK,或者到LunarG官方尋求幫助,需要注冊帳號。

 

最終,修改VkInstanceCreateInfo結構體,填充當前上下文已經開啟的validation layers名稱集合。

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


比較遺憾的是單純開啟validation layers是沒有任何幫助的,因為到現在沒有任何途徑將診斷信息回傳給應用程序。要接受消息,我們必須設置回調,需要VK_EXT_debug_report擴展。

 

我們新增一個getRequiredExtensions函數,該函數將基於是否開啟validation layers返回需要的擴展列表。

std::vector<const char*> getRequiredExtensions() {
    std::vector<const char*> extensions;

    unsigned int glfwExtensionCount = 0;
    const char** glfwExtensions;
    glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);

    for (unsigned int i = 0; i < glfwExtensionCount; i++) {
        extensions.push_back(glfwExtensions[i]);
    }

    if (enableValidationLayers) {
        extensions.push_back(VK_EXT_DEBUG_REPORT_EXTENSION_NAME);
    }

    return extensions;
}

GLFW的擴展總是需要的,而debug report擴展是根據編譯條件添加。與此同時我們使用VK_EXT_DEBUG_REPORT_EXTENSION_NAME宏定義,它等價字面值 "VK_EXT_debug_report",使用宏定義避免了硬編碼。

 

我們在createInstance函數中調用:

auto extensions = getRequiredExtensions();
createInfo.enabledExtensionCount = static_cast<uint32_t>(extensions.size());
createInfo.ppEnabledExtensionNames = extensions.data();

運行程序確保沒有收到VK_ERROR_EXTENSION_NOT_PRESENT錯誤信息,我們不需要去驗證擴展是否存在,因為它會被有效的validation layers引擎的驗證。

 

現在讓我們看一下callback函數的樣子,添加一個靜態函數debugCallback,並使用PFN_vkDebugReportCallbackEXT 原型進行修飾。VKAPI_ATTRVKAPI_CALL確保了正確的函數簽名,從而被Vulkan調用。

static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback(
    VkDebugReportFlagsEXT flags,
    VkDebugReportObjectTypeEXT objType,
    uint64_t obj,
    size_t location,
    int32_t code,
    const char* layerPrefix,
    const char* msg,
    void* userData) {

    std::cerr << "validation layer: " << msg << std::endl;

    return VK_FALSE;
}

函數的第一個參數指定了消息的類型,它可以通過一下任意標志位組合:

  • VK_DEBUG_REPORT_INFORMATION_BIT_EXT
  • VK_DEBUG_REPORT_WARNING_BIT_EXT
  • VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT
  • VK_DEBUG_REPORT_ERROR_BIT_EXT
  • VK_DEBUG_REPORT_DEBUG_BIT_EXT

objType參數描述作為消息主題的對象的類型,比如一個objVkPhysicalDevice,那么objType就是VK_DEBUG_REPORT_OBJECT_TYPE_DEVICE_EXT。這樣做被允許是因為Vulkan的內部句柄都被定義為uint64_tmsg參數包含指向消息的指針。最后,有一個userData參數可將自定義的數據進行回調。

 

回調返回一個布爾值,表明觸發validation layer消息的Vulkan調用是否應被中止。如果返回true,則調用將以VK_ERROR_VALIDATION_FAILED_EXT錯誤中止。這通常用於測試validation layers本身,所以我們總是返回VK_FALSE

 

現在需要告知Vulkan關於定義的回調函數。也許你會比較驚訝,即使是debug 回調也需要一個明確的創建和銷毀句柄的管理工作。添加一個類成員存儲回調句柄,在instance下。

VkDebugReportCallbackEXT callback;

現在添加一個函數setupDebugCallback,該函數會在initVulkan函數 調用createInstance之后調用。

void initVulkan() {
    createInstance();
    setupDebugCallback();
}

void setupDebugCallback() {
    if (!enableValidationLayers) return;

}

現在我們填充有關回調的結構體詳細信息:

VkDebugReportCallbackCreateInfoEXT createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_REPORT_CALLBACK_CREATE_INFO_EXT;
createInfo.flags = VK_DEBUG_REPORT_ERROR_BIT_EXT | VK_DEBUG_REPORT_WARNING_BIT_EXT;
createInfo.pfnCallback = debugCallback;

標志位允許過濾掉你不希望的消息。pfnCallback字段描述了回調函數的指針。在這里可以有選擇的傳遞一個pUserData指針,最為回調的自定義數據結構使用,比如可以傳遞HelloTriangleApplication類的指針。

 

該結構體應該傳遞給vkCreateDebugReportCallbackEXT函數創建VkDebugReportCallbackEXT對象。不幸的是,因為這個功能是一個擴展功能,它不會被自動加載。所以必須使用vkGetInstanceProcAddr查找函數地址。我們將在后台創建代理函數。在HelloTriangleApplication類定義之上添加它。

VkResult CreateDebugReportCallbackEXT(VkInstance instance, const VkDebugReportCallbackCreateInfoEXT* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDebugReportCallbackEXT* pCallback) {
    auto func = (PFN_vkCreateDebugReportCallbackEXT) vkGetInstanceProcAddr(instance, "vkCreateDebugReportCallbackEXT");
    if (func != nullptr) {
        return func(instance, pCreateInfo, pAllocator, pCallback);
    } else {
        return VK_ERROR_EXTENSION_NOT_PRESENT;
    }
}

如果函數無法加載,則vkGetInstanceProcAddr函數返回nullptr。如果非nullptr,就可以調用此函數來創建擴展對象:

if (CreateDebugReportCallbackEXT(instance, &createInfo, nullptr, &callback) != VK_SUCCESS) {
    throw std::runtime_error("failed to set up debug callback!");
}

倒數第二個參數仍然是分配器回調指針,我們仍然設置為nullptr。debug回調與Vulkan instance和layers相對應,所以需要明確指定第一個參數。現在運行程序,關閉窗口,你會在命令行看到提示信息:

validation layer: Debug Report callbacks not removed before DestroyInstance

現在Vulkan已經在程序中發現了一個錯誤!需要通過調用vkDestroyDebugReportCallbackEXT清理VkDebugReportCallbackEXT對象。與vkCreateDebugReportCallbackEXT類似,該函數需要顯性的加載。在CreateDebugReportCallbackEXT下創建另一個代理函數。

void DestroyDebugReportCallbackEXT(VkInstance instance, VkDebugReportCallbackEXT callback, const VkAllocationCallbacks* pAllocator) {
    auto func = (PFN_vkDestroyDebugReportCallbackEXT) vkGetInstanceProcAddr(instance, "vkDestroyDebugReportCallbackEXT");
    if (func != nullptr) {
        func(instance, callback, pAllocator);
    }
}

該函數定義為類靜態函數或者外部函數,我們在cleanup函數中進行調用:

void cleanup() {
    DestroyDebugReportCallbackEXT(instance, callback, nullptr);
    vkDestroyInstance(instance, nullptr);

    glfwDestroyWindow(window);

    glfwTerminate();
}

再次運行程序,會看到錯誤信息已經消失。如果要查看哪個調用觸發了一條消息,可以向消息回調添加斷點,並查看堆棧調用鏈。

Configuration


Validation layers的行為可以有更多的設置,不僅僅是VkDebugReportCallbackCreateInfoEXT結構中指定的標志位信息。瀏覽Vulkan SDK的Config目錄。找到vk_layer_settings.txt文件,里面有說明如何配置layers。

 

要為自己的應用程序配置layers,請將文件賦值到項目的DebugRelease目錄,然后按照說明設置需要的功能特性。除此之外,本教程將使用默認的設置。

現在是時候進入系統的Vulkan devices小節了。

 

項目代碼獲取 GitHub 地址。 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM