本部分结果可参考 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 的创建、销毁和关键数据,其定义如下,在本节的后续部分将逐步实现这些函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class SwapChainMgr
{
public:
static SwapChainSupportDetails querySwapChainSupport(VkPhysicalDevice device);
static void createSwapChain();
static void destroySwapChain();
static VkSwapchainKHR swapChain;
static std::vector<VkImage> images;
static VkFormat imageFormat;
static VkExtent2D imageExtent;

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

#检查 Physical Device 是否支持 Swap Chain

并不是所有的显卡都支持将 Image 呈现到屏幕上,例如一些服务器的显卡,并不包含有任何的 Display 输出功能。同时因为 Image 的 Presentation 与操作系统的 Window System 强相关,所以 Swap Chain 并非 Vulkan Core 的一部分,而是通过 device extension VK_KHR_swapchain 提供的标准扩展。要使用 SwapChain,必须启用该扩展。

PhysicalDevicesMgr 新增函数 checkDeviceExtensionsSupport 来检查设备是否支持 VK_KHR_swapchain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 };

PhysicalDevicesMgr::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 的数量和类型:

TD
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    VkInstance
VkPhysicalDevice
VkSurfaceKHR
VkSwapchainCreateInfoKHR
VkSwapchainKHR
VkImage

VkInstance -->|vkEnumeratePhysicalDevices| VkPhysicalDevice
VkPhysicalDevice -->|vkGetPhysicalDeviceSurfaceSupportKHR| VkSurfaceKHR
VkSurfaceKHR -->|vkGetPhysicalDeviceSurfaceCapabilitiesKHR| VkSwapchainCreateInfoKHR
VkSurfaceKHR -->|vkGetPhysicalDeviceSurfaceFormatsKHR| VkSwapchainCreateInfoKHR
VkSurfaceKHR -->|vkGetPhysicalDeviceSurfacePresentModesKHR| VkSwapchainCreateInfoKHR
VkSwapchainCreateInfoKHR -->|vkCreateSwapchainKHR| VkSwapchainKHR
VkSwapchainKHR -->|vkGetSwapchainImagesKHR| VkImagep
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 的兼容性:如 image 的最小/最大数量,image 的最小/最大宽度和高度
  • Surface 的格式:如颜色格式和色彩空间
  • 可选的 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 (extensionsSupported)
{
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 重新尝试获取。这是 Vulkan 标准要求所有实现必须支持的 present mode。
  • VK_PRESENT_MODE_FIFO_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 Manager 允许开发者定义与屏幕不相同的分辨率,那么会将 VkSurfaceCapabilitiesKHR.currentExtent 的宽高都设定为 UINT32_MAX,反之 currentExtent 即为屏幕分辨率。

定义函数 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; // 通常为 1,除非 stereoscopic rendering
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 都是在 为 SwapChain 选择正确的设置 中获取的数据。

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

#minImageCount

minImageCount 是 swapChain 中最小需要使用到的 image 数量,该数量可以通过 swapChainSupport.capabilities.minImageCount 获得。通常会取 swapChainSupport.capabilities.minImageCount + 1,这样可以实现 triple buffering,减少等待,提升性能。需要确认 +1 后不会超过 swapChainSupport.capabilities.maxImageCount

#imageArrayLayers

createInfo.imageArrayLayers 指定了每个 SwapChain image 由几层 layer 构成。对于普通 2D 渲染应为 1,只有在立体显示或多视图(如 VR/AR)应用中才会大于 1。

#imageUsage

createInfo.imageUsage 指定 SwapChain 中 Image 的用途。这里仅需要将 swapChain 作为 Color Attachment,因此设定为 VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT。如需支持后处理或读取 swapchain image,可添加 VK_IMAGE_USAGE_TRANSFER_SRC_BIT 等标志。

#imageSharingMode

createInfo.imageSharingMode 指定了 SwapChain 中的 image 如何在多个 Queue families 中处理。

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

通常推荐使用 EXCLUSIVE,除非确实需要 CONCURRENT。

之所以 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 可能会因窗口大小变化等原因需要重建。此时创建新的 SwapChain 时,通过 oldSwapchain 指定上一个 SwapChain,可以优化资源重用。首次创建时应为 VK_NULL_HANDLE