Vulkan之Swapchain

交换链也是一个不太好理解的概念。

交换链本质上也是一个队列,这个队列中存放着等待present的图像。程序从交换链上获取图像进行渲染,然后再将渲染后的图像返回给交换链,等待屏幕去显示。有点类似于生产者和消费者的模式。所以我们可以看到swapchain的地位是很关键的。

1)查询swapchain的扩展支持

不是所有的显卡都支持swapchain,如果显卡只是用来计算,那就不需要swapchain,只有需要显示的时候,才需要查看显卡是否支持swapchain扩展。这个在我们选择物理设备的时候设置的。

1
2
3
4
5
6
7
8
9
10
11
const std::vector<const char*> deviceExtensions = {
VK_KHR_SWAPCHAIN_EXTENSION_NAME
};

bool extensionsSupported = checkDeviceExtensionSupport(device);

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

2) 创建交换链

可以用下面的命令创建交换链

1
2
3
4
5
VKAPI_ATTR VkResult VKAPI_CALL vkCreateSwapchainKHR(
VkDevice device,
const VkSwapchainCreateInfoKHR* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkSwapchainKHR* pSwapchain);

device是逻辑设备
pCreateInfo 是创建所需的结构体的指针
pAllocator 一般为NULL
pSwapchain就是生成的交换链

而VkSwapchainCreateInfoKHR这个结构体的内容可是很多的啊。在看这个结构之前,我们先看下swapchain的三个基本的属性:

  • a)capabilities 容量(图像数量,最大/最小图像的宽高等)
  • b)formats 格式(像素空间,颜色空间等信息)
  • c)presentModes 呈现模式
    1
    2
    3
    4
    5
    struct SwapChainSupportDetails {
    VkSurfaceCapabilitiesKHR capabilities;
    std::vector<VkSurfaceFormatKHR> formats;
    std::vector<VkPresentModeKHR> presentModes;
    };

这里一般我们首先需要获取device支持的一些属性,然后用这些属性创建交换链

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

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

uint32_t formatCount;
vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, nullptr);

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

uint32_t presentModeCount;
vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, nullptr);

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

return details;
}

上面的代码实际上就是指定的函数获取物理设备支持的三个属性(我们可以从函数中看出,这依赖于device和surface)

当我们获取到了属性后,还需要选择合适的属性用来创建swapchain

容量信息主要有下面的内容

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct VkSurfaceCapabilitiesKHR {
uint32_t minImageCount;
uint32_t maxImageCount;
VkExtent2D currentExtent;
VkExtent2D minImageExtent;
VkExtent2D maxImageExtent;
uint32_t maxImageArrayLayers;
VkSurfaceTransformFlagsKHR supportedTransforms;
VkSurfaceTransformFlagBitsKHR currentTransform;
VkCompositeAlphaFlagsKHR supportedCompositeAlpha;
VkImageUsageFlags supportedUsageFlags;
} VkSurfaceCapabilitiesKHR;

然而其中主要的就是交换链中的图像的宽高,以及交换链支持的最大的图像的个数

1
2
3
4
5
6
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;
}

格式主要是像素的格式以及颜色空间,这个有很多的格式,每个格式可能后续会占用不同的资源和内存,需要评估使用

1
2
3
4
typedef struct VkSurfaceFormatKHR {
VkFormat format;
VkColorSpaceKHR colorSpace;
} VkSurfaceFormatKHR;

1
2
3
4
5
6
7
8
9
VkSurfaceFormatKHR 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];
}

展示模式有如下的几种模式

1
2
3
4
5
6
7
8
9
typedef enum VkPresentModeKHR {
VK_PRESENT_MODE_IMMEDIATE_KHR = 0, //立即渲染,会造成撕裂的效果
VK_PRESENT_MODE_MAILBOX_KHR = 1, //三倍缓冲的模式
VK_PRESENT_MODE_FIFO_KHR = 2, //先进先出对列渲染
VK_PRESENT_MODE_FIFO_RELAXED_KHR = 3,
VK_PRESENT_MODE_SHARED_DEMAND_REFRESH_KHR = 1000111000,
VK_PRESENT_MODE_SHARED_CONTINUOUS_REFRESH_KHR = 1000111001,
VK_PRESENT_MODE_MAX_ENUM_KHR = 0x7FFFFFFF
} VkPresentModeKHR;

