本文主要是学习Vulkan Tutorial.pdf文档中的Window Surface和Swap Chain的部分。
由于Vulkan是一个与平台无关的API,因此无法直接与窗口系统进行交互。
要在Vulkan和窗口系统之间建立连接以向屏幕显示结果,我们需要使用WSI(窗口系统集成)扩展。
这就需要使用到VK_KHR_surface,它公开了一个VkSurfaceKHR对象,表示一个抽象类型的Surface,以呈现渲染图像。
我们程序中的Surface将由我们已经使用GLFW打开的窗口支持。VK_KHR_surface扩展是一个实例级扩展,我们实际上已经启用它,因为它包含在glfwGetRequiredInstanceExtensions返回的列表中。
一般需要在创建实例后立即创建窗口Surface,因为它实际上可以影响物理设备选择。
但如果你只需要离屏渲染,窗口Surface是Vulkan中完全可选的组件。 Vulkan允许这样做,而不是像OpenGL一样必须创建一个不可见的窗口。
在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);
......
}
Vulkan没有“默认帧缓冲区”的概念,取而代之的是名为 “swap chain” 即交换链,也就是渲染的缓冲区,必须在Vulkan中明确创建。
交换链本质上是一个等待呈现给屏幕的图像队列。应用程序将获取这样的图像以绘制它,然后将其返回到队列中。
队列的工作原理以及从队列中显示图像的条件取决于交换链的设置方式,但交换链的一般用途是将图像的显示与屏幕的刷新率同步。
并不是所有的GPU都支持图像显示(比如专为服务器设计的),其次,由于图像显示严重依赖于窗口系统和与窗口相关的Surface,因此它实际上不是Vulkan核心的一部分。
所以必须在查询其支持后才能启用K_KHR_swapchain设备扩展。
可以扩展isDeviceSuitable函数以检查是否支持此扩展。
之前已经实现过如何列出VkPhysicalDevice支持的扩展,这样做法其实一样。值得注意的是,Vulkan头文件提供了一个很好的宏VK_KHR_SWAPCHAIN_EXTENSION_NAME,定义为VK_KHR_swapchain。
使用此宏的优点是编译器将捕获拼写错误。首先声明所需设备扩展的列表,类似于要启用的验证层列表。
const std::vector 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 availableExtensions(extensionCount);
vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount, availableExtensions.data());
std::set requiredExtensions(deviceExtensions.begin(), deviceExtensions.end());
// 遍历当前所有可支持的扩展,并逐步移除也存在于deviceExtensions中的
for (const auto& extension : availableExtensions) {
requiredExtensions.erase(extension.extensionName);
}
// 为empty时,表示所有deviceExtensions中的扩展均支持
return requiredExtensions.empty();
}
使用交换链需要首先使能VK_KHR_swapchain扩展。
方法也很简单,只需要在创建逻辑设备的时候声明一下即可:
createInfo.enabledExtensionCount = static_cast(deviceExtensions.size());
createInfo.ppEnabledExtensionNames = deviceExtensions.data();
只检查交换链是否可用是不够的,因为它实际上可能与我们创建的窗口Surface不兼容(有点坑)。
创建交换链还涉及比vulkan实例和设备创建时更多的设置,因此我们需要在能够继续之前查明更多的细节。
需要检查的基本上有以下三种属性:
与创建队列类似,同样可以使用一个结构体来存储传递这些细节部分:
struct SwapChainSupportDetails {
VkSurfaceCapabilitiesKHR capabilities; // 基本Surface功能
std::vector formats; // Surface格式
std::vector 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;
}
满足了swapChainAnequate条件,那么物理设备肯定是支持我们的应用程序的,但还可以有许多不同的最优性模式。
有三种类型的设置可以选择:
在Vulkan中,Surface格式使用结构体 VkSurfaceFormatKHR 来表示:
typedef struct VkSurfaceFormatKHR {
VkFormat format; // 格式
VkColorSpaceKHR colorSpace; // 色彩空间
} VkSurfaceFormatKHR;
如上每个VkSurfaceFormatKHR条目都包含一个格式和一个colorSpace成员。
最好的情况是 Surface 没有首选格式,Vulkan只通过返回一个格式成员设置为VK_FORMAT_UNDEFINED的VkSurfaceFormatKHR条目来指示。
// 选择合适交换链和Surface的格式
VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector& 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];
}
呈现模式可以说是交换链中最重要的设置,因为它代表了向屏幕显示图像的实际条件。
Vulkan有四种可用的模式:
比如我们试试机器是否支持三重缓冲, 即允许我们通过渲染尽可能最新的新图像直到垂直空白来避免撕裂,同时仍保持相当低的延迟:
VkPresentModeKHR chooseSwapPresentMode(const std::vector& 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;
}
交换范围是交换链图像的分辨率,它几乎总是完全等于绘制的窗口的分辨率。
可能的分辨率范围在 VkSurfaceCapabilitiesKHR 结构中定义。
Vulkan通过在currentExtent成员中设置宽度和高度来匹配窗口的分辨率。
但是,一些窗口管理器允许在这里有所不同, 通过将currentExtent中的宽度和高度设置为特殊值来表示:uint32_t的最大值。在这种情况下,要选择与minImageExtent和maxImageExtent范围内的窗口最匹配的分辨率。
// 选择交换链分辨率
VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities) {
if (capabilities.currentExtent.width != std::numeric_limits::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;
}
}
上述步骤已经把创建交换链的三个主要属性设置了,接下来就是创建交换链对象:VkSwapchainKHR swapChain;
除了上述的三个主要属性,其实还需要做的是:
类似创建其他对象,交换链的创建依赖结构体: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!");
}
}
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;
如果队列系列不同,那么需要使用并发模式来避免必须执行所有权。
并发模式要求使用queueFamilyIndexCount和pQueueFamilyIndices参数预先指定将共享哪些队列系列所有权。
如果图形队列系列和表示队列系列是相同的(大多数硬件都是这种情况),那么我们应该坚持独占模式,因为并发模式要求指定至少两个不同的队列系列。
创建了交换链对象后,就可以检索其中的VkImages的句柄,这些VkImages是用于后续渲染操作的。
我们可以使用一个Set集合存储这些VkImages:
std::vector 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());
这部分内容比较多,小结一下:
交换链本质上是一个等待呈现给屏幕的图像队列。应用程序将获取这样的图像以绘制它,然后将其返回到队列中。
虽然到目前为止,我们编出来的程序还只是一个800*600的黑窗口。但是已经万事具备了,下一步就可以把图像内容呈现到窗口里了。