Vulkan教程 - 05 邏輯設備與窗口表面

邏輯設備

選擇了物理設備後,我們需要建立邏輯設備來交互了。邏輯設備創建過程和實例創建過程類似,且描述了我們想要的特性。向類中添加一個新的成員變量存儲邏輯設備句柄:

VkDevice device;

接着,添加函數createLogicalDevice以便在initVulkan中調用。創建邏輯設備涉及到在結構體中明確許多信息的操作,首先是設置VkDeviceQueueCreateInfo。該結構體表明瞭我們對單個隊列族所需的隊列個數,當前我們僅僅關心有圖形能力的隊列:

QueueFamilyIndices indices = findQueueFamilies(physicalDevice);

VkDeviceQueueCreateInfo queueCreateInfo = {};
queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
queueCreateInfo.queueFamilyIndex = indices.graphicsFamily.value();
queueCreateInfo.queueCount = 1;

當前可用的驅動只允許你爲每個隊列族創建較少的隊列,實際上你需要的不會超過一個。因爲你可以在多個線程上創建所有的命令緩衝,之後以一個低消耗的調用來一次性提交到主線程。Vulkan允許你向隊列優先級賦值以改變命令緩衝的執行順序,範圍是0到1的浮點數,即使單個隊列也要賦值:

float queuePriority = 1.0f;
queueCreateInfo.pQueuePriorities = &queuePriority;

接着要設置的信息是我們要用到的設備特性,這是之前章節用到的,也就是vkGetPhysicalDeviceFeatures查出來的,比如是否支持幾何着色器。當前我們不需要什麼特殊的東西,所以我們就簡單設置爲VK_FALSE。當需要做別的事情的時候我們會回過頭來設置它:

VkPhysicalDeviceFeatures deviceFeatures = {};

有了前面兩個結構體,我們就能對主要的設備創建結構體進行設置了,首先要向隊列創建信息和設備特性結構體中添加指針:

VkDeviceCreateInfo createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;

createInfo.pQueueCreateInfos = &queueCreateInfo;
createInfo.queueCreateInfoCount = 1;
createInfo.pEnabledFeatures = &deviceFeatures;

這裏看起來和VkInstanceCreateInfo有些像,而且後面還要設置擴展和驗證層,而不同之處是本次設置是設備相關的。設備相關的擴展,舉個例子,VK_KHR_swapchain允許你將渲染後的圖像通過那個設備呈現到窗口。系統中的Vulkan設備可能沒這個能力,比如只能支持計算操作,這些我們後面在交換鏈章節學習。

之前Vulkan的實現在實例和特定設備驗證層做了區分,但是現在已經不是這樣的了,也就是說VkDeviceCreateInfo的enabledLayerCount和ppEnabledLayerNames會被忽略。但是,爲了兼容以前的實現,我們仍然進行設置:

if (enableValidationLayers) {
    createInfo.enabledLayerCount = static_cast<uint32_t>(validationLayers.size());
    createInfo.ppEnabledLayerNames = validationLayers.data();
}
else {
    createInfo.enabledLayerCount = 0;
}

當前我們並不需要設備有關的擴展,現在準備實例化該邏輯設備,調用之前的vkCreateDevice:

if (vkCreateDevice(physicalDevice, &createInfo, nullptr, &device) != VK_SUCCESS) {
    throw std::runtime_error("failed to create logical device!");
}

其中的參數是,要交互的物理設備,我們剛剛指定的隊列和用法信息,可選的分配回調指針和一個指向存儲邏輯設備句柄的指針。在cleanup中該設備需要清理掉:

vkDestroyDevice(device, nullptr);

隊列是創建邏輯設備的時候自動創建的,但是我們沒有一個可以與之交互的句柄,首先要向類中添加一個成員來存儲圖形隊列句柄:

VkQueue graphicsQueue;

設備被銷燬的時候設備隊列也自動被銷燬,因此我們不用在cleanup中再處理一遍。我們可以用vkGetDeviceQueue方法獲取每個隊列族的隊列句柄,參數是邏輯設備,隊列族,隊列索引和一個指向存儲隊列句柄的指針。因爲我們才創建了一個隊列,所以其索引就是0:

vkGetDeviceQueue(device, indices.graphicsFamily.value(), 0, &graphicsQueue);

邏輯設備創建完成,我們後續就會用它做一些有趣的事情了。由於代碼很長,這裏就不貼完整代碼了。

窗口表面(Window surface,我這個翻譯應該不咋準)

由於Vulkan是平臺無關的,它不能直接與窗口系統交互。爲了建立Vulkan和窗口系統的連接,以便將結果呈現到屏幕上,我們需要使用WSI(Window System Integration)擴展。本章討論該擴展的第一個,也就是VK_KHR_surface。它暴露了VkSurfaceKHR對象,表示了一個抽象類型的表面來呈現渲染好的圖像。該surface會被我們已經用GLFW打開的窗口所支持。

VK_KHR_surface擴展是一個實例級別的擴展,我們實際上已經啓用它了,因爲它是包含在glfwGetRequiredInstanceExtensions返回的列表中的,該列表同時包含了一些其他WSI擴展,以後章節會用到。窗口表面需要在實例創建後立即被創建,因爲它實際上會影響物理設備選取。我們延後它的原因是窗口表面是更大範圍渲染對象的一部分,另外如果你只是要無屏幕渲染,窗口表面完全是可選的。

