本部分结果可参考 09_SwapChain

在 Vulkan 中必须显式的创建 Swap Chain。SwapChain 是与 Surface 绑定的数据结构,其包含了多个 Image,应用渲染时会将渲染的结果放置到这些 Image 中,当调用 Present 时,SwapChain 会将这些 Image 通过其与 Surface 绑定,传递给 Surface,Surface 再将这些 Image 显示到平台的窗口或屏幕上。

关于 Swap Chain 中的 queue 如何工作,以及何时将 queue 中的 Image present 到屏幕上,都可以在创建 Swap Chain 时配置。

创建类 SwapChainMgr 来管理 Swap Chain 的创建,销毁和关键的数据,其定义如下,在本节的后续部分将逐步实现这些函数:

``cpp
class SwapChainMgr
{
public:
static SwapChainSupportDetails querySwapChainSupport(VkPhysicalDevice device);
static void createSwapChain();
static void destroySwapChain();
static VkSwapchainKHR swapChain;
static std::vector images;
static VkFormat imageFormat;
static VkExtent2D imageExtent;

private:
static VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector& availableFormats);
static VkPresentModeKHR chooseSwapPresentMode(const std::vector& availablePresentModes);
static VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities);
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 检查 Physical Device 是否支持 Swap Chain

并不是所有的显卡都支持将 Image 表现到屏幕上,例如一些服务器的显卡,并不包含有任何的 Display 输出功能。 同时因为 Image 的 Presentation 与操作系统的 Window System 强相关,所以 Swap Chain 并非是 Vulkan Core 的部分,所以要使用 SwapChain,必须启用 Device Extension `VK_KHR_swapchain`。

在 `PhysicalDeviceMgr` 新增函数 `checkDeviceExtensionsSupport` 来检查设备是否支持 `VK_KHR_swapchain`:

```cpp
bool PhysicalDevicesMgr::checkDeviceExtensionsSupport(VkPhysicalDevice device)
{
uint32_t extensionCount;
vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount, nullptr);

std::vector<VkExtensionProperties> availableExtensions(extensionCount);
vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount, availableExtensions.data());

std::set<std::string> requiredExtensions(deviceExtensions.begin(), deviceExtensions.end());

for (const auto& extension : availableExtensions)
requiredExtensions.erase(extension.extensionName);

return requiredExtensions.empty();
}

其中 deviceExtensions 定义如下,其中的 ``VK_KHR_SWAPCHAIN_EXTENSION_NAME为 Vulkan 头文件中用来表示 VK_KHR_swapchain` 的宏:

1
const std::vector<const char*> deviceExtensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME };

PhysicalDeviceMgr::isDeviceSuitable 函数中需要添加对 checkDeviceExtensionsSupport 的调用:

1
2
3
4
5
6
7
bool PhysicalDevicesMgr::isDeviceSuitable(VkPhysicalDevice device)
{
// ...
bool extensionsSupported = checkDeviceExtensionsSupport(device);

return deviceSuitable && queueFamilySuitable && extensionsSupported;
}

#启用 Device Extension

一旦 Physical Device 支持 SwapChain 拓展,我们就需要在创建 Logical Device 时需要指明 Extensions 的数目和类型:

1
2
3
4
5
6
7
void LogicDevicesMgr::createLogicalDevice()
{
// ...
createInfo.enabledExtensionCount = static_cast<uint32_t>(PhysicalDevicesMgr::deviceExtensions.size());
createInfo.ppEnabledExtensionNames = PhysicalDevicesMgr::deviceExtensions.data();
// ...
}

#查询更多关于 SwapChain 的细节

在确保了 Physical Device 支持 Swap Chain 后,还需要查询更多关于 Swap Chain 的信息。因为不同的设备可能支持不同的 Surface 格式,Presentation 模式等。这些信息在后续创建 Swap Chain 时会用到。

需要查询的更多的关于 swap chain 的信息,基本由三种信息需要检查:

  • 与 Surface 的兼容性:如 images 的最小,最大数量,Image 的最小最大宽度和高度
  • Surface 的格式:如纹理的格式,Color Space
  • 可选的 Presentation 模式

首先定义一个结构体存储上述需要的所有信息:

1
2
3
4
5
6
struct SwapChainSupportDetails
{
VkSurfaceCapabilitiesKHR capabilities;
std::vector<VkSurfaceFormatKHR> formats;
std::vector<VkPresentModeKHR> presentModes;
};

