创建交换链

现在,我们已经编写了大量辅助函数帮助我们在应用程序运行时选择最合适的设置,可以开始进行交换链的创建了。

我们添加一个叫做createSwapChain的函数,它会选择合适的交换链设置,然后,我们在initVulkan函数中在逻辑设备创建之后调用它:

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

void createSwapChain() {
    SwapChainSupportDetails swapChainSupport = querySwapChainSupport(physicalDevice);

    VkSurfaceFormatKHR surfaceFormat = chooseSwapSurfaceFormat(swapChainSupport.formats);
    VkPresentModeKHR presentMode = chooseSwapPresentMode(swapChainSupport.presentModes);
    VkExtent2D extent = chooseSwapExtent(swapChainSupport.capabilities);
}

除了上面这些,还有一些地方需要我们进行设置,但这些设置都很简单,没有必要为它们编写独立的设置函数。这些设置包括交换链中的图像个数,也就是交换链的队列可以容纳的图像个数。我们使用交换链支持的最小图像个数+1数量的图像来实现三倍缓冲:

uint32_t imageCount = swapChainSupport.capabilities.minImageCount + 1;
if (swapChainSupport.capabilities.maxImageCount > 0 && imageCount > swapChainSupport.capabilities.maxImageCount) {
    imageCount = swapChainSupport.capabilities.maxImageCount;
}

maxImageCount的值为0表明,只要内存可以满足,我们可以使用任意数量的图像。

和其它Vulkan对象相同,创建交换链对象需要填写一个包含大量信息的结构体。这一结构体的一些成员我们已经非常熟悉:

VkSwapchainCreateInfoKHR createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
createInfo.surface = surface;

指定交换链绑定的表面后,我们还需要指定有关交换链图像的信息:

createInfo.minImageCount = imageCount;
createInfo.imageFormat = surfaceFormat.format;
createInfo.imageColorSpace = surfaceFormat.colorSpace;
createInfo.imageExtent = extent;
createInfo.imageArrayLayers = 1;
createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;

imageArrayLayers成员变量用于指定每个图像所包含的层次。通常,来说它的值为1。但对于VR相关的应用程序来说,会使用更多的层次。imageUsage成员变量用于指定我们将在图像上进行怎样的操作。在本教程,我们在图像上进行绘制操作,也就是将图像作为一个颜色附着来使用。如果读者需要对图像进行后期处理之类的操作,可以使用VK_IMAGE_USAGE_TRANSFER_DST_BIT作为imageUsage成员变量的值,让交换链图像可以作为传输的目的图像。

QueueFamilyIndices indices = findQueueFamilies(physicalDevice);
uint32_t queueFamilyIndices[] = {(uint32_t) indices.graphicsFamily, (uint32_t) indices.presentFamily};

if (indices.graphicsFamily != indices.presentFamily) {
    createInfo.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
    createInfo.queueFamilyIndexCount = 2;
    createInfo.pQueueFamilyIndices = queueFamilyIndices;
} else {
    createInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
    createInfo.queueFamilyIndexCount = 0; // Optional
    createInfo.pQueueFamilyIndices = nullptr; // Optional
}

接着,我们需要指定在多个队列族使用交换链图像的方式。这一设置对于图形队列和呈现队列不是同一个队列的情况有着很大影响。我们通过图形队列在交换链图像上进行绘制操作,然后将图像提交给呈现队列来显示。有两种控制在多个队列访问图像的方式:

  • VK_SHARING_MODE_EXCLUSIVE:一张图像同一时间只能被一个队列族所拥有,在另一队列族使用它之前,必须显式地改变图像所有权。这一模式下性能表现最佳。

  • VK_SHARING_MODE_CONCURRENT:图像可以在多个队列族间使用,不需要显式地改变图像所有权。

如果图形和呈现不是同一个队列族,我们使用协同模式来避免处理图像所有权问题。协同模式需要我们使用queueFamilyIndexCount和pQueueFamilyIndices来指定共享所有权的队列族。如果图形队列族和呈现队列族是同一个队列族(大部分情况下都是这样),我们就不能使用协同模式,协同模式需要我们指定至少两个不同的队列族。

createInfo.preTransform = swapChainSupport.capabilities.currentTransform;

我们可以为交换链中的图像指定一个固定的变换操作(需要交换链具有supportedTransforms特性),比如顺时针旋转90度或是水平翻转。如果读者不需要进行任何变换操作,指定使用currentTransform变换即可。

createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;

compositeAlpha成员变量用于指定alpha通道是否被用来和窗口系统中的其它窗口进行混合操作。通常,我们将其设置为VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR来忽略掉alpha通道。

createInfo.presentMode = presentMode;
createInfo.clipped = VK_TRUE;

presentMode成员变量用于设置呈现模式。clipped成员变量被设置为VK_TRUE表示我们不关心被窗口系统中的其它窗口遮挡的像素的颜色,这允许Vulkan采取一定的优化措施,但如果我们回读窗口的像素值就可能出现问题。

createInfo.oldSwapchain = VK_NULL_HANDLE;

最后是oldSwapchain成员变量,需要指定它,是因为应用程序在运行过程中交换链可能会失效。比如,改变窗口大小后,交换链需要重建,重建时需要之前的交换链,具体细节,我们会在之后的章节详细介绍。现在,我们还没有创建任何一个交换链,将它设置为VK_NULL_HANDLE即可。 添加一个VkSwapchainKHR成员变量来存储交换链:

VkSwapchainKHR swapChain;

调用vkCreateSwapchainKHR函数创建交换链:

if (vkCreateSwapchainKHR(device, &createInfo, nullptr, &swapChain) != VK_SUCCESS) {
    throw std::runtime_error("failed to create swap chain!");
}

vkCreateSwapchainKHR函数的参数依次是逻辑设备对象,交换链创建信息,可选的自定义内存分配器和用于存储返回的交换链对象的内存地址。接着,我们需要在cleanup函数中在逻辑设备被清除前调用vkDestroySwapchainKHR函数来清除交换链对象:

void cleanup() {
    vkDestroySwapchainKHR(device, swapChain, nullptr);
        ...
}

现在可以编译运行程序,确保我们成功地创建了交换链。如果vkCreateSwapchainKHR函数调用出现错误,那么,就移除createInfo.imageExtent=extent;这行代码,然后,启用校验层,编译运行程序,就可以捕获错误,得到一些有用地信息:

image