操作系統: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的常見操作情景有:
- 根據規范檢查參數數值,最終確認是否存與預期不符的情況
- 跟蹤對象的創建和銷毀,以查找是否存在資源的泄漏
- 跟蹤線程的調用鏈,確認線程執行過程中的安全性
- 將每次函數調用所使用的參數記錄到標准的輸出中,進行初步的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_ATTR和VKAPI_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參數描述作為消息主題的對象的類型,比如一個obj是VkPhysicalDevice,那么objType就是VK_DEBUG_REPORT_OBJECT_TYPE_DEVICE_EXT。這樣做被允許是因為Vulkan的內部句柄都被定義為uint64_t。msg參數包含指向消息的指針。最后,有一個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,請將文件賦值到項目的Debug和Release目錄,然后按照說明設置需要的功能特性。除此之外,本教程將使用默認的設置。
現在是時候進入系統的Vulkan devices小節了。
項目代碼獲取 GitHub 地址。