向類添加一個surface成員:

VkSurfaceKHR surface;

儘管VkSurfaceKHR對象和它的用法是平臺無關的,但是它的創建卻是平臺有關的,因爲它依賴窗口系統。比如在Windows上它需要HWND和HMODULE。因此有個平臺有關的附件,Windows上就是VK_KHR_win32_surface,也是自動包含在前面提到的列表中的。我會展示如何在Windows上用這個平臺相關的擴展創建一個表面,但是本教程實際上不會用它,因爲GLFW這樣的庫是平臺無關的,用它的同時再寫平臺相關的代碼就沒必要了。GLFW有glfwCreateWindowSurface能處理平臺特異問題,不過我們在開始用它之前還是瞭解下比較好。因爲窗口表面是Vulkan對象,自帶VkWin32SurfaceCreateInfoKHR結構體,需要我們填充。它有兩個重要的參數,hwnd和hinstance,它們是窗口和進程的句柄:

VkWin32SurfaceCreateInfoKHR createInfo = { };
createInfo.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR;
createInfo.hwnd = glfwGetWin32Window(window);
createInfo.hinstance = GetModuleHandle(nullptr);

glfwGetWin32Window方法用於從GLFW的window對象獲取原生HWND,GetModuleHandle返回當前進程的HINSTANCE句柄。之後可以用vkCreateWin32SurfaceKHR創建表面,包含了一個實例參數,表面創建明細信息,自定義分配器以及存儲表面句柄的變量。嚴格來說這個是WSI擴展方法,但是由於很常用,Vulkan加載器也包含了它,所以不像其他擴展,你不用顯式加載:

if (vkCreateWin32SurfaceKHR(instance, &createInfo, nullptr, &surface) != VK_SUCCESS) {
    throw std::runtime_error("failed to create window surface!");
}

該進程和其他平臺的如Linux等比較像,vkCreateXcbSurfaceKHR用一個XCB連接以及一個window作爲X11創建的詳細信息。glfwCreateWindowSurface方法對不同平臺都是一樣的操作,現在我們會把它包含進來。創建函數createSurface以便initVulkan調用:

void initVulkan() {
    createInstance();
    setupDebugMessenger();
    createSurface();
    pickPhysicalDevice();
    createLogicalDevice();
}

GLFW只要用簡單的參數就可以,所以實現很簡單:

void createSurface() {
    if (glfwCreateWindowSurface(instance, window, nullptr, &surface) != VK_SUCCESS) {
        throw std::runtime_error("failed to create window surface!");
    }
}

參數就是VkInstance,GLFW窗口指針,自定義分配器和指向VkSurfaceKHR變量的指針。在cleanup中銷燬如下:

vkDestroySurfaceKHR(instance, surface, nullptr);

該調用位於銷燬instance之前。

儘管Vulkan實現支持WSI,但是不表示系統中的每個設備都支持。因此我們要擴展isDeviceSuitable方法保證設備能將圖像呈現到表面。由於呈現是隊列有關的特性,所以實際上該問題就是找到一個隊列族,能夠支持呈現內容到表面。

實際上,隊列族支持繪製命令和支持呈現的可以不重合,因此我們需要考慮,可能有一個不同的呈現隊列,QueueFamilyIndices結構體修改如下:

struct QueueFamilyIndices {
    std::optional<uint32_t> graphicsFamily;
    std::optional<uint32_t> presentFamily;

    bool isComplete() {
        return graphicsFamily.has_value() && presentFamily.has_value();
    }
};

接着,修改findQueueFamilies方法,查找有呈現內容到窗口表面能力的隊列族。做該檢查任務的函數是vkGetPhysicalDeviceSurfaceSupportKHR,參數爲物理設備,隊列族索引以及表面。在和VK_QUEUE_GRAPHICS_BIT同一處循環調用:

VkBool32 presentSupport = false;
vkGetPhysicalDeviceSurfaceSupportKHR(device, i, surface, &presentSupport);

然後檢查布爾值,存儲呈現族隊列索引:

if (queueFamily.queueCount > 0 && presentSupport) {
    indices.presentFamily = i;
}

還有一件事,就是修改邏輯設備的創建過程,以創建呈現隊列並獲取VkQueue句柄,還是要添加一個變量:

VkQueue presentQueue;

接着,我們需要多個VkDeviceQueueCreateInfo結構體來從兩個族創建隊列。比較好的做法時創建一套所需隊列各不相同的隊列族:

QueueFamilyIndices indices = findQueueFamilies(physicalDevice);
std::vector<VkDeviceQueueCreateInfo> queueCreateInfos;
std::set<uint32_t> uniqueQueueFailies = { indices.graphicsFamily.value(), indices.presentFamily.value() };

float queuePriority = 1.0f;
for (uint32_t queueFamily : uniqueQueueFailies) {
    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指向vector:

createInfo.queueCreateInfoCount = static_cast<uint32_t>(queueCreateInfos.size());
createInfo.pQueueCreateInfos = queueCreateInfos.data();

如果隊列族是一樣的,那麼我們只需要傳一次索引。最後,調用一下獲取隊列句柄:

vkGetDeviceQueue(device, indices.presentFamily.value(), 0, &presentQueue);

下一章開始學習交換鏈。

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