参考资料
- [Vulkan-文档]https://github.com/KhronosGroup/Vulkan-Docs
简述
本文主要是学习Vulkan Tutorial.pdf文档中的Window Surface和Swap Chain的部分。
一. Window Surface
由于Vulkan是一个与平台无关的API,因此无法直接与窗口系统进行交互。
要在Vulkan和窗口系统之间建立连接以向屏幕显示结果,我们需要使用WSI(窗口系统集成)扩展。
这就需要使用到VK_KHR_surface,它公开了一个VkSurfaceKHR对象,表示一个抽象类型的Surface,以呈现渲染图像。
我们程序中的Surface将由我们已经使用GLFW打开的窗口支持。VK_KHR_surface扩展是一个实例级扩展,我们实际上已经启用它,因为它包含在glfwGetRequiredInstanceExtensions返回的列表中。
一般需要在创建实例后立即创建窗口Surface,因为它实际上可以影响物理设备选择。
但如果你只需要离屏渲染,窗口Surface是Vulkan中完全可选的组件。 Vulkan允许这样做,而不是像OpenGL一样必须创建一个不可见的窗口。
1.1 创建窗口Surface
在Vulkan中使用VkSurfaceKHR表示窗口Surface:
VkSurfaceKHR surface;
虽然VkSurfaceKHR对象及其用法与平台无关,但它的创建并不是因为它取决于窗口系统的细节。例如,它需要Windows上的HWND和HMODULE句柄。因此,扩展中有一个特定于平台的添加,在Windows上称为VK_KHR_win32_surface,并且还自动包含在glfwGetRequiredInstanceExtensions的列表中。
但使用像GLFW这样的库然后继续使用特定于平台的代码没有任何意义。
GLFW实际上有glfwCreateWindowSurface来帮我们处理平台的差异。
// 使用glfw创建WindowSurface
void createWindowSurface() {
if (glfwCreateWindowSurface(instance, window, nullptr, &surface) != VK_SUCCESS) {
throw std::runtime_error("failed to create window surface!");
}
}
// 记得释放
void cleanup() {
// 释放WindowSurface
vkDestroySurfaceKHR(instance, surface, nullptr);
......
}
二. Swap Chain-交换链
Vulkan没有“默认帧缓冲区”的概念,取而代之的是名为 “swap chain” 即交换链,也就是渲染的缓冲区,必须在Vulkan中明确创建。
交换链本质上是一个等待呈现给屏幕的图像队列。应用程序将获取这样的图像以绘制它,然后将其返回到队列中。
队列的工作原理以及从队列中显示图像的条件取决于交换链的设置方式,但交换链的一般用途是将图像的显示与屏幕的刷新率同步。
2.1 检查GPU是否支持交换链
并不是所有的GPU都支持图像显示(比如专为服务器设计的),其次,由于图像显示严重依赖于窗口系统和与窗口相关的Surface,因此它实际上不是Vulkan核心的一部分。
所以必须在查询其支持后才能启用K_KHR_swapchain设备扩展。
可以扩展isDeviceSuitable函数以检查是否支持此扩展。
之前已经实现过如何列出VkPhysicalDevice支持的扩展,这样做法其实一样。值得注意的是,Vulkan头文件提供了一个很好的宏VK_KHR_SWAPCHAIN_EXTENSION_NAME,定义为VK_KHR_swapchain。
使用此宏的优点是编译器将捕获拼写错误。首先声明所需设备扩展的列表,类似于要启用的验证层列表。
const std::vector<const char*> deviceExtensions = {
VK_KHR_SWAPCHAIN_EXTENSION_NAME
};
bool isDeviceSuitable(VkPhysicalDevice device) {
QueueFamilyIndices indices = findQueueFamilies(device);
bool extensionsSupported = checkDeviceExtensionSupport(device);
return indices.isComplete() && extensionsSupported;
}
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());
// 遍历当前所有可支持的扩展,并逐步移除也存在于deviceExtensions中的
for (const auto& extension : availableExtensions) {
requiredExtensions.erase(extension.extensionName);
}
// 为empty时,表示所有deviceExtensions中的扩展均支持
return requiredExtensions.empty();
}
2.2 使能设备扩展
使用交换链需要首先使能VK_KHR_swapchain扩展。
方法也很简单,只需要在创建逻辑设备的时候声明一下即可:
createInfo.enabledExtensionCount = static_cast<uint32_t>(deviceExtensions.size());
createInfo.ppEnabledExtensionNames = deviceExtensions.data();
2.3 获取关于swap chain更多支持细节
只检查交换链是否可用是不够的,因为它实际上可能与我们创建的窗口Surface不兼容(有点坑)。
创建交换链还涉及比vulkan实例和设备创建时更多的设置,因此我们需要在能够继续之前查明更多的细节。
需要检查的基本上有以下三种属性:
- 基本Surface功能(交换链中的最小/最大图像数,图像的最小/最大宽度和高度)
- Surface的格式(像素格式,色彩空间)
- 可用的呈现模式
与创建队列类似,同样可以使用一个结构体来存储传递这些细节部分:
struct SwapChainSupportDetails {
VkSurfaceCapabilitiesKHR capabilities; // 基本Surface功能
std::vector<VkSurfaceFormatKHR> formats; // Surface格式
std::vector<VkPresentModeKHR> presentModes; // 可用的呈现模式(presentation modes)
};
接来下是查询细节部分:
SwapChainSupportDetails querySwapChainSupport(VkPhysicalDevice device) {
SwapChainSupportDetails details;
// 1. 查询基本Surface功能
// 需要使用到本机物理设备(GPU), 以及创建的逻辑设备对应的Surface,结果保存在VkSurfaceCapabilitiesKHR结构体中
// VkResult vkGetPhysicalDeviceSurfaceCapabilitiesKHR(VkPhysicalDevice physicalDevice, VkSurfaceKHR surface, VkSurfaceCapabilitiesKHR *pSurfaceCapabilities)
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface, &details.capabilities);
// 2. 查询可支持的Surface格式
// 类似扩展,按例先查询一下数量,然后更新细节到VkSurfaceFormatKHR的队列中
uint32_t formatCount;
vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, nullptr);
if (formatCount != 0) {
details.formats.resize(formatCount);
vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, details.formats.data());
}
// 3. 查询可用的呈现模式(presentation modes)
uint32_t presentModeCount;
vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, nullptr);
if (presentModeCount != 0) {
details.presentModes.resize(presentModeCount);
vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, details.presentModes.data());
}
return details;
}
2.4 为swap chain-交换链选择合适的设置
满足了swapChainAnequate条件,那么物理设备肯定是支持我们的应用程序的,但还可以有许多不同的最优性模式。
有三种类型的设置可以选择:
- Surface格式(颜色深度)
- 呈现模式(将图像“交换”到屏幕的条件)
- 交换范围(交换链中图像的分辨率)
2.4.1 Surface格式
在Vulkan中,Surface格式使用结构体 VkSurfaceFormatKHR 来表示:
typedef struct VkSurfaceFormatKHR {
VkFormat format; // 格式
VkColorSpaceKHR colorSpace; // 色彩空间
} VkSurfaceFormatKHR;
如上每个VkSurfaceFormatKHR条目都包含一个格式和一个colorSpace成员。
- 格式成员指定颜色通道和类型。例如,VK_FORMAT_B8G8R8A8_UNORM意味着我们以8位无符号整数的顺序存储B,G,R和alpha通道,每个像素总共32位。
- colorSpace成员使用VK_COLOR_SPACE_SRGB_NONLINEAR_KHR标志指示是否支持SRGB色彩空间。请注意,在旧版本的规范中,此标志曾被称为VK_COLORSPACE_SRGB_NONLINEAR_KHR。对于色彩空间,我们将使用SRGB(如果可用),因为它会产生更准确的感知色彩。直接使用SRGB颜色有点挑战,因此我们将使用标准RGB作为颜色格式,其中最常见的一种是VK_FORMAT_B8G8R8A8_UNORM。
最好的情况是 Surface 没有首选格式,Vulkan只通过返回一个格式成员设置为VK_FORMAT_UNDEFINED的VkSurfaceFormatKHR条目来指示。
// 选择合适交换链和Surface的格式
VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector<VkSurfaceFormatKHR>& availableFormats) {
if (availableFormats.size() == 1 && availableFormats[0].format == VK_FORMAT_UNDEFINED) {
return {VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR};
}
for (const auto& availableFormat : availableFormats) {
if (availableFormat.format == VK_FORMAT_B8G8R8A8_UNORM &&
availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) {
return availableFormat;
}
}
// 如果没有找到合适的,保险起见,直接使用第一个就好了
return availableFormats[0];
}
2.4.2 呈现模式(Presentation Mode)
呈现模式可以说是交换链中最重要的设置,因为它代表了向屏幕显示图像的实际条件。
Vulkan有四种可用的模式:
- VK_PRESENT_MODE_IMMEDIATE_KHR:应用程序提交的图像会立即传输到屏幕上,这可能会导致撕裂现象(上下或者左右图像不匹配,前后两帧合成)。
- VK_PRESENT_MODE_FIFO_KHR:交换链是一个队列,当刷新显示并且程序在队列的后面插入渲染图像时,显示从队列前面获取图像。如果队列已满,则程序必须等待。这与现代游戏中的垂直同步最相似。刷新显示的那一刻
被称为“垂直空白”。 - VK_PRESENT_MODE_FIFO_RELAXED_KHR:如果应用程序延迟且队列在最后一个垂直空白处为空,则此模式仅与前一个模式不同。而不是等待下一个垂直空白,图像最终到达时立即传输。这可能会导致明显的撕裂。
- VK_PRESENT_MODE_MAILBOX_KHR:这是第二种模式的另一种变体。当队列已满时,已排队的图像将被替换为较新的图像,而不是阻塞应用程序。此模式可用于实现三重缓冲,与使用双缓冲的标准垂直同步相比,可以避免使用明显更少的延迟问题进行撕裂。
比如我们试试机器是否支持三重缓冲, 即允许我们通过渲染尽可能最新的新图像直到垂直空白来避免撕裂,同时仍保持相当低的延迟:
VkPresentModeKHR chooseSwapPresentMode(const std::vector<VkPresentModeKHR>& availablePresentModes) {
VkPresentModeKHR bestMode = VK_PRESENT_MODE_FIFO_KHR;
for (const auto& availablePresentMode : availablePresentModes) {
if (availablePresentMode == VK_PRESENT_MODE_MAILBOX_KHR) {
return availablePresentMode;
} else if (availablePresentMode == VK_PRESENT_MODE_IMMEDIATE_KHR) {
bestMode = availablePresentMode;
}
}
return bestMode;
}
2.4.3 交换范围(Swap extent)
交换范围是交换链图像的分辨率,它几乎总是完全等于绘制的窗口的分辨率。
可能的分辨率范围在 VkSurfaceCapabilitiesKHR 结构中定义。
Vulkan通过在currentExtent成员中设置宽度和高度来匹配窗口的分辨率。
但是,一些窗口管理器允许在这里有所不同, 通过将currentExtent中的宽度和高度设置为特殊值来表示:uint32_t的最大值。在这种情况下,要选择与minImageExtent和maxImageExtent范围内的窗口最匹配的分辨率。
// 选择交换链分辨率
VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities) {
if (capabilities.currentExtent.width != std::numeric_limits<uint32_t>::max()) {
return capabilities.currentExtent;
} else {
VkExtent2D actualExtent = {WIDTH, 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;
}
}
2.5 创建交换链
上述步骤已经把创建交换链的三个主要属性设置了,接下来就是创建交换链对象:VkSwapchainKHR swapChain;
除了上述的三个主要属性,其实还需要做的是:
- 绑定窗口Surface
- 设置交换链图像细节,包括最小图像数量、图像图层数量
- 处理将在多个队列系列中使用的交换链图像。如果图形队列系列与表示队列不同,需要从图形队列中绘制交换链中的图像,然后在呈现队列中提交它们。 有两种方法可以处理从多个队列访问的图像:
- VK_SHARING_MODE_EXCLUSIVE:映像一次由一个队列系列拥有,并且必须在将其用于另一个队列系列之前显式转移所有权。 此选项提供最佳性能。
- VK_SHARING_MODE_CONCURRENT:可以跨多个队列系列使用映像,而无需显式所有权传输。
类似创建其他对象,交换链的创建依赖结构体:VkSwapchainCreateInfoKHR
void createSwapChain() {
// 获取物理设备可支持的交换链细节部分
SwapChainSupportDetails swapChainSupport = querySwapChainSupport(physicalDevice);
// 选择合适的Surface格式
VkSurfaceFormatKHR surfaceFormat = chooseSwapSurfaceFormat(swapChainSupport.formats);
// 选择合适的呈现模式
VkPresentModeKHR presentMode = chooseSwapPresentMode(swapChainSupport.presentModes);
// 选择合适的分辨率
VkExtent2D extent = chooseSwapExtent(swapChainSupport.capabilities);
// 必须设置交换链运行所需的最小图像数量:
// 有时可能必须等待驱动程序完成内部操作才能获取另一个要渲染的图像。 因此,建议最小值加1:
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 = surface; // 绑定窗口Surface
createInfo.minImageCount = imageCount; // 设置最小图像数量
createInfo.imageFormat = surfaceFormat.format; // 设置图像格式
createInfo.imageColorSpace = surfaceFormat.colorSpace; // 设置图像颜色空间
createInfo.imageExtent = extent; // 设置分辨率
createInfo.imageArrayLayers = 1; // 指定每个图像所包含的图层数量, 除非是立体3D应用程序,否则始终为1。
// imageUsage 是 VkImageUsageFlags 类型, 指定将使用交换链中的图像进行哪种操作
createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; // 指定该图像可用于创建适合用作颜色的VkImageView或解析VkFramebuffer中的附件。
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
}
// 可以指定某个变换应该应用于交换链中的图像(支持变换功能),如顺时针旋转90度或水平翻转。 要指定您不需要任何转换,只需指定当前转换。
createInfo.preTransform = swapChainSupport.capabilities.currentTransform;
// compositeAlpha字段指定是否应该使用alpha通道与窗口系统中的其他窗口进行混合。 一般都是忽略alpha通道,因此设置为VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR。
createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
// 设置呈现模式
createInfo.presentMode = presentMode;
// 如果剪裁的成员设置为VK_TRUE,那么这意味着我们不关心被遮挡的像素的颜色
// 例如因为另一个窗口位于它们前面。 除非真的需要能够读回这些像素并获得可预测的结果,否则可以通过启用剪辑获得最佳性能。
createInfo.clipped = VK_TRUE;
// 使用Vulkan时,交换链可能会在应用程序运行时变为无效或未优化,例如因为窗口已调整大小。
// 在这种情况下,交换链实际上需要从头开始重新创建,并且必须在此字段中指定对旧交换链的引用, 后续在研究。
// 假设我们只会创建一个交换链
createInfo.oldSwapchain = VK_NULL_HANDLE;
if (vkCreateSwapchainKHR(device, &createInfo, nullptr, &swapChain) != VK_SUCCESS) {
throw std::runtime_error("failed to create swap chain!");
}
}
2.5.1 VkImageUsageFlags
VkImageUsageFlags是一个位掩码类型,用于设置零或更多VkImageUsageFlagBits的掩码。
用于指定图像进行的操作类型, 有如下
typedef enum VkImageUsageFlagBits {
VK_IMAGE_USAGE_TRANSFER_SRC_BIT = 0x00000001,
VK_IMAGE_USAGE_TRANSFER_DST_BIT = 0x00000002,
VK_IMAGE_USAGE_SAMPLED_BIT = 0x00000004,
VK_IMAGE_USAGE_STORAGE_BIT = 0x00000008,
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT = 0x00000010,
VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT = 0x00000020,
VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT = 0x00000040,
VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT = 0x00000080,
VK_IMAGE_USAGE_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF
} VkImageUsageFlagBits;
- VK_IMAGE_USAGE_TRANSFER_SRC_BIT: 指定图像可用作传输命令的源。
- VK_IMAGE_USAGE_TRANSFER_DST_BIT: 指定图像可用作传输命令的目标。
- VK_IMAGE_USAGE_SAMPLED_BIT: 指定该图像可用于创建适合占用VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE或VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER类型的VkDescriptorSet槽的VkImageView,并由着色器进行采样。
- VK_IMAGE_USAGE_STORAGE_BIT: 指定该图像可用于创建适合占用VK_DESCRIPTOR_TYPE_STORAGE_IMAGE类型的VkDescriptorSet插槽的VkImageView。
- VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT: 指定该图像可用于创建适合用作颜色的VkImageView或解析VkFramebuffer中的附件。
- VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT: 指定该图像可用于创建适合用作VkFramebuffer中的深度/模板附件的VkImageView。
- VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT: 指定绑定到此映像的内存将使用VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT进行分配。可以为任何可用于创建适合用作颜色,分辨率,深度/模板或输入附件的VkImageView的图像设置此位。
- VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT: 指定该图像可用于创建适合占用VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT类型的VkDescriptorSet槽的VkImageView; 从着色器读取作为输入附件; 并用作帧缓冲区中的输入附件。
2.5.2 处理将在多个队列系列中使用的交换链图像
如果队列系列不同,那么需要使用并发模式来避免必须执行所有权。
并发模式要求使用queueFamilyIndexCount和pQueueFamilyIndices参数预先指定将共享哪些队列系列所有权。
如果图形队列系列和表示队列系列是相同的(大多数硬件都是这种情况),那么我们应该坚持独占模式,因为并发模式要求指定至少两个不同的队列系列。
2.6 检索交换链图像
创建了交换链对象后,就可以检索其中的VkImages的句柄,这些VkImages是用于后续渲染操作的。
我们可以使用一个Set集合存储这些VkImages:
std::vector<VkImage> swapChainImages;
类似于Surface, 这些VkImage是不需要我们主动销毁的。在交换链被销毁时,Vulkan自动就会销毁这些VkImage了。
一般在vkCreateSwapchainKHR调用之后立即检索createSwapChain函数末尾的句柄。
在交换链中只是指定了最少数量的图像,Vulkan允许实现创建更多的图像。创建后记得调整容器大小,最后再次调用它来检索VkImage。
// 先获取交换链中图像数量
vkGetSwapchainImagesKHR(device, swapChain, &imageCount, nullptr);
// 调整set集合大小
swapChainImages.resize(imageCount);
// 获取VkImage对象
vkGetSwapchainImagesKHR(device, swapChain, &imageCount, swapChainImages.data());
2.7 小结
这部分内容比较多,小结一下:
2.7.1 什么是交换链?
交换链本质上是一个等待呈现给屏幕的图像队列。应用程序将获取这样的图像以绘制它,然后将其返回到队列中。
2.7.2 创建交换链的一般步骤
- 确认当前物理设备(GPU)是否支持交换链
- 创建逻辑设备时,使能交换链扩展(ppEnabledExtensionNames)
- 获取当前设备支持swap chain的更多细节
- 创建swap chain对象
- 绑定窗口Surface
- 设置最小图像数量 minImageCount
- 选择合适的图像格式 imageFormat
- 选择合适的图像颜色空间 imageColorSpace
- 选择合适的图像分辨率 imageExtent
- 设置图像图层 imageArrayLayers
- 设置图像操作方式 imageUsage
- 选择图像呈现模式 presentMode
- 是否需要裁剪功能 clipped(VK_TRUE, VK_FALSE)
- 设置旧交换链的引用 oldSwapchain
- 获取交换链图像(VkImage)对象集合
虽然到目前为止,我们编出来的程序还只是一个800*600的黑窗口。但是已经万事具备了,下一步就可以把图像内容呈现到窗口里了。