Vulkan 学习笔记(八) 逻辑设备和队列
介绍
在选择要使用的物理设备后,我们需要设置一个逻辑设备来与之交互。逻辑设备的创建过程与实例创建过程类似,用于描述我们想要使用的特性。由于我们已经查询了可用的队列家族,现在还需要指定要创建哪些队列。即使需求不同,你也可以从同一个物理设备创建多个逻辑设备。
首先,在类中添加一个新成员变量来存储逻辑设备句柄:
VkDevice device;
接下来,添加一个 createLogicalDevice 函数,并在 initVulkan 中调用它:
void initVulkan(){
createInstance();
setupDebugMessenger();
pickPhysicalDevice();
createLogicalDevice();
}
void createLogicalDevice(){
}
💡 术语解释:逻辑设备 (Logical Device)
逻辑设备是应用程序与物理GPU设备之间的软件抽象层。可以这样理解:物理设备是你的实际显卡硬件,而逻辑设备是你程序用来"对话"的软件接口。就像你使用手机时,硬件是物理手机,而操作系统是逻辑接口一样。逻辑设备允许你指定需要哪些功能(如图形渲染、计算等)以及如何组织这些功能,而不需要关心底层硬件的具体实现细节。一个物理设备可以创建多个逻辑设备,每个逻辑设备可以有不同的配置。
指定要创建的队列
创建逻辑设备需要在结构体中指定许多细节,其中第一个是 VkDeviceQueueCreateInfo。这个结构体描述了我们想要为单个队列家族创建的队列数量。目前,我们只对具有图形能力的队列感兴趣。
QueueFamilyIndices indices = findQueueFamilies(physicalDevice);
VkDeviceQueueCreateInfo queueCreateInfo{};
queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
queueCreateInfo.queueFamilyIndex = indices.graphicsFamily.value();
queueCreateInfo.queueCount = 1;
当前可用的驱动程序只允许你为每个队列家族创建少量队列,而且你实际上不需要超过一个队列。这是因为你可以在多个线程上创建所有的命令缓冲区,然后在主线程上通过一次低开销的调用来提交它们。
Vulkan允许你为队列分配优先级(使用0.0到1.0之间的浮点数),以影响命令缓冲区执行的调度。即使只有一个队列,这也是必需的:
float queuePriority = 1.0f;
queueCreateInfo.pQueuePriorities = &queuePriority;
指定使用的设备特性
接下来需要指定的是我们将要使用的设备特性集合。这些特性正是我们在上一章中通过 vkGetPhysicalDeviceFeatures 查询支持的特性,比如几何着色器。目前我们不需要任何特殊功能,因此可以简单定义并将所有内容设置为 VK_FALSE。当我们开始使用Vulkan进行更有趣的任务时,我们会回到这个结构体。
VkPhysicalDeviceFeatures deviceFeatures{};
创建逻辑设备
有了前面两个结构体,我们可以开始填充主 VkDeviceCreateInfo 结构体了。
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 字段。不过,为了与旧实现兼容,设置它们仍然是个好主意:
createInfo.enabledExtensionCount = 0;
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 函数销毁:
void cleanup(){
vkDestroyDevice(device, nullptr);
...
}
逻辑设备不直接与实例交互,这就是为什么实例没有作为参数包含在内。
获取队列句柄
队列在创建逻辑设备时会自动创建,但我们还没有用于与它们交互的句柄。首先,添加一个类成员变量来存储图形队列的句柄:
VkQueue graphicsQueue;
当设备被销毁时,设备队列会隐式清理,因此我们不需要在 cleanup 中做任何事情。
我们可以使用 vkGetDeviceQueue 函数来检索每个队列家族的队列句柄。参数分别是:逻辑设备、队列家族、队列索引和一个用于存储队列句柄的变量指针。因为我们只从这个家族创建了一个队列,所以只需使用索引 0。
vkGetDeviceQueue(device, indices.graphicsFamily.value(), 0, &graphicsQueue);
有了逻辑设备和队列句柄,我们现在就可以真正开始使用显卡来完成工作了!在接下来的几章中,我们将设置资源,以便将结果呈现到窗口系统。
[C++ 代码]https://vulkan-tutorial.com/code/04_logical_device.cpp