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);

下一章开始学习交换链。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 共1条
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片