实现函数 querySwapChainSupport 生成上述结构体,其中调用了一系列的 vkGetPhysicalDeviceSurfaceXXX 函数来获取 Surface 的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
SwapChainSupportDetails SwapChainMgr::querySwapChainSupport(VkPhysicalDevice device)
{
SwapChainSupportDetails details;

vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, SurfaceMgr::surface, &details.capabilities);

uint32_t formatCount;
vkGetPhysicalDeviceSurfaceFormatsKHR(device, SurfaceMgr::surface, &formatCount, nullptr);
if (formatCount != 0)
{
details.formats.resize(formatCount);
vkGetPhysicalDeviceSurfaceFormatsKHR(device, SurfaceMgr::surface, &formatCount, details.formats.data());
}

uint32_t presentModeCount;
vkGetPhysicalDeviceSurfacePresentModesKHR(device, SurfaceMgr::surface, &presentModeCount, nullptr);
if (presentModeCount != 0)
{
details.presentModes.resize(presentModeCount);
vkGetPhysicalDeviceSurfacePresentModesKHR(device, SurfaceMgr::surface, &presentModeCount, details.presentModes.data());
}

return details;
}

再次修改函数 PhysicalDevicesMgr::isDeviceSuitable,在支持的 Image Format 和 Presentation Mode 都不为空时才认为设备合适:

1
2
3
4
5
6
7
8
9
10
11
12
13
bool PhysicalDevicesMgr::isDeviceSuitable(VkPhysicalDevice device)
{
// ...

bool swapChainAdequate = false;
if (extensionSupport)
{
SwapChainSupportDetails swapChainSupport = SwapChainMgr::querySwapChainSupport(device);
swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.presentModes.empty();
}

return deviceSuitable && queueFamilySuitable && extensionsSupported && swapChainAdequate;
}

#为 SwapChain 选择正确的设置

在确认 Swap Chain 可以获取后,还需要一个函数为 SwapChain 选择最合适的设置,不同的设置适合不同的场景,也可能带来不同的性能表现。Swap chain 主要有三种类型数据需要设置:

  • Surface Format:如颜色 / 深度缓冲
  • Presentation Mode:如交换 Image 到屏幕上的条件
  • Swap Extent:Swap Chain 图片的分辨率

对于上述每一种数据类型,都会尝试找寻最合适的值,如果该值无法获取即寻找次优的值,这三种数据类型分别在 SwapChainMgr 的三个 chooseSwapSurfaceFormatchooseSwapPresentModechooseSwapExtent 函数中实现。

#Surface format

定义函数 chooseSwapSurfaceFormat 找寻最合适的 Surface format:

1
2
3
4
5
6
7
8
9
10
VkSurfaceFormatKHR SwapChainMgr::chooseSwapSurfaceFormat(const std::vector<VkSurfaceFormatKHR>& availableFormats)
{
for (const auto& availableFormat : availableFormats)
{
if (availableFormat.format == VK_FORMAT_B8G8R8A8_SRGB && availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR)
return availableFormat;
}

return availableFormats[0];
}

#Presentation mode

在 Vulkan 中一共有如下四种可能的 Present 模式:

  • VK_PRESENT_MODE_IMMEDIATE_KHR:当应用提交了一个画面后,马上将画面显示到屏幕上。该模式可能造成画面撕裂。
  • VK_PRESENT_MODE_FIFO_KHR:当设备的 VSync 到来后,从 queue 中获取一个画面显式到屏幕上。当应用提交画面时,放置到 queue 的末尾。如果应用提交画面时,发现 queue 已满,则会阻塞应用。如果 VSync 到来但是 Queue 为空时,则会等待下一个 VSync 重新尝试获取。
  • Vk_PRESENT_MODE_FIFI_RELAXED_KHR:与 VK_PRESENT_MODE_FIFO_KHR 类似,只不过在 VSync 到来但 Queue 为空后,不会再次等到设备 VSync 时做第二次查询,而是当新的图像一到来就刷新到屏幕上
  • VK_PRESENT_MODE_MAILBOX_KHR:与 VK_PRESENT_MODE_FIFO_KHR 类似,只不过当应用提交画面且 Queue 已满时,不再阻塞应用提交画面,而是会用新画面取代 Queue 中最老的画面。

