逻辑设备
选择了物理设备后,我们需要建立逻辑设备来交互了。逻辑设备创建过程和实例创建过程类似,且描述了我们想要的特性。向类中添加一个新的成员变量存储逻辑设备句柄:
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);
下一章开始学习交换链。