选取了物理设备之后,我们需要创建一个逻辑设备来跟它进行交互。逻辑设备的创建过程跟实例的创建过程类似,我们需要描述要用到的特性。我们还需要根据之前查询好的队列族,指定需要创建的队列。我们甚至可以对于同一个物理设备创建多个逻辑设备来应对不同的需求

创建逻辑设备

同样的,让我们添加一个成员变量

VkDevice device;

然后添加一个 createLogicalDevice 成员函数,在 initVulkan 里调用它

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

void createLogicalDevice() {

}

接着我们就可以开始填充这个成员函数。创建逻辑设备需要我们在结构体里指定很多的细节

第一个就是 VkDeviceQueueCreateInfo,这个结构体描述了对于一个队列族我们需要创建的队列数量以及各自的优先级

uint32_t queueFamilyIndex = findQueueFamily(physicalDevice).value();
VkDeviceQueueCreateInfo queueCreateInfo = {};
queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
queueCreateInfo.queueFamilyIndex = queueFamilyIndex;
queueCreateInfo.queueCount = 1;
float queuePriority = 1.0f;
queueCreateInfo.pQueuePriorities = &queuePriority;

对于一个队列族,目前可用的驱动只允许我们创建少量的队列,并且我们也不会真的遇到多于一个的情况。因为我们可以为每个线程创建命令缓冲区,然后在主线程中用一个低开销的调用把它们全部提交

Vulkan 允许我们为队列指定优先级,这会影响命令缓冲区执行规划,优先级由一个零到一的浮点数指定。即使只有一个队列我们也需要为它指定优先级

接下来我们需要指定设备的特性。我们可以通过 vkGetPhysicalDeviceFeatures 来查询物理设备的特性支持,比如几何着色器。但现在我们不需要任何特性,所以我们就直接简单定义它并把所有的值设为零

VkPhysicalDeviceFeatures deviceFeatures = {};

然后我们就可以开始填充逻辑设备的创建信息并通过它来创建逻辑设备了

VkDeviceCreateInfo createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
createInfo.queueCreateInfoCount = 1;
createInfo.pQueueCreateInfos = &queueCreateInfo;
createInfo.pEnabledFeatures = &deviceFeatures;
const std::vector<const char*> extensionNames = {VK_KHR_SWAPCHAIN_EXTENSION_NAME};
createInfo.enabledExtensionCount = static_cast<uint32_t>(extensionNames.size());
createInfo.ppEnabledExtensionNames = extensionNames.data();

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

可以看到创建逻辑设备主要是把上面的队列创建信息和设备特性传进去,最后指定一下逻辑设备需要启用的扩展,这里传入了 VK_KHR_swapchain 来让我们能够使用交换链,交换链将会在后面的文章中介绍

取回队列句柄

创建逻辑设备的同时会根据传入的队列创建信息创建相应的队列,所以我们需要取回对队列句柄

还是先添加一个成员变量

VkQueue graphicsAndPresentQueue;

然后在创建逻辑设备之后添加如下代码

vkGetDeviceQueue(device, queueFamilyIndex, 0, &graphicsAndPresentQueue)

可以使用 vkGetDeviceQueue 函数对每个队列族取回队列句柄,参数依次分别是逻辑设备、队列族索引、队列索引、用于存储队列句柄的变量的指针,然后因为我们的这个队列族只有一个队列,所以直接使用零作为队列索引

销毁逻辑设备

void cleanup() {
    vkDestroyDevice(device, nullptr);
    vkDestroySurfaceKHR(instance, surface, nullptr);
    vkDestroyInstance(instance, nullptr);

    glfwDestroyWindow(window);
    glfwTerminate();
}