std::vector<const char*> requiredDeviceExtension = {
vk::KHRSwapchainExtensionName};
Vulkan 没有“默认帧缓冲”的概念,因此它需要一个基础设施来拥有我们在屏幕上显示之前要渲染到的缓冲。这个基础设施被称为 交换链(swap chain),并且必须在 Vulkan 中显式创建。交换链本质上是一个等待呈现到屏幕的图像队列。我们的应用程序将获取这样一个图像进行绘制,然后将其返回给队列。队列具体如何工作以及从队列呈现图像的条件取决于交换链的设置方式。但是,交换链的通用目的是将图像的呈现与屏幕的刷新率同步。
并非所有显卡都能够直接向屏幕呈现图像,原因多种多样,例如,因为它们是为服务器设计的,没有任何显示输出。其次,由于图像呈现与窗口系统以及与窗口关联的表面密切相关,它不是 Vulkan 核心的一部分。你必须在查询支持后启用 VK_KHR_swapchain 设备扩展。
为此,我们首先扩展 createLogicalDevice 函数,以检查是否支持此扩展。我们之前已经了解了如何列出 vk::raii::PhysicalDevice 支持的扩展,因此这样做应该相当简单。请注意,Vulkan 头文件提供了一个很好的宏 vk::KHRSwapchainExtensionName,它被定义为 VK_KHR_swapchain。使用此宏的好处是编译器会捕获拼写错误。
首先声明一个必需的设备扩展列表,类似于启用验证层的列表。
std::vector<const char*> requiredDeviceExtension = {
vk::KHRSwapchainExtensionName};
应该注意的是,呈现队列的可用性(正如我们在前一章中检查的那样)意味着必须支持交换链扩展。但是,该扩展确实必须显式启用。
使用交换链首先需要启用 VK_KHR_swapchain 扩展。启用扩展只需要对逻辑设备创建结构体进行一个小改动:
deviceCreateInfo.enabledExtensionCount = requiredDeviceExtension.size();
deviceCreateInfo.ppEnabledExtensionNames = requiredDeviceExtension.data();
或者,我们可以在构造时执行此操作,并保持非常简洁:
std::vector requiredDeviceExtension = { vk::KHRSwapchainExtensionName };
float queuePriority = 0.5f;
vk::DeviceQueueCreateInfo deviceQueueCreateInfo{.queueFamilyIndex = queueIndex, .queueCount = 1, .pQueuePriorities = &queuePriority};
vk::DeviceCreateInfo deviceCreateInfo{.pNext = &featureChain.get<vk::PhysicalDeviceFeatures2>(),
.queueCreateInfoCount = 1,
.pQueueCreateInfos = &deviceQueueCreateInfo,
.enabledExtensionCount = static_cast<uint32_t>(requiredDeviceExtension.size()),
.ppEnabledExtensionNames = requiredDeviceExtension.data()};
仅仅检查交换链是否可用是不够的,因为它可能与我们的窗口表面不兼容。创建交换链也涉及比实例和设备创建多得多的设置,因此我们需要在继续之前查询更多详细信息。
基本上有三种属性需要检查:
基本表面能力(交换链中图像的最小/最大数量,图像的最小/最大宽度和高度)
表面格式(像素格式,颜色空间)
可用的呈现模式
本节介绍如何查询包含这些信息的结构体。这些结构体的含义以及它们包含的确切数据将在下一节讨论。
让我们从基本表面能力开始。这些属性很容易查询,并返回到一个单独的 vk::SurfaceCapabilitiesKHR 结构体中。
auto surfaceCapabilities = physicalDevice.getSurfaceCapabilitiesKHR( *surface );
此函数在确定支持的能力时会考虑指定的 vk::SurfaceKHR 窗口表面。所有支持查询函数都将此作为第一个参数,因为它是交换链的核心组件。
下一步是查询支持的表面格式。
std::vector<vk::SurfaceFormatKHR> availableFormats = physicalDevice.getSurfaceFormatsKHR( surface );
最后,使用 vk::raii::PhysicalDevice::getSurfacePresentModesKHR 查询支持的呈现模式,其工作方式完全相同:
std::vector<vk::PresentModeKHR> availablePresentModes = physicalDevice.getSurfacePresentModesKHR( surface );
现在所有细节都可用。如果给定我们拥有的窗口表面,至少有一种支持的图像格式和一种支持的呈现模式,则交换链支持足以满足本教程的要求。重要的是,我们只在验证扩展可用后才尝试查询交换链支持。
交换链中可能仍然存在许多不同最优性的模式。现在我们将编写一些函数,以找到最佳交换链的正确设置。有三种类型的设置需要确定:
表面格式(颜色深度)
呈现模式(将图像“交换”到屏幕的条件)
交换范围(交换链中图像的分辨率)
对于这些设置中的每一个,我们心中都有一个理想值,如果可用,我们将采用它,否则我们将创建一些逻辑来找到次优值。
此设置的函数如下所示。稍后我们将把 SwapChainSupportDetails 结构体的 formats 成员作为参数传递。
vk::SurfaceFormatKHR chooseSwapSurfaceFormat(std::vector<vk::SurfaceFormatKHR> const &availableFormats)
{
assert(!availableFormats.empty());
return availableFormats[0];
}
每个 vk::SurfaceFormatKHR 条目包含一个 format 和一个 colorSpace 成员。format 成员指定颜色通道和类型。例如,vk::Format::eB8G8R8A8Srgb 表示我们按顺序存储 B、G、R 和 alpha 通道,每个通道为 8 位无符号整数,每个像素总共 32 位。colorSpace 成员使用 vk::ColorSpaceKHR::eSrgbNonlinear 标志指示是否支持 SRGB 颜色空间。
对于颜色空间,如果可用,我们将使用 SRGB,因为它 可以产生更准确的感知颜色。它也是图像(如我们稍后将使用的纹理)相当标准的颜色空间。
因此,我们还应该使用 SRGB 颜色格式,其中最常用的格式之一是 vk::Format::eB8G8R8A8Srgb。
让我们浏览列表,看看首选组合是否可用:
const auto formatIt = std::ranges::find_if(
availableFormats,
[](const auto &format) { return format.format == vk::Format::eB8G8R8A8Srgb && format.colorSpace == vk::ColorSpaceKHR::eSrgbNonlinear; });
}
如果失败,那么我们可以根据“好”的程度对可用格式进行排名,但在大多数情况下,使用指定的第一种格式就可以了。
vk::SurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector<vk::SurfaceFormatKHR>& availableFormats) {
const auto formatIt = std::ranges::find_if(
availableFormats,
[](const auto &format) { return format.format == vk::Format::eB8G8R8A8Srgb && format.colorSpace == vk::ColorSpaceKHR::eSrgbNonlinear; });
return formatIt != availableFormats.end() ? *formatIt : availableFormats[0];
}
呈现模式可以说是交换链最重要的设置,因为它代表了将图像显示到屏幕的实际条件。Vulkan 中有四种可能的模式:
vk::PresentModeKHR::eImmediate:应用程序提交的图像立即传输到屏幕,这可能会导致画面撕裂。
vk::PresentModeKHR::eFifo:交换链是一个队列,显示在刷新时从队列前面取出一张图像,程序在队列后面插入渲染后的图像。如果队列已满,则程序必须等待。这最类似于现代游戏中的垂直同步。显示刷新的时刻称为“垂直空白”。
vk::PresentModeKHR::eFifoRelaxed:此模式仅在前一个模式的基础上,如果应用程序延迟并且队列在上一个垂直空白时为空,则有所不同。程序不会等待下一个垂直空白,而是在图像最终到达时立即传输。这可能会导致可见的撕裂。
vk::PresentModeKHR::eMailbox:这是第二种模式的另一种变体。当队列满时,不是阻塞应用程序,而是直接用较新的图像替换已经排队的图像。此模式可用于尽可能快地渲染帧,同时仍然避免撕裂,与标准垂直同步相比,延迟问题更少。这通常被称为“三重缓冲”,尽管仅存在三个缓冲并不一定意味着帧率不受限制。
只有 vk::PresentModeKHR::eFifo 模式保证可用,因此我们再次必须编写一个函数来查找可用的最佳模式:
vk::PresentModeKHR chooseSwapPresentMode(std::vector<vk::PresentModeKHR> const &availablePresentModes)
{
return vk::PresentModeKHR::eFifo;
}
我认为,如果不考虑能耗,vk::PresentModeKHR::eMailbox 是一个非常不错的折衷方案。它允许我们避免撕裂,同时通过渲染尽可能新的图像直到垂直空白来保持相当低的延迟。在移动设备上,能耗更为重要,你可能希望改用 vk::PresentModeKHR::eFifo。现在,让我们浏览列表,看看 vk::PresentModeKHR::eMailbox 是否可用:
vk::PresentModeKHR chooseSwapPresentMode(std::vector<vk::PresentModeKHR> const &availablePresentModes)
{
assert(std::ranges::any_of(availablePresentModes, [](auto presentMode) { return presentMode == vk::PresentModeKHR::eFifo; }));
return std::ranges::any_of(availablePresentModes,
[](const vk::PresentModeKHR value) { return vk::PresentModeKHR::eMailbox == value; }) ?
vk::PresentModeKHR::eMailbox :
vk::PresentModeKHR::eFifo;
}
只剩下一个主要属性,为此我们将添加最后一个函数:
vk::Extent2D chooseSwapExtent(vk::SurfaceCapabilitiesKHR const &capabilities)
{}
交换范围是交换链图像的分辨率,它几乎总是等于我们正在绘制的窗口的 像素 分辨率(稍后会详细介绍)。可能分辨率的范围在 vk::SurfaceCapabilitiesKHR 结构体中定义。Vulkan 告诉我们可以通过将 currentExtent 成员中的宽度和高度设置为窗口的分辨率来匹配它。但是,某些窗口管理器确实允许我们在此处有所差异,这通过将 currentExtent 中的宽度和高度设置为一个特殊值来表示:uint32_t 的最大值。在这种情况下,我们将在 minImageExtent 和 maxImageExtent 范围内选择最匹配窗口的分辨率。但我们必须以正确的单位指定分辨率。
GLFW 在测量大小时使用两种单位:像素和 屏幕坐标。例如,我们之前在创建窗口时指定的分辨率 {WIDTH, HEIGHT} 是以屏幕坐标测量的。但 Vulkan 使用像素,因此交换链范围也必须以像素指定。不幸的是,如果你使用高 DPI 显示器(如 Apple 的 Retina 显示器),屏幕坐标与像素不对应。相反,由于更高的像素密度,窗口的像素分辨率将大于屏幕坐标分辨率。因此,如果 Vulkan 没有为我们固定交换范围,我们不能直接使用原始的 {WIDTH, HEIGHT}。相反,我们必须使用 glfwGetFramebufferSize 查询窗口的像素分辨率,然后再将其与最小和最大图像范围进行匹配。
#include <cstdint> // 必需的 uint32_t
#include <limits> // 必需的 std::numeric_limits
#include <algorithm> // 必需的 std::clamp
...
vk::Extent2D chooseSwapExtent(vk::SurfaceCapabilitiesKHR const &capabilities)
{
if (capabilities.currentExtent.width != std::numeric_limits<uint32_t>::max())
{
return capabilities.currentExtent;
}
int width, height;
glfwGetFramebufferSize(window, &width, &height);
return {
std::clamp<uint32_t>(width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width),
std::clamp<uint32_t>(height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height)
};
}
这里使用 clamp 函数将 width 和 height 的值限制在实现支持的最小和最大范围之间。
现在我们有了所有这些辅助函数来帮助我们做出运行时必须做出的选择,我们终于拥有了创建工作交换链所需的所有信息。
创建一个 createSwapChain 函数,从这些调用的结果开始,并确保在逻辑设备创建之后从 initVulkan 调用它。
void initVulkan() {
createInstance();
setupDebugMessenger();
createSurface();
pickPhysicalDevice();
createLogicalDevice();
createSwapChain();
}
void createSwapChain() {
vk::SurfaceCapabilitiesKHR surfaceCapabilities = physicalDevice.getSurfaceCapabilitiesKHR( *surface );
swapChainExtent = chooseSwapExtent(surfaceCapabilities);
uint32_t minImageCount = chooseSwapMinImageCount(surfaceCapabilities);
std::vector<vk::SurfaceFormatKHR> availableFormats = physicalDevice.getSurfaceFormatsKHR(*surface);
swapChainSurfaceFormat = chooseSwapSurfaceFormat(availableFormats);
}
除了这些属性之外,我们还必须决定交换链中希望有多少张图像。实现指定了其运行所需的最小数量:
uint32_t imageCount = surfaceCapabilities.minImageCount;
然而,仅仅坚持这个最小值意味着我们有时可能不得不等待驱动程序完成内部操作,然后才能获取另一张图像进行渲染。因此,建议至少请求比最小值多一张图像:
uint32_t imageCount = surfaceCapabilities.minImageCount + 1;
我们还应该确保在这样做时不超过最大图像数量,其中 0 是一个特殊值,表示没有最大值,从而得到这个辅助函数
uint32_t chooseSwapMinImageCount(vk::SurfaceCapabilitiesKHR const &surfaceCapabilities)
{
auto minImageCount = std::max(3u, surfaceCapabilities.minImageCount);
if ((0 < surfaceCapabilities.maxImageCount) && (surfaceCapabilities.maxImageCount < minImageCount))
{
minImageCount = surfaceCapabilities.maxImageCount;
}
return minImageCount;
}
按照 Vulkan 对象的传统,创建交换链对象需要填充一个大型结构体,公平地说,交换链是一个相当复杂的对象,因此它是 Vulkan 中较大的 createInfo 结构体之一:
vk::SwapchainCreateInfoKHR swapChainCreateInfo{.surface = *surface,
.minImageCount = minImageCount,
.imageFormat = swapChainSurfaceFormat.format,
.imageColorSpace = swapChainSurfaceFormat.colorSpace,
.imageExtent = swapChainExtent,
.imageArrayLayers = 1,
.imageUsage = vk::ImageUsageFlagBits::eColorAttachment,
.imageSharingMode = vk::SharingMode::eExclusive,
.preTransform = surfaceCapabilities.currentTransform,
.compositeAlpha = vk::CompositeAlphaFlagBitsKHR::eOpaque,
.presentMode = chooseSwapPresentMode(availablePresentModes),
.clipped = true};
};
.imageArrayLayers = 1,
imageArrayLayers 指定每个图像包含的层数。除非你正在开发立体 3D 应用程序,否则这始终是 1。
.imageUsage = vk::ImageUsageFlagBits::eColorAttachment,
imageUsage 位字段指定我们将对交换链中的图像进行哪种操作。在本教程中,我们将直接渲染到它们,这意味着它们用作颜色附件。也有可能你首先将图像渲染到单独的图像以执行后期处理等操作。在这种情况下,你可以使用 vk::ImageUsageFlagBits::eTransferDst 之类的值,并使用内存操作将渲染后的图像传输到交换链图像。
.imageSharingMode = vk::SharingMode::eExclusive,
imageSharingMode 指定如何处理可能跨多个队列族使用的交换链图像。有两种方式处理从多个队列访问的图像:
vk::SharingMode::eExclusive:图像一次由一个队列族拥有,在另一个队列族中使用之前必须显式转移所有权。此选项提供最佳性能。
vk::SharingMode::eConcurrent:图像可以在多个队列族之间使用,无需显式所有权转移。
如果队列族不同,那么你可以使用并发模式来避免进行所有权章节,因为这些涉及一些最好在稍后解释的概念。并发模式要求你使用 queueFamilyIndexCount 和 pQueueFamilyIndices 参数提前指定在哪些队列族之间共享所有权。并发模式要求你至少指定两个不同的队列族。如果图形队列族和呈现队列族相同(在大多数硬件上都是如此),那么我们应该坚持使用独占模式。
.preTransform = surfaceCapabilities.currentTransform,
如果支持(capabilities 中的 supportedTransforms),我们可以指定应对交换链中的图像应用某种变换,例如顺时针旋转 90 度或水平翻转。要指定你不希望进行任何变换,只需指定当前变换。
.compositeAlpha = vk::CompositeAlphaFlagBitsKHR::eOpaque,
compositeAlpha 字段指定是否应将 alpha 通道用于窗口系统中与其他窗口的混合。你几乎总是希望简单地忽略 alpha 通道,因此使用 vk::CompositeAlphaFlagBitsKHR::eOpaque。
.presentMode = chooseSwapPresentMode(availablePresentModes),
.clipped = true};
presentMode 成员不言自明。如果 clipped 成员设置为 vk::True,那么这意味着我们不关心被遮挡像素的颜色,例如,因为另一个窗口在它们前面。除非你真的需要能够读取这些像素并获得可预测的结果,否则启用裁剪将获得最佳性能。
swapChainCreateInfo.oldSwapchain = nullptr;
最后剩下一个字段,oldSwapChain。使用 Vulkan,有可能在你的应用程序运行时交换链变得无效或未优化,例如,因为窗口被调整大小。在这种情况下,交换链实际上需要从头开始重新创建,并且必须在此字段中指定对旧交换链的引用。这是一个复杂的主题,我们将在 未来的章节 中了解更多。现在,我们将假设我们只会创建一个交换链,并且可以将此成员保留为其默认值 nullptr。
现在添加类成员来存储 vk::SwapchainKHR 对象及其图像:
vk::raii::SwapchainKHR swapChain;
std::vector<vk::Image> swapChainImages;
现在创建交换链就像调用 vk::raii::SwapchainKHR 的构造函数一样简单:
swapChain = vk::raii::SwapchainKHR( device, swapChainCreateInfo );
swapChainImages = swapChain.getImages();
参数是逻辑设备和交换链创建信息。
现在运行应用程序以确保交换链创建成功!如果在 vkCreateSwapchainKHR 中出现访问冲突错误,或者看到类似 Failed to find 'vkGetInstanceProcAddress' in layer SteamOverlayVulkanLayer.dll 的消息,请参阅 常见问题解答条目 了解 Steam 覆盖层。
尝试在启用验证层的情况下去掉 swapChainCreateInfo.imageExtent = extent; 这一行。你会看到其中一个验证层立即捕获了错误,并打印了一条有用的消息:
交换链现在已经创建,剩下要做的就是检索它包含的 vk::Image 对象的句柄。我们将在后续章节的渲染操作中引用这些句柄。
std::vector<vk::Image> swapChainImages = swapChain->getImages();
最后,将我们为交换链图像选择的格式和范围存储在成员变量中。我们将在未来的章节中需要它们。
vk::raii::SwapchainKHR swapChain = nullptr;
std::vector<vk::Image> swapChainImages;
vk::SurfaceFormatKHR swapChainSurfaceFormat;
vk::Extent2D swapChainExtent;
我们现在有了一组可以绘制并可以呈现到窗口的图像。下一章将开始介绍如何将图像设置为渲染目标,然后我们将开始研究实际的图形管线和绘制命令!
Vulkan 是 Khronos Group Inc. 的注册商标
教程内容版权归原作者,遵循 CC BY-SA 4.0;本站独立代码及设计除外。