目录
Checking for swap chain support
Querying details of swap chain support
Choosing the right settings for the swap chain
Retrieving the swap chain images
Vulkan没有“默认帧缓冲区”的概念,因此它需要一个基础设施,在我们将缓冲区呈现在屏幕上之前,它将拥有这些缓冲区。这种基础设施被称为交换链,必须在Vulkan中明确创建。交换链本质上是一个等待显示在屏幕上的图像队列。我们的应用程序将获取这样的图像以绘制到其中,然后将其返回到队列中。队列的工作方式以及从队列中呈现图像的条件取决于交换链的设置方式,但交换链的一般目的是使图像的呈现与屏幕的刷新率同步。
Checking for swap chain support
由于各种原因,并非所有的图形卡都能够直接向屏幕显示图像,例如,因为它们是为服务器设计的,没有任何显示输出。其次,由于图像呈现与窗口系统和与窗口相关的表面紧密相关,因此它实际上不是Vulkan核心的一部分。在查询VK_KHR_swapchain设备扩展的支持后,必须启用它。
为此,我们将首先扩展isDeviceSuitable函数,以检查是否支持此扩展。我们之前已经看到了如何列出VkPhysicalDevice支持的扩展,所以这样做应该非常简单。请注意,Vulkan头文件提供了一个很好的宏VK_KHR_SWAPCHAIN_EXTENSION_NAME,该宏定义为VK_KHR_swapchain。使用此宏的优点是编译器将捕捉拼写错误。
首先声明所需设备扩展的列表,类似于要启用的验证层的列表。
const std::vector<const char*> deviceExtensions = {
VK_KHR_SWAPCHAIN_EXTENSION_NAME
};
接下来,创建一个新的函数checkDeviceExtensionSupport,该函数从isDeviceSuitable调用,作为附加检查:
bool isDeviceSuitable(VkPhysicalDevice device) {
QueueFamilyIndices indices = findQueueFamilies(device);
bool extensionsSupported = checkDeviceExtensionSupport(device);
return indices.isComplete() && extensionsSupported;
}
bool checkDeviceExtensionSupport(VkPhysicalDevice device) {
return true;
}
修改函数体以枚举扩展,并检查是否所有必需的扩展都在其中。
bool checkDeviceExtensionSupport(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();
}
我选择在这里使用一组字符串来表示未确认的所需扩展。这样,我们可以在列举可用扩展的序列时轻松地勾选它们。当然,您也可以使用checkValidationLayerSupport中的嵌套循环。性能差异无关紧要。现在运行代码并验证您的图形卡确实能够创建交换链。应该注意的是,正如我们在上一章中所检查的,演示队列的可用性意味着必须支持交换链扩展。然而,明确说明事情还是很好的,而且扩展必须明确启用。
Enabling device extensions
使用交换链需要先启用VK_KHR_swapchain扩展。启用扩展只需要对逻辑设备创建结构进行一点小小的更改:
createInfo.enabledExtensionCount = static_cast<uint32_t>(deviceExtensions.size());
createInfo.ppEnabledExtensionNames = deviceExtensions.data();
确保替换现有行createInfo.enabledExtensionCount=0;当你这样做时。
Querying details of swap chain support
仅仅检查交换链是否可用是不够的,因为它实际上可能与我们的窗口表面不兼容。创建交换链还涉及比实例和设备创建多得多的设置,因此我们需要在继续之前查询更多详细信息。
基本上有三种特性需要检查:
- 基本表面功能(交换链中的最小/最大图像数量、最小/最大宽度和图像高度)
- 曲面格式(像素格式、颜色空间)
- 可用的演示模式
与findQueueFamilies类似,我们将在查询完这些细节后使用结构传递这些细节。上述三种类型的特性以以下结构和结构列表的形式出现:
struct SwapChainSupportDetails {
VkSurfaceCapabilitiesKHR capabilities;
std::vector<VkSurfaceFormatKHR> formats;
std::vector<VkPresentModeKHR> presentModes;
};
现在,我们将创建一个新函数querySwapChainSupport,它将填充此结构。
SwapChainSupportDetails querySwapChainSupport(VkPhysicalDevice device) {
SwapChainSupportDetails details;
return details;
}
本节介绍如何查询包含此信息的结构。下一节将讨论这些结构的含义及其包含的数据。
让我们从基本的表面功能开始。这些特性易于查询,并返回到单个VkSurfaceCapabilitiesKHR结构中。
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface, &details.capabilities);
此函数在确定支持的功能时考虑指定的VkPhysicalDevice和VkSurfaceKHR窗口表面。所有支持查询函数都将这两个参数作为第一个参数,因为它们是交换链的核心组件。
下一步是查询支持的曲面格式。因为这是一个结构列表,所以它遵循了熟悉的2个函数调用的惯例:
uint32_t formatCount;
vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, nullptr);
if (formatCount != 0) {
details.formats.resize(formatCount);
vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, details.formats.data());
}
确保调整矢量大小以容纳所有可用格式。最后,查询支持的表示模式与vkGetPhysicalDeviceSurfacePresentModesKHR的工作方式完全相同:
uint32_t presentModeCount;
vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, nullptr);
if (presentModeCount != 0) {
details.presentModes.resize(presentModeCount);
vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, details.presentModes.data());
}
所有的细节现在都在结构中,所以让我们再次扩展isDeviceSuitable以利用这个函数来验证交换链支持是否足够。如果给定我们的窗口表面,至少有一种受支持的图像格式和一种支持的演示模式,那么交换链支持对于本教程来说就足够了。
bool swapChainAdequate = false;
if (extensionsSupported) {
SwapChainSupportDetails swapChainSupport = querySwapChainSupport(device);
swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.presentModes.empty();
}
重要的是,我们只有在验证扩展可用后才尝试查询交换链支持。函数的最后一行更改为:
return indices.isComplete() && extensionsSupported && swapChainAdequate;
Choosing the right settings for the swap chain
如果swapChain充分条件得到满足,那么支持肯定是充分的,但可能仍有许多不同的优化模式。现在,我们将编写一些函数,以找到最佳交换链的正确设置。有三种类型的设置需要确定:
- 表面格式(颜色深度)
- 演示模式(将图像“交换”到屏幕的条件)
- 交换范围(交换链中图像的分辨率)
对于这些设置中的每一个,我们都会记住一个理想的值,如果它可用,我们会使用它,否则我们会创建一些逻辑来找到下一个最佳的设置。
Surface format
此设置的功能开始时是这样的。稍后我们将传递SwapChainSupportDetails结构的formats成员作为参数。
VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector<VkSurfaceFormatKHR>& availableFormats) {
}
每个VkSurfaceFormatKHR条目都包含一个format和一个colorSpace成员。format成员指定颜色通道和类型。例如,VK_FORMAT_B8G8R8A8_SRGB表示我们以8位无符号整数的顺序存储B、G、R和alpha通道,每个像素总共32位。colorSpace成员使用VK_COLOR_SPACE_SRGB_NONLINEAR_KHR标志指示是否支持SRGB颜色空间。请注意,在旧版本的规范中,此标志曾被称为VK_COLORSPACE_SRGB_NONLINEAR_KHR。
对于颜色空间,如果可用,我们将使用SRGB,因为它会产生更准确的感知颜色。它也是图像的标准颜色空间,就像我们稍后将使用的纹理一样。因此,我们还应该使用SRGB颜色格式,其中最常见的格式之一是VK_FORMAT_B8G8R8A8_SRGB。
让我们浏览一下列表,看看首选组合是否可用:
for (const auto& availableFormat : availableFormats) {
if (availableFormat.format == VK_FORMAT_B8G8R8A8_SRGB && availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) {
return availableFormat;
}
}
如果这也失败了,那么我们可以根据可用格式的“好”程度对其进行排名,但在大多数情况下,只需使用指定的第一种格式即可。
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];
}
Presentation mode
演示模式可以说是交换链最重要的设置,因为它代表了向屏幕显示图像的实际条件。Vulkan有四种可能的模式:
- VK_PRESENT_MODE_IMMEDIATE_KHR:您的应用程序提交的图像会立即转移到屏幕上,这可能会导致撕裂。
- VK_PRESENT_MODE_FIFO_KHR:交换链是一个队列,当刷新显示时,显示从队列的前面获取图像,程序在队列的后面插入渲染图像。如果队列已满,则程序必须等待。这与现代游戏中的垂直同步最为相似。刷新显示的瞬间称为“垂直空白”。
- VK_PRESENT_MODE_FIFO_RELAXED_KHR:此模式仅在应用程序延迟且队列在最后一个垂直空白处为空时与前一个模式不同。而不是等待下一个垂直空白,当图像最终到达时,它立即被传送。这可能会导致可见撕裂。
- VK_PRESENT_MODE_MAILBOX_KHR:这是第二种模式的另一种变体。而不是在队列已满时阻止应用程序,而是简单地用较新的图像替换已排队的图像。此模式可用于尽可能快地渲染帧,同时仍避免撕裂,从而导致比标准垂直同步更少的延迟问题。这通常被称为“三重缓冲”,尽管仅存在三个缓冲区并不一定意味着帧速率被解锁。
只有VK_PRESENT_MODE_FIFO_KHR模式保证可用,因此我们必须再次编写一个查找可用最佳模式的函数:
VkPresentModeKHR chooseSwapPresentMode(const std::vector<VkPresentModeKHR>& availablePresentModes) {
return VK_PRESENT_MODE_FIFO_KHR;
}
我个人认为,如果不考虑能源使用,VK_PRESENT_MODE_MAILBOX_KHR是一个非常好的权衡。它允许我们避免撕裂,同时仍然保持相当低的延迟,通过渲染尽可能最新的新图像直到垂直空白。在能源使用更为重要的移动设备上,您可能希望改用VK_PRESENT_MODE_FIFO_KHR。现在,让我们查看列表,看看VK_PRESENT_MODE_MAILBOX_KHR是否可用:
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;
}
Swap extent
这只剩下一个主要属性,我们将为其添加最后一个函数:
VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities) {
}
交换范围是交换链图像的分辨率,它几乎总是完全等于我们正在绘制的窗口的分辨率(以像素为单位)(稍后将详细介绍)。VkSurfaceCapabilitiesKHR结构中定义了可能的分辨率范围。Vulkan告诉我们通过设置当前Extent成员中的宽度和高度来匹配窗口的分辨率。然而,有些窗口管理器允许我们在这里有所不同,这是通过将currentExtent中的宽度和高度设置为一个特殊值来表示的:uint32_t的最大值。在这种情况下,我们将在minImageExtent和maxImageExtent边界内选择与窗口最匹配的分辨率。但我们必须以正确的单位指定分辨率。
GLFW在测量尺寸时使用两个单位:像素和屏幕坐标。例如,我们先前在创建窗口时指定的分辨率{WIDTH,HEIGHT}是以屏幕坐标测量的。但Vulkan使用像素,因此交换链范围也必须以像素为单位指定。不幸的是,如果您使用的是高DPI显示器(如苹果的Retina显示器),屏幕坐标与像素不符。相反,由于像素密度较高,窗口的像素分辨率将大于屏幕坐标分辨率。因此,如果Vulkan不为我们修复交换范围,我们就不能只使用原始的{WIDTH,HEIGHT}。相反,我们必须使用glfwGetFramebufferSize来查询窗口的像素分辨率,然后将其与最小和最大图像范围进行匹配。
#include <cstdint> // Necessary for uint32_t
#include <limits> // Necessary for std::numeric_limits
#include <algorithm> // Necessary for std::clamp
...
VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities) {
if (capabilities.currentExtent.width != std::numeric_limits<uint32_t>::max()) {
return capabilities.currentExtent;
} else {
int width, height;
glfwGetFramebufferSize(window, &width, &height);
VkExtent2D actualExtent = {
static_cast<uint32_t>(width),
static_cast<uint32_t>(height)
};
actualExtent.width = std::clamp(actualExtent.width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width);
actualExtent.height = std::clamp(actualExtent.height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height);
return actualExtent;
}
}
这里使用clamp函数将宽度和高度值限制在实现支持的允许的最小和最大范围之间。
Creating the swap chain
现在我们有了所有这些函数来帮助我们在运行时做出选择,我们终于有了创建工作交换链所需的所有信息。
创建一个createSwapChain函数,该函数从这些调用的结果开始,并确保在创建逻辑设备后从initVulkan调用它。
void initVulkan() {
createInstance();
setupDebugMessenger();
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);
}
除了这些特性之外,我们还必须决定要在交换链中包含多少图像。实现指定了运行所需的最小数量:
uint32_t imageCount = swapChainSupport.capabilities.minImageCount;
然而,简单地坚持这一最小值意味着有时我们可能需要等待驱动程序完成内部操作,然后才能获取另一个要渲染的图像。因此,建议至少比最小值多请求一个图像:
uint32_t imageCount = swapChainSupport.capabilities.minImageCount + 1;
执行此操作时,我们还应确保不超过图像的最大数量,其中0是一个特殊值,表示没有最大值:
if (swapChainSupport.capabilities.maxImageCount > 0 && imageCount > swapChainSupport.capabilities.maxImageCount) {
imageCount = swapChainSupport.capabilities.maxImageCount;
}
与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指定每个图像包含的层的数量。除非您正在开发立体3D应用程序,否则此值始终为1。imageUsage位字段指定我们将在交换链中使用图像的操作类型。在本教程中,我们将直接向它们渲染,这意味着它们用作颜色附件。也可以先将图像渲染到单独的图像,以执行诸如后处理之类的操作。在这种情况下,您可以使用类似VK_IMAGE_USAGE_TRANSFER_DST_BIT的值,并使用内存操作将渲染图像传输到交换链图像。
QueueFamilyIndices indices = findQueueFamilies(physicalDevice);
uint32_t queueFamilyIndices[] = {indices.graphicsFamily.value(), indices.presentFamily.value()};
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;
我们可以指定,如果支持(功能中支持转换),则应将特定转换应用于交换链中的图像,如顺时针旋转90度或水平翻转。要指定不需要任何转换,只需指定当前转换。
createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
compositeAlpha字段指定alpha通道是否应用于与窗口系统中的其他窗口混合。您几乎总是希望忽略alpha通道,因此VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR。
createInfo.presentMode = presentMode;
createInfo.clipped = VK_TRUE;
presentMode成员为自己说话。如果剪裁成员设置为VK_TRUE,那么这意味着我们不关心被遮挡的像素的颜色,例如,因为另一个窗口在它们前面。除非您真的需要能够读取这些像素并获得可预测的结果,否则您将通过启用剪裁获得最佳性能。
createInfo.oldSwapchain = VK_NULL_HANDLE;
剩下最后一个字段,oldSwapChain。使用Vulkan,在应用程序运行时,交换链可能会无效或未优化,例如,因为窗口已调整大小。在这种情况下,实际上需要从头创建交换链,并且必须在此字段中指定对旧链的引用。这是一个复杂的主题,我们将在未来的章节中了解更多。现在我们假设只创建一个交换链。
现在添加一个类成员来存储VkSwapchainKHR对象:
VkSwapchainKHR swapChain;
创建交换链现在与调用vkCreateSwapchainKHR一样简单:
if (vkCreateSwapchainKHR(device, &createInfo, nullptr, &swapChain) != VK_SUCCESS) {
throw std::runtime_error("failed to create swap chain!");
}
这些参数包括逻辑设备、交换链创建信息、可选的自定义分配器和指向存储句柄的变量的指针。在销毁设备之前,应使用vkDestroySwapchainKHR对其进行清理:
void cleanup() {
vkDestroySwapchainKHR(device, swapChain, nullptr);
...
}
现在运行应用程序以确保成功创建交换链!如果此时您在vkCreateSwapchainKHR中遇到访问冲突错误,或看到“未能在SteamOverlayVulkanLayer.dll层中找到”vkGetInstanceProcAddress“之类的消息,请参阅有关Steam覆盖层的常见问题解答条目。
尝试删除createInfo.imageExtent=extent;已启用验证层的行。您将看到其中一个验证层立即发现错误,并打印出一条有用的消息:
Retrieving the swap chain images
交换链现在已经创建,所以剩下的就是检索其中VkImages的句柄。我们将在后面的章节中在渲染操作中引用这些句柄。添加类成员以存储句柄:
std::vector<VkImage> swapChainImages;
图像是由交换链的实现创建的,一旦交换链被破坏,它们将被自动清理,因此我们不需要添加任何清理代码。
我将在vkCreateSwapchainKHR调用之后添加代码以检索createSwapChain函数末尾的句柄。检索它们与我们从Vulkan检索对象数组的其他时间非常相似。请记住,我们只在交换链中指定了最少数量的图像,因此允许实现创建具有更多图像的交换链。这就是为什么我们将首先使用vkGetSwapchainImagesKHR查询最终数量的图像,然后调整容器大小,最后再次调用它以检索句柄。
vkGetSwapchainImagesKHR(device, swapChain, &imageCount, nullptr);
swapChainImages.resize(imageCount);
vkGetSwapchainImagesKHR(device, swapChain, &imageCount, swapChainImages.data());
最后一件事,将我们为交换链图像选择的格式和范围存储在成员变量中。我们将在以后的章节中使用它们。
VkSwapchainKHR swapChain;
std::vector<VkImage> swapChainImages;
VkFormat swapChainImageFormat;
VkExtent2D swapChainExtent;
...
swapChainImageFormat = surfaceFormat.format;
swapChainExtent = extent;
我们现在有一组图像,可以绘制到窗口上并呈现给窗口。下一章将开始介绍如何将图像设置为渲染目标,然后我们开始研究实际的图形管道和绘图命令!