邏輯設備
選擇了物理設備後,我們需要建立邏輯設備來交互了。邏輯設備創建過程和實例創建過程類似,且描述了我們想要的特性。向類中添加一個新的成員變量存儲邏輯設備句柄:
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);
下一章開始學習交換鏈。