操作系統:Windows8.1
顯卡:Nivida GTX965M
開發工具:Visual Studio 2017
到目前為止,我們了解到Vulkan是一個與平台特性無關聯的API集合。它不能直接與窗口系統進行交互。為了將渲染結果呈現到屏幕,需要建立Vulkan與窗體系統之間的連接,我們需要使用WSI(窗體系統集成)擴展。在本小節中,我們將討論第一個,即VK_KHR_surface。它暴露了VkSurfaceKHR,它代表surface的一個抽象類型,用以呈現渲染圖像使用。我們程序中將要使用到的surface是由我們已經引入的GLFW擴展及其打開的相關窗體支持的。簡單來說surface就是Vulkan與窗體系統的連接橋梁。
VK_KHR_surface擴展是一個instance級擴展,我們目前為止已經啟用過它,它包含在glfwGetRequiredInstanceExtensions返回的列表中。該列表還包括將在接下來幾小節中使用的一些其他WSI擴展。
需要在instance創建之后立即創建窗體surface,因為它會影響物理設備的選擇。之所以在本小節將surface創建邏輯納入討論范圍,是因為窗體surface對於渲染、呈現方式是一個比較大的課題,如果過早的在創建物理設備加入這部分內容,會混淆基本的物理設備設置工作。另外窗體surface本身對於Vulkan也是非強制的。Vulkan允許這樣做,不需要同OpenGL一樣必須要創建窗體surface。
Window surface creation
現在開始着手創建窗體surface,在類成員debugCallback下加入成員變量surface。
VkSurfaceKHR surface;
雖然VkSurfaceKHR對象及其用法與平台無關聯,但創建過程需要依賴具體的窗體系統的細節。比如,在Windows平台中,它需要WIndows上的HWND和HMODULE句柄。因此針對特定平台提供相應的擴展,在Windows上為VK_KHR_win32_surface,它自動包含在glfwGetRequiredInstanceExtensions列表中。
我們將會演示如何使用特定平台的擴展來創建Windows上的surface橋,但是不會在教程中實際使用它。使用GLFW這樣的庫避免了編寫沒有任何意義的跨平台相關代碼。GLFW實際上通過glfwCreateWindowSurface很好的處理了平台差異性。當然了,比較理想是在依賴它們幫助我們完成具體工作之前,了解一下背后的實現是有幫助的。
因為一個窗體surface是一個Vulkan對象,它需要填充VkWin32SurfaceCreateInfoKHR結構體,這里有兩個比較重要的參數:hwnd和hinstance。如果熟悉windows下開發應該知道,這些是窗口和進程的句柄。
VkWin32SurfaceCreateInfoKHR createInfo; createInfo.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR; createInfo.hwnd = glfwGetWin32Window(window); createInfo.hinstance = GetModuleHandle(nullptr);
glfwGetWin32Window函數用於從GLFW窗體對象獲取原始的HWND。GetModuleHandle函數返回當前進程的HINSTANCE句柄。
填充完結構體之后,可以利用vkCreateWin32SurfaceKHR創建surface橋,和之前獲取創建、銷毀DebugReportCallEXT一樣,這里同樣需要通過instance獲取創建surface用到的函數。這里涉及到的參數分別為instance, surface創建的信息,自定義分配器和最終保存surface的句柄變量。
auto CreateWin32SurfaceKHR = (PFN_vkCreateWin32SurfaceKHR) vkGetInstanceProcAddr(instance, "vkCreateWin32SurfaceKHR"); if (!CreateWin32SurfaceKHR || CreateWin32SurfaceKHR(instance, &createInfo, nullptr, &surface) != VK_SUCCESS) { throw std::runtime_error("failed to create window surface!"); }
該過程與其他平台類似,比如Linux,使用X11界面窗體系統,可以通過vkCreateXcbSurfaceKHR函數建立連接。
glfwCreateWindowSurface函數根據不同平台的差異性,在實現細節上會有所不同。我們現在將其整合到我們的程序中。從initVulkan中添加一個函數createSurface,安排在createInstnace和setupDebugCallback函數之后。
void initVulkan() { createInstance(); setupDebugCallback(); createSurface(); pickPhysicalDevice(); createLogicalDevice(); } void createSurface() { }
GLFW沒有使用結構體,而是選擇非常直接的參數傳遞來調用函數。
void createSurface() { if (glfwCreateWindowSurface(instance, window, nullptr, &surface) != VK_SUCCESS) { throw std::runtime_error("failed to create window surface!"); } }
參數是VkInstance,GLFW窗體的指針,自定義分配器和用於存儲VkSurfaceKHR變量的指針。對於不同平台統一返回VkResult。GLFW沒有提供專用的函數銷毀surface,但是可以簡單的通過Vulkan原始的API完成:
void cleanup() { ... vkDestroySurfaceKHR(instance, surface, nullptr); vkDestroyInstance(instance, nullptr); ... }
最后請確保surface的清理是在instance銷毀之前完成。
Querying for presentation support
雖然Vulkan的實現支持窗體集成功能,但是並不意味着系統中的每一個物理設備都支持它。因此,我們需要擴展isDeviceSuitable函數,確保設備可以將圖像呈現到我們創建的surface。由於presentation是一個隊列的特性功能,因此解決問題的方法就是找到支持presentation的隊列簇,最終獲取隊列滿足surface創建的需要。
實際情況是,支持graphics命令的的隊列簇和支持presentation命令的隊列簇可能不是同一個簇。因此,我們需要修改QueueFamilyIndices結構體,以支持差異化的存儲。
struct QueueFamilyIndices { int graphicsFamily = -1; int presentFamily = -1; bool isComplete() { return graphicsFamily >= 0 && presentFamily >= 0; } };
接下來,我們修改findQueueFamilies函數來查找具備presentation功能的隊列簇。函數中用於檢查的核心代碼是vkGetPhysicalDeviceSurfaceSupportKHR,它將物理設備、隊列簇索引和surface作為參數。在VK_QUEUE_GRAPHICS_BIT相同的循環體中添加函數的調用:
VkBool32 presentSupport = false; vkGetPhysicalDeviceSurfaceSupportKHR(device, i, surface, &presentSupport);
然后之需要檢查布爾值並存儲presentation隊列簇的索引:
if (queueFamily.queueCount > 0 && presentSupport) { indices.presentFamily = i; }
需要注意的是,為了支持graphics和presentation功能,我們實際環境中得到的可能是同一個隊列簇,也可能不同,為此在我們的程序數據結構及選擇邏輯中,將按照均來自不同的隊列簇分別處理,這樣便可以統一處理以上兩種情況。除此之外,出於性能的考慮,我們也可以通過添加邏輯明確的指定物理設備所使用的graphics和presentation功能來自同一個隊列簇。
Creating the presentation queue
剩下的事情是修改邏輯設備創建過程,在於創建presentation隊列並獲取VkQueue的句柄。添加保存隊列句柄的成員變量:
VkQueue presentQueue;
接下來,我們需要多個VkDeviceQueueCreateInfo結構來創建不同功能的隊列。一個優雅的方式是針對不同功能的隊列簇創建一個set集合確保隊列簇的唯一性:
#include <set> ... QueueFamilyIndices indices = findQueueFamilies(physicalDevice); std::vector<VkDeviceQueueCreateInfo> queueCreateInfos; std::set<int> uniqueQueueFamilies = {indices.graphicsFamily, indices.presentFamily}; float queuePriority = 1.0f; for (int queueFamily : uniqueQueueFamilies) { VkDeviceQueueCreateInfo queueCreateInfo = {}; queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; queueCreateInfo.queueFamilyIndex = queueFamily; queueCreateInfo.queueCount = 1; queueCreateInfo.pQueuePriorities = &queuePriority; queueCreateInfos.push_back(queueCreateInfo); }
同時還要修改VkDeviceCreateInfo指向隊列集合:
createInfo.queueCreateInfoCount = static_cast<uint32_t>(queueCreateInfos.size());
createInfo.pQueueCreateInfos = queueCreateInfos.data();
如果隊列簇相同,那么我們之需要傳遞一次索引。最后,添加一個調用檢索隊列句柄:
vkGetDeviceQueue(device, indices.presentFamily, 0, &presentQueue);
在這個例子中,隊列簇是相同的,兩個句柄可能會有相同的值。在下一個章節中我們會看看交換鏈,以及它們如何使我們能夠將圖像呈現給surface。
獲取工程代碼 GitHub checkout