Vulkan填坑學習Day07—Window Surface

Vulkan Window Surface

注:本章大部分代碼在前一章節交換鏈源代碼前中已經實現。

Vulkan Window Surface,到目前爲止,我們瞭解到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

現在開始着手創建窗體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銷燬之前完成。

二、查詢呈現支持

雖然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功能來自同一個隊列簇。
在這裏插入圖片描述

三、創建呈現隊列

剩下的事情是修改邏輯設備創建過程,在於創建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。

附:源碼

見上一章節day06源碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章