在设备支持 Swap Chain 后,VK_PRESENT_MODE_FIFO_KHR 模式可保证必然支持。但这里选择使用 VK_PRESENT_MODE_MAILBOX_KHR 模式,避免应用意外的阻塞,仅当 VK_PRESENT_MODE_MAILBOX_KHR 不存在时再使用 VK_PRESENT_MODE_FIFO_KHR

封装函数 chooseSwapPresentMode 选择最合适的 Present Mode:

1
2
3
4
5
6
7
8
9
10
VkPresentModeKHR SwapChainMgr::chooseSwapPresentMode(const std::vector<VkPresentModeKHR>& availablePresentModes)
{
for (const auto& availablePresentMode : availablePresentModes)
{
if (availablePresentMode == VK_PRESENT_MODE_MAILBOX_KHR)
return availablePresentMode;
}

return VK_PRESENT_MODE_FIFO_KHR;
}

#Swap extent

最后一个需要设置的属性是 Swap Extent,该属性表示 Swap Chain Image 的分辨率,Swap Extent 的上下限在 VkSurfaceCapabilitiesKHR 结构体中定义。

Swap Extent 分辨率几乎一直和屏幕的分辨率相同,如果平台的 Window Managers 允许开发者定义与屏幕不相同的分辨率,那么会将 VkSurfaceCapabilitiesHKR.current 的宽高都设定为 UINT32_MAX,反之 VkSurfaceCapabilitiesHKR.current 即为即为屏幕分辨率。

定义函数 chooseSwapExtent 在允许的情况下,将 Swap Extent 设定为之前窗口的宽高:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
VkExtent2D SwapChainMgr::chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities)
{
if (capabilities.currentExtent.width != std::numeric_limits<uint32_t>::max())
return capabilities.currentExtent;

int width, height;
glfwGetFramebufferSize(HelloTriangleApplication::window, &width, &height);

VkExtent2D actualExtent = {static_cast<uint32_t>(width), static_cast<uint32_t>(height)};

actualExtent.width = std::max(capabilities.minImageExtent.width, std::min(capabilities.maxImageExtent.width, actualExtent.width));
actualExtent.height = std::max(capabilities.minImageExtent.height, std::min(capabilities.maxImageExtent.height, actualExtent.height));

return actualExtent;
}

#创建 SwapChain

initVulkan 函数中,添加对 createSwapChain 的调用:

1
2
3
4
5
void HelloTriangleApplication::initVulkan()
{
// ...
SwapChainMgr::createSwapChain();
}

完整的 SwapChainMgr::createSwapChain 函数为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
void SwapChainMgr::createSwapChain()
{
SwapChainSupportDetails swapChainSupport = querySwapChainSupport(PhysicalDevicesMgr::physicalDevice);
VkSurfaceFormatKHR surfaceFormat = chooseSwapSurfaceFormat(swapChainSupport.formats);
VkPresentModeKHR presentMode = chooseSwapPresentMode(swapChainSupport.presentModes);
VkExtent2D extent = chooseSwapExtent(swapChainSupport.capabilities);

uint32_t imageCount = swapChainSupport.capabilities.minImageCount + 1;

if (swapChainSupport.capabilities.maxImageCount > 0 && imageCount > swapChainSupport.capabilities.maxImageCount)
imageCount = swapChainSupport.capabilities.maxImageCount;

VkSwapchainCreateInfoKHR createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
createInfo.surface = SurfaceMgr::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;

QueueFamilyIndices indices = QueueFamilyMgr::findQueueFamilies(PhysicalDevicesMgr::physicalDevice);
uint32_t queueFamilyIndices[] = {indices.graphicsFamily.value(), indices.presentFamily.value()};
if (indices.graphicsFamily == indices.presentFamily)
{
createInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;

}
else
{
createInfo.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
createInfo.queueFamilyIndexCount = 2;
createInfo.pQueueFamilyIndices = queueFamilyIndices;
}

createInfo.preTransform = swapChainSupport.capabilities.currentTransform;
createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
createInfo.presentMode = presentMode;
createInfo.clipped = VK_TRUE;
createInfo.oldSwapchain = VK_NULL_HANDLE;

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

vkGetSwapchainImagesKHR(LogicDevicesMgr::device, swapChain, &imageCount, nullptr);
images.resize(imageCount);
vkGetSwapchainImagesKHR(LogicDevicesMgr::device, swapChain, &imageCount, images.data());
imageFormat = surfaceFormat.format;
imageExtent = extent;
}