• VK_PRESENT_MODE_IMMEDIATE_KHR:应用程序提交的图像会被立即传输到屏幕上,可能会导致撕裂现象。
• VK_PRESENT_MODE_MAILBOX_KHR:这一模式是第二种模式的另一个变种。它不会在交换链的队列满时阻塞应用程序,队列中的图像会被直接替换为应用程序新提交的图像。这一模式可以用来实现三倍缓冲,避免撕裂现象的同时减小了延迟问题。
• VK_PRESENT_MODE_FIFO_KHR:交换链变成一个先进先出的队列,每次从队列头部取出一张图像进行显示,应用程序渲染的图像提交给交换链后,会被放在队列尾部。当队列为满时,应用程序需要进行等待。这一模式非常类似现在常用的垂直同步。刷新显示的时刻也被叫做垂直回扫。
• VK_PRESENT_MODE_FIFO_RELAXED_KHR:这一模式和上一模式的唯一区别是,如果应用程序延迟,导致交换链的队列在上一次垂直回扫时为空,那么,如果应用程序在下一次垂直回扫前提交图像,图像会立即被显示。这一模式可能会导致撕裂现象。

所以实际上最好的模式应该是VK_PRESENT_MODE_MAILBOX_KHR ,然后是VK_PRESENT_MODE_FIFO_KHR 。当然我们需要检查显卡支不支持。

1
2
3
4
5
6
7
8
9
10
vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, details.presentModes.data());

VkPresentModeKHR 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;
}

有了上面的信息,我们就可以构建VkSwapchainCreateInfoKHR结构了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct VkSwapchainCreateInfoKHR {
VkStructureType sType; //VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR
const void* pNext; //扩展
VkSwapchainCreateFlagsKHR flags; //遗留
VkSurfaceKHR surface; //创建的window窗口界面
uint32_t minImageCount; // swapchain中的图像个数
VkFormat imageFormat; // 颜色格式
VkColorSpaceKHR imageColorSpace; // 颜色空间
VkExtent2D imageExtent; // 支持的图像大小
uint32_t imageArrayLayers; // 每个图像组成的层数。除非我们开发3D应用程序,否则始终为1
VkImageUsageFlags imageUsage; //图像的用途,一般是用作颜色附着VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT
VkSharingMode imageSharingMode; // 图像是否可以被多个队列簇访问,有两种模式
uint32_t queueFamilyIndexCount;
const uint32_t* pQueueFamilyIndices; // 队列簇索引
VkSurfaceTransformFlagBitsKHR preTransform; //为交换链图像指定某些转换逻辑,比如90度顺时针旋转或者水平反转。如果不需要任何transoform操作,可以简单的设置为currentTransoform。
VkCompositeAlphaFlagBitsKHR compositeAlpha; // 指定alpha通道是否应用与与其他的窗体系统进行混合操作
VkPresentModeKHR presentMode; // 呈现模式
VkBool32 clipped; // clipped成员设置为VK_TRUE,意味着我们不关心被遮蔽的像素数据
VkSwapchainKHR oldSwapchain; //比较复杂,假设我们这里只是创建一个交换链的话,则设置VK_NULL_HANDLE
} VkSwapchainCreateInfoKHR;

创建好了交换链后,由于我们没有办法直接操作swapchain, 所以我们需要获取swapchain图像的句柄,如下所示:

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

而实际上我们获取到swapChainImage后是没有直接使用的,一般我们使用的是其图像视图,因此还要创建swapChainImageView

1
2
3
4
5
6
7
void createImageViews() {
swapChainImageViews.resize(swapChainImages.size());

for (uint32_t i = 0; i < swapChainImages.size(); i++) {
swapChainImageViews[i] = createImageView(swapChainImages[i], swapChainImageFormat, VK_IMAGE_ASPECT_COLOR_BIT, 1);
}
}

3) 重建交换链

从上面的流程看,我们的交换链实际上是跟surface的大小有关的,这也就意味着当我们的窗口surface大小改变时时需要重新创建swapchain的,这点时需要注意的。

显示 Gitment 评论