上述函数主要的工作还是装填 VkSwapchainCreateInfoKHR 数据,并最终调用 vkCreateSwapchainKHR 创建出需要的 swapchain 对象。

在 SwapChain 创建完成后,调用 vkGetSwapchainImagesKHR 获取 SwapChain 中的 Image 数量和数据,并将 Image 的 Extent 和 Format 存储下来,即完整函数实现中的如下部分:

1
2
3
vkGetSwapchainImagesKHR(device, swapchain, &imageCount, nullptr);
swapChainImages.resize(imageCount);
vkGetSwapchainImagesKHR(device, swapchain, &imageCount, swapChainImages.data());

当程序结束时,也应当清理 SwapChain,因为创建 SwapChain 时依赖 device,因此销毁时 SwapChain 也必须在 Device 被销毁前销毁:

1
2
3
4
5
6
7
8
9
10
void HelloTriangleApplication::cleanup()
{
SwapChainMgr::destroySwapChain();
// ...
}

void SwapChainMgr::destroySwapChain()
{
vkDestroySwapchainKHR(LogicDevicesMgr::device, swapChain, nullptr);
}

createInfosurfaceFormatpresentModeextent 都是在 Choosing the right settings for the swap chain 中获取的数据。

createInfo 中 剩下还有的比较重要的参数:

#minImageCount

minImageCount 是 swapChain 中最小需要使用到的纹理数量,该数量可以通过 swapChainSupport.capabilities.minImageCount 获得。但如果直接将 swapChainSupport.capabilities.minImageCount 的数量赋值给 createInfo.minImageCount 会在某些时刻导致程序必须等待驱动完成一些操作后,才能去拿取下一张图片。

因此这里通常会取 swapChainSupport.capabilities.minImageCount + 1,且需要确认 + 1 后不会超过 swapChainSupport.capabilities.maxImageCount

#imageArrayLayers

createInfo.imageArrayLayers 指定了每个 SwapChain 中 image 由几层 layer 构成。通常这个参数永远是 1,除非需要 XR 的应用。

#imageUsage

createInfo.imageUsage 是指定 SwapChain 中 Image 的作用,因为这里仅需要将 swapChain 作为 Color Attachment,因此设定为 VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT

#ImageSharingMode

createrInfo.ImageSharingMode 指定了 SwapChain 中的 image 该如何在多个 Queue families 中处理。

ImageSharingMode 可以指定两种模式:

  • VK_SHARING_MODE_EXCLUSIVE :Image 在同一时刻只会被一个 Queue Family 拥有,且另一个 Queue Family 需要处理该 Image 时,需要显示的转移拥有权。该模式下拥有最好的性能
  • VK_SHARING_MODE_CONCURRENT:当不同的 Queue Families 使用同一个 Image 时,不需要显式的转移拥有权。

在这里,会需要首先判断之前获取的 Queue Families( graphicsFamilypresentFamily )是否是同一个 Queue Family。如果是的话,使用 VK_SHARING_MODE_EXCLUSIVE 获取更好的性能,否则使用 VK_SHARING_MODE_CONCURRENT 因为目前还不需要显示的控制 Image 拥有权。

之所以 VK_SHARING_MODE_EXCLUSIVE 性能更好,是因为当资源处于独占模式时,GPU 驱动可以优化访问,而如果是 VK_SHARING_MODE_CONCURRENT 共享模式,GPU 必须有同步机制确保多个 Queue Family 访问同一个 Image 时不会发生冲突。

#preTransform

preTransform 可以指定设置在 SwapChain Image 的 Transform 变化,如顺时针旋转 90 °。

如果不需要这种变化的话,简单设置为 swapChainSupport.capabilities.currentTransform 就好。

#compositeAlpha

compositeAlpha 指定了有多个 Window 叠加时,该如何处理混合模式。通常而言当前的 Window 并不需要与背后的 Window 进行混合,因此可以设为 VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR

#clipped

clipped 表示当一部分像素无法显示时(如被其他窗口遮挡),是否需要舍弃这些像素。通常而言,除非时真的需要依赖所有像素计算某些数据,否则为了更好的性能,都应该设置为 VK_TRUE

#oldSwapchain

在 Vulkan 程序的整个生命周期内,可能 SwapChain 会变得不合法或者出现了性能问题,此时 Vulkan 是支持重新创建 SwapChain 的,当创建时,需要通过 oldSwapchain 指定上一个使用 SwapChain,这里即为 VK_NULL_HANDLE