由于Vulkan是一个平台无关的API,因此它不能自己直接与窗口系统接口。为了在Vulkan和窗口系统之间建立连接并将结果显示到屏幕上,我们需要使用WSI(窗口系统集成)扩展。在本章中,我们将讨论第一个,即VK_KHR_surface。它是一个VkSurfaceKHR对象,该对象表示要呈现渲染图像的抽象表面类型。我们程序中的surface将由我们已经用GLFW打开的窗口支持。
VK_KHR_surface扩展是一个实例级扩展,我们实际上已经启用了它,因为它包含在由glfwGetRequiredInstanceExtensions返回的列表中。该列表还包括一些我们将在接下来的几章中使用的其他WSI扩展。需要在实例创建之后立即创建窗口表面,因为它实际上会影响物理设备的选择。我们推迟这一点的原因是,窗口表面是渲染目标和表示的更大主题的一部分,解释将打乱基本设置。还应该注意的是,如果您只需要屏幕外渲染,那么窗口表面在Vulkan中是完全可选的组件。Vulkan允许你做到这一点,而不需要像创建一个不可见窗口(OpenGL所必需的)。
窗口表面创建
首先定义一个VkSurfaceKHR类型的抽象表面,VkSurfaceKHR是表面对象的抽象句柄。VK_KHR_surface扩展声明了VkSurfaceKHR对象,并提供了销毁VkSurfaceKHR对象的函数。单独的特定于平台的扩展都提供了一个函数,用于为各自的平台创建VkSurfaceKHR对象。从应用程序的角度来看,这是一个不透明的句柄,就像其他Vulkan对象的句柄一样。例如在Windows平台下创建一个抽象表面,首先与创建实例等过程中需要填写创建VkSurfaceKHR所需的结构体信息。
// Provided by VK_KHR_win32_surface
typedef struct VkWin32SurfaceCreateInfoKHR {
VkStructureType sType;
const void* pNext;
VkWin32SurfaceCreateFlagsKHR flags;
HINSTANCE hinstance;
HWND hwnd;
} VkWin32SurfaceCreateInfoKHR;
VkWin32SurfaceCreateInfoKHR createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR;
createInfo.hwnd = glfwGetWin32Window(window);
createInfo.hinstance = GetModuleHandle(nullptr);
- sType是标识该结构的VkStructureType值。
- pNext为NULL或指向扩展该结构的结构的指针。
- Flags保留以供将来使用。
- hinstance是Win32的HINSTANCE,在Windows用于标记或者说记录一个程序的实例,她与HMODULE是一样的,这两种类型最终都是无符号长整型;用于与surface相关联的window。
- hwnd是Win32的HWND,HWND同样是个数据类型,可以把她看作像是身份证号一样,是唯一的标识,当窗口被创建的时候会分配一个句柄,这个句柄是唯一的,可以通过这个句柄找到窗口的各种属性,用于与surface相关联的window。
之后,可以使用vkCreateWin32SurfaceKHR创建表面,其中包括实例的参数,表面创建细节,自定义分配器和表面句柄要存储的变量。从技术上讲,这是一个WSI扩展函数,但它是如此常用,以至于标准的Vulkan加载程序包含它,因此与其他扩展不同,您不需要显式加载它。
// Provided by VK_KHR_win32_surface
VkResult vkCreateWin32SurfaceKHR(
VkInstance instance,
const VkWin32SurfaceCreateInfoKHR* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkSurfaceKHR* pSurface);
GLFW提供了创建surface的函数调用glfwCreateWindowSurface(instance, window, nullptr, &surface),接受简单的参数而不是结构体,这使得函数的实现非常简单。注意要在销毁实例之前销毁surface。
之前在创建逻辑设备的过程中我们选择相应的队列族中队列在创建逻辑设备时同步创建了所使用的队列,之前我们只创建了支持图形操作的队列。其支持的图形操作具体包括:
- 创建和管理各种资源,如纹理、缓冲和着色器。
- 渲染命令,包括准备、启动和提交渲染任务。
- 控制渲染状态,如设置渲染目标、绑定图形管道和设置渲染状态等。
- 创建和管理指令缓冲区,记录并提交图形操作到队列。
- 内存管理,包括分配和释放内存,以及将数据传输到设备内存。
- 处理事件和同步,如信号量和门信号。
现在为了确保设备可以将图像呈现到我们创建的表面。由于Present这是一个特定于队列的特性,因此问题实际上是找到一个支持向我们创建的Surface显示的队列族。 我们通过下面的函数来查询物理设备中的队列族是否支持Surface显示:
VkResult vkGetPhysicalDeviceSurfaceSupportKHR(
VkPhysicalDevice physicalDevice,
uint32_t queueFamilyIndex,
VkSurfaceKHR surface,
VkBool32* pSupported);
交换链
Vulkan没有“默认framebuffer”的概念,因此它需要一个持有一定缓冲区的工具,在我们看到渲染的图像之前先将图像渲染到这个上面。这个工具被称为交换链,必须在Vulkan中明确创建。交换链本质上是一个等待显示到屏幕上的图像队列。我们的应用程序将获取这样一个图像来绘制它,然后将它返回到队列。队列的具体工作方式以及显示图像的条件取决于交换链的设置方式,但交换链的一般用途是将图像的显示与屏幕的刷新率同步。
检查交换链支持
由于各种原因,并非所有显卡都能够将图像直接显示到屏幕上,例如,有些是为服务器设计的,没有任何显示输出。其次,由于图像呈现与窗口系统和与窗口相关的表面紧密相连,因此它实际上不是Vulkan核心的一部分。您必须在查询其支持后启用VK_KHR_swapchain设备扩展。
为此,我们将首先扩展isDeviceSuitable函数,以检查是否支持此扩展。我们之前已经看到了如何列出VkPhysicalDevice支持的扩展,所以这应该是相当简单的。请注意,Vulkan头文件提供了一个很好的宏VK_KHR_SWAPCHAIN_EXTENSION_NAME,定义为VK_KHR_swapchain。使用这个宏的好处是编译器会发现拼写错误。
在之前选择物理设备时我们要选择支持VK_KHR_SWAPCHAIN_EXTENSION_NAME的物理设备,我们可以通过vkEnumerateDeviceExtensionPropertiesh函数获取设备支持的所有扩展来确定,与创建实例过程中扩展的选择类似。
// Provided by VK_VERSION_1_0
VkResult vkEnumerateDeviceExtensionProperties(
VkPhysicalDevice physicalDevice,
const char* pLayerName,
uint32_t* pPropertyCount,
VkExtensionProperties* pProperties);
使用swapchain需要首先启用VK_KHR_swapchain扩展。启用扩展只需要对逻辑设备创建结构做一个小的改变:
createInfo.enabledExtensionCount = static_cast<uint32_t>(deviceExtensions.size());
createInfo.ppEnabledExtensionNames = deviceExtensions.data();
查询交换链支持的信息
仅仅检查交换链是否可用是不够的,因为它实际上可能与我们的window表面不兼容。创建交换链还涉及比创建实例和设备多得多的设置,因此在继续之前,我们需要查询更多的细节。我们基本上需要检查三种属性:
- 基本表面功能(交换链中的最小/最大图像数量,图像的最小/最大宽度和高度)
- 表面格式(像素格式、色彩空间)
- 可用的表示模式
// Provided by VK_KHR_surface
VkResult vkGetPhysicalDeviceSurfaceCapabilitiesKHR(
VkPhysicalDevice physicalDevice,
VkSurfaceKHR surface,
VkSurfaceCapabilitiesKHR* pSurfaceCapabilities);
我们通过vkGetPhysicalDeviceSurfaceCapabilitiesKHR获取交换链的基本功能,其中PhysicalDevice是与要创建的交换链关联的物理设备,如vkCreateSwapchainKHR所述。Surface是将关联到交换链的表面。pSurfaceCapabilities是一个指向VkSurfaceCapabilitiesKHR结构的指针。
// Provided by VK_KHR_surface
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;
- minImageCount是指定设备为surface创建的swapchain支持的最小图像数量,至少是一个。
- maxImageCount是指定设备为surface创建的swapchain所支持的最大图像数量,取值为0或大于等于minImageCount。值为0意味着图像的数量没有限制,尽管可能会有与可展示图像所使用的总内存量有关的限制。
- currentExtent是当前表面的宽度和高度,或者特殊值(0xF有序列表FFFFFFF, 0xFFFFFFFF),表示表面大小将由针对该表面的交换链的范围决定。
- minImageExtent包含指定设备表面的最小有效交换链区。除非currentexcontent具有上述特殊值,否则范围的宽度和高度都小于或等于currentexcontent对应的宽度和高度。
- maxImageExtent包含指定设备上表面的最大有效交换链范围。范围的宽度和高度将分别大于或等于minImageExtent的相应宽度和高度。范围的宽度和高度将分别大于或等于currentExtent的相应宽度和高度,除非currentExtent具有上面描述的特殊值。
- maxImageArrayLayers是为这个设备和面创建的交换链所能呈现的图像的最大层数,并且至少是一个。
- supportedTransforms是VkSurfaceTransformFlagBitsKHR的位掩码,表示指定设备上表面支持的表示转换。至少会设置一个位。
- currentTransform是VkSurfaceTransformFlagBitsKHR值,指示表面相对于呈现引擎的自然方向的当前转换。
- supportedCompositeAlpha是VkCompositeAlphaFlagBitsKHR的位掩码,表示表示引擎对指定设备表面支持的alpha合成模式,并且至少会设置一个位。在任何alpha合成模式下,通过使用没有alpha组件的图像格式,或者通过确保可呈现图像中的所有像素的alpha值为1.0,都可以实现不透明合成。
- supportedUsageFlags是VkImageUsageFlagBits的位掩码,表示应用程序可以使用由VkPresentModeKHR设置为VK_PRESENT_MODE_IMMEDIATE_KHR, VK_PRESENT_MODE_MAILBOX_KHR, VK_PRESENT_MODE_FIFO_KHR或VK_PRESENT_MODE_FIFO_RELAXED_KHR创建的交换链的可呈现图像的方式。VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT必须包含在集合中。实现可能支持其他用法。
下一步是查询支持的表示格式。因为这是一个结构体列表,所以它遵循两个函数调用的熟悉流程:
// Provided by VK_KHR_surface
VkResult vkGetPhysicalDeviceSurfaceFormatsKHR(
VkPhysicalDevice physicalDevice,
VkSurfaceKHR surface,
uint32_t* pSurfaceFormatCount,
VkSurfaceFormatKHR* pSurfaceFormats);
// Provided by VK_KHR_surface
typedef struct VkSurfaceFormatKHR {
VkFormat format;
VkColorSpaceKHR colorSpace;
} VkSurfaceFormatKHR;
最后,查询支持的呈现模式:
// Provided by VK_KHR_surface
VkResult vkGetPhysicalDeviceSurfacePresentModesKHR(
VkPhysicalDevice physicalDevice,
VkSurfaceKHR surface,
uint32_t* pPresentModeCount,
VkPresentModeKHR* pPresentModes);
// Provided by VK_KHR_surface
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,
// Provided by VK_KHR_shared_presentable_image
VK_PRESENT_MODE_SHARED_DEMAND_REFRESH_KHR = 1000111000,
// Provided by VK_KHR_shared_presentable_image
VK_PRESENT_MODE_SHARED_CONTINUOUS_REFRESH_KHR = 1000111001,
} VkPresentModeKHR;
现在我们已经确定了物理设备支持的所有交换链信息,所以让我们再次扩展物理设备的检查函数,以利用这个函数来验证物理设备的交换链支持是否足够。如果至少有一种支持的图像格式和一种支持的表示模式,那么交换链支持就足够了。
配置交换链信息
对于交换链的配置存在许多不同的最优性模式。现在,我们将编写两个函数来找到最佳交换链的正确设置。有三种类型的设置需要确定:
- 表面格式(颜色深度)
- 呈现模式(将图像“交换”到屏幕的条件)
- 交换范围(交换链中图像的分辨率)
表面格式
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];
}
每一个 VkSurfaceFormatKHR 条目包含了一个 format 和 colorSpace 成员变量。format 成员变量用于指定 颜色通道和存储类型。比如,如果 fomat 成员变量的值为 VK_FORMAT_B8G8R8A8_UNORM 表示我们以 B,G,R 和 A 的顺序,每个颜色通道用 8 位无符号整型数表示,总共每像素使用 32 位表示。colorSpace 成员变量用来表示 SRGB 颜色空间是否被支持,是否使用 VK_COLOR_SPACE_SRGB_NONLINEAR_KHR标志。
呈现模式
呈现模式可以说是交换链中最重要的设置。它决定了什么条件下图像才会显示到屏幕。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) {
for (const auto& availablePresentMode : availablePresentModes) {
if (availablePresentMode == VK_PRESENT_MODE_MAILBOX_KHR) {
return availablePresentMode;
}
}
return VK_PRESENT_MODE_FIFO_KHR;
}
交换范围
交换范围是交换链图像的分辨率,它几乎总是正好等于我们绘制的窗口的像素分辨率。可能分辨率的范围定义在VkSurfaceCapabilitiesKHR结构中。Vulkan通过在currentExtent成员中设置宽度和高度来匹配窗口的分辨率。不过,有些窗口管理器允许我们在这方面有所不同,具体做法是将currentExtent的width和height设置为一个特殊的值:uint32_t的最大值。在这种情况下,我们将在minImageExtent和maxImageExtent的范围内选择与窗口最匹配的分辨率。但我们必须以正确的单位指定分辨率。
GLFW在测量尺寸时使用两个单位:像素和屏幕坐标。例如,我们之前在创建窗口时指定的分辨率{WIDTH, HEIGHT}是用屏幕坐标来度量的。Vulkan使用像素来确定图像的分辨率,所以交换链程度必须指定像素。不幸的是,如果你使用的是高DPI显示屏(如苹果的Retina显示屏),屏幕坐标与像素并不对应。相反,由于更高的像素密度,窗口的像素分辨率将大于屏幕坐标的分辨率。因此,如果Vulkan不为我们修正分辨率,我们就不能只使用原始的{WIDTH, HEIGHT}。相反,在将窗口与最小和最大图像范围进行匹配之前,我们必须使用glfwGetFramebufferSize查询窗口的像素分辨率。
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);
}
}
创建交换链
在创建交换链时,首先获取选择的交换链设置,并在逻辑设备创建之后调用:
void createSwapChain() {
SwapChainSupportDetails swapChainSupport = querySwapChainSupport(physicalDevice);
VkSurfaceFormatKHR surfaceFormat = chooseSwapSurfaceFormat(swapChainSupport.formats);
VkPresentModeKHR presentMode = chooseSwapPresentMode(swapChainSupport.presentModes);
VkExtent2D extent = chooseSwapExtent(swapChainSupport.capabilities);
}
除了这些属性,我们还必须决定在交换链中有多少图像。然而,简单地设置swapChainSupport.capabilities.minImageCount最小值意味着我们有时可能不得不等待驱动程序完成内部操作,然后才能获得另一个图像来渲染。因此,建议请求至少比最小值多一个图像,我们还应该确保在执行此操作时不超过最大图像数量,其中0是一个特殊值,表示没只要内存可以满足,我们可以使用任意数量的图像。
uint32_t imageCount = swapChainSupport.capabilities.minImageCount + 1;
if (swapChainSupport.capabilities.maxImageCount > 0 && imageCount > swapChainSupport.capabilities.maxImageCount) {
imageCount = swapChainSupport.capabilities.maxImageCount;
}
与传统的Vulkan对象一样,创建交换链对象需要填充一个大的结构体。
// Provided by VK_KHR_swapchain
typedef struct VkSwapchainCreateInfoKHR {
VkStructureType sType; //VkStructureType值,标识该结构
const void* pNext; //VkStructureType值,标识该结构
VkSwapchainCreateFlagsKHR flags; //VkSwapchainCreateFlagBitsKHR的位掩码,表示创建交换链的参数
VkSurfaceKHR surface; //交换链显示图像的表面。如果创建成功,则swapchain与surface关联
uint32_t minImageCount; //应用程序需要的最小可展示图像数量。该实现要么用至少这个数目的图像创建swapchain,要么创建该swapchain失败。
VkFormat imageFormat; //一个VkFormat值,指定创建swapchain图像时使用的格式
VkColorSpaceKHR imageColorSpace; //一个VkColorSpaceKHR值,指定了swapchain解释图像数据的方式
VkExtent2D imageExtent; //wapchain图像的大小(单位为像素)。如果图像范围与vkGetPhysicalDeviceSurfaceCapabilitiesKHR返回的表面的当前内容不匹配,则行为是平台相关的
uint32_t imageArrayLayers; //多视图/立体曲面中的视图数。对于非立体3d应用,此值为1。
VkImageUsageFlags imageUsage; //VkImageUsageFlagBits的位掩码,描述(获得的)交换链图像的预期用途
VkSharingMode imageSharingMode; //交换链图像使用的共享模式。
uint32_t queueFamilyIndexCount; //当imageSharingMode为VK_SHARING_MODE_CONCURRENT时,可以访问交换链图像的队列族的数量。
const uint32_t* pQueueFamilyIndices; //当imageSharingMode为VK_SHARING_MODE_CONCURRENT时,指向可以访问交换链图像的队列族索引数组的指针。
VkSurfaceTransformFlagBitsKHR preTransform; //描述相对于显示引擎的自然方向的转换,在显示之前应用于图像内容。如果它不匹配vkgetphysicaldevicessurfacecapabilitieskhr返回的currentTransform值,显示引擎将转换图像内容作为显示操作的一部分。
VkCompositeAlphaFlagBitsKHR compositeAlpha; //一个VkCompositeAlphaFlagBitsKHR值,指示当此表面与某些窗口系统上的其他表面一起合成时使用的alpha合成模式。
VkPresentModeKHR presentMode; //交换链将使用的显示模式。交换链的present模式决定如何处理传入的present请求并在内部排队。
VkBool32 clipped; //指定是否允许Vulkan实现放弃影响表面不可见区域的渲染操作。
VkSwapchainKHR oldSwapchain; //是VK_NULL_HANDLE,或当前与surface关联的现有未销毁的交换链。提供有效的oldSwapchain可能有助于资源重用,并且还允许应用程序仍然显示已经从它获得的任何图像。
} VkSwapchainCreateInfoKHR;
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 成员变量用于指定每个图像所包含的层次。通常,来说它的值为 1。但对于 VR 相关的应用程序来说,会使用更多的layer。imageUsage 成员变量用于指定我们将在图像上进行怎样的操作。我们在图像上进行绘制操作,也就是将图像作为一个颜色附着来使用。如果读者对图像进行后期处理之类的操作,可以 使用 VK_IMAGE_USAGE_TRANSFER_DST_BIT 作为 imageUsage 成员变量的值,让交换链图像可以作为传输的目的图像。
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
我们可以为交换链中的图像指定一个固定的变换操作 (需要交换链具有 supportedTransforms 特性),比如顺时针 旋转 90 度或是水平翻转。如果读者不需要进行任何变换操作,指定使用 currentTransform 变换即可。
createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR
compositeAlpha 成员变量用于指定 alpha 通道是否被用来和窗口系统中的其它窗口进行混合操作。通常,我们将 其设置为 VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR 来忽略掉 alpha 通道。
createInfo.presentMode = presentMode;
createInfo.clipped = VK_TRUE;
presentMode 成员变量用于设置呈现模式。clipped 成员变量被设置为 VK_TRUE 表示我们不关心被窗口系统中 的其它窗口遮挡的像素的颜色,这允许 Vulkan 采取一定的优化措施,但如果我们回读窗口的像素值就可能出现问题。
createInfo.oldSwapchain = VK_NULL_HANDLE;
最后是 oldSwapchain 成员变量,需要指定它,是因为应用程序在运行过程中交换链可能会失效。比如,改变窗口 大小后,交换链需要重建,重建时需要之前的交换链,具体细节,我们会在之后的章节详细介绍。现在,我们还没有 创建任何一个交换链,将它设置为 VK_NULL_HANDLE 即可。
调用 vkCreateSwapchainKHR 函数创建交换链:
if (vkCreateSwapchainKHR(device, &createInfo, nullptr, &swapChain) != VK_SUCCESS) {
throw std::runtime_error("failed to create swap chain!");
}
vkCreateSwapchainKHR 函数的参数依次是逻辑设备对象,交换链创建信息,可选的自定义内存分配器和用 于存储返回的交换链对象的内存地址。接着,我们需要在 cleanup 函数中在逻辑设备被清除前调用 vkDestroySwapchainKHR 函数来清除交换链对象:
vkDestroySwapchainKHR(device, swapChain, nullptr);
获取交换链图像
我们已经创建了交换链,接下来需要做地就是获取交换链图像的图像句柄。我们会在之后使用这些图像句柄进行渲染操作。我们在 createSwapChain 函数的尾部,vkCreateSwapchainKHR 函数调用之后,添加代码来获取交换链图像句 柄。获取它们的方法和获取其它 Vulkan 对象的方法类似,首先获取交换链图像的数量,然后分配数组空间,获取交换链图像句柄。
vkGetSwapchainImagesKHR(device, swapChain, &imageCount, nullptr);
swapChainImages.resize(imageCount);
vkGetSwapchainImagesKHR(device, swapChain, &imageCount, swapChainImages.data());
创建图像视图
要在渲染管线中使用任何VkImage,包括交换链中的VkImage,我们必须创建一个VkImageView对象。VkImageView描述了如何访问图像以及访问图像的哪个部分。比如,图像可以被图像视图描述 为一个没有细化级别的二维深度纹理,进而可以在其上进行与二维深度纹理相关的操作。
现在我们为交换链中的每一个VkImage创建相应的VkImageView:
void createImageViews() {
swapChainImageViews.resize(swapChainImages.size());
for (size_t i = 0; i < swapChainImageViews.size(); i++) {
VkImageViewCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
createInfo.image = swapChainImages[i];
createInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
createInfo.format = swapChainImageFormat;
createInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.components.g = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.components.a = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
createInfo.subresourceRange.baseMipLevel = 0;
createInfo.subresourceRange.levelCount = 1;
createInfo.subresourceRange.baseArrayLayer = 0;
createInfo.subresourceRange.layerCount = 1;
if (vkCreateImageView(device, &createInfo, nullptr, &swapChainImageViews[i]) != VK_SUCCESS) {
throw std::runtime_error("failed to create image views!");
}
}
}
视图继承了一些图像创建参数。特别是,图像视图的创建继承了指定允许使用的图像视图的隐式参数用法,默认情况下,该用法使用在图像创建时在VkImageCreateInfo中指定的相应用法参数的值。隐式用法可以通过添加一个VkImageViewUsageCreateInfo结构到pNext链来覆盖,但是视图用法必须是图像用法的一个子集。如果图像具有深度模板格式,并且是使用VkImageStencilUsageCreateInfo结构(包含在VkImageCreateInfo的pNext链中)创建的,实际用法由subresource.aspectMask决定。
// Provided by VK_VERSION_1_0
typedef struct VkImageViewCreateInfo {
VkStructureType sType; //sType是一个VkStructureType值,标识该结构。
const void* pNext; //pNext是NULL或指向扩展该结构的结构的指针。
VkImageViewCreateFlags flags; //flags是VkImageViewCreateFlagBits的位掩码,指定图像视图的其他参数
VkImage image; //image是一个VkImage,视图将在其上创建。
VkImageViewType viewType; //指定图像视图的类型。
VkFormat format; //指定了用于解释图像的纹元块的格式和类型
VkComponentMapping components; //指定颜色组件的重映射(或深度或模板组件转换为颜色组件后的重映射)
VkImageSubresourceRange subresourceRange; //选择了视图可访问的mipmap级别和数组层的集合
} VkImageViewCreateInfo;
代码
#define GLFW_INCLUDE_VULKAN
#include<GLFW/glfw3.h>
#include <exception>
#include <iostream>
#include <vector>
#include <optional>
#include <set>
#include <algorithm>
#define WIDTH 800
#define HEIGHT 600
#if NDEBUG
const bool enableValidationLayers = false;
#else
const bool enableValidationLayers = true;
#endif // enableValidationLayers
const std::vector<const char*> validationLayers = {
"VK_LAYER_KHRONOS_validation"
};
const std::vector<const char*> deviceExtensions = {
VK_KHR_SWAPCHAIN_EXTENSION_NAME
};
VkResult CreateDebugUtilsMessengerEXT(VkInstance instance, const VkDebugUtilsMessengerCreateInfoEXT* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDebugUtilsMessengerEXT* pCallback) {
auto func = (PFN_vkCreateDebugUtilsMessengerEXT)
vkGetInstanceProcAddr(instance, "vkCreateDebugUtilsMessengerEXT");
if (func != nullptr) {
return func(instance, pCreateInfo, pAllocator, pCallback);
}
else {
return VK_ERROR_EXTENSION_NOT_PRESENT;
}
}
void DestroyDebugUtilsMessengerEXT(VkInstance instance, VkDebugUtilsMessengerEXT debugMessenger, const VkAllocationCallbacks* pAllocator) {
auto func = (PFN_vkDestroyDebugUtilsMessengerEXT)vkGetInstanceProcAddr(instance, "vkDestroyDebugUtilsMessengerEXT");
if (func != nullptr) {
func(instance, debugMessenger, pAllocator);
}
}
struct QueueFamiliyIndices {
std::optional<uint32_t> graphicsFamily;
std::optional<uint32_t> presentFamily;
bool isCompletion() {
return graphicsFamily.has_value() && presentFamily.has_value();
}
};
struct SwapChainSupportDetails {
VkSurfaceCapabilitiesKHR capabilities;
std::vector<VkSurfaceFormatKHR> formats;
std::vector<VkPresentModeKHR> presentModes;
};
class HelloVulkanApplication {
public:
void run() {
initWindow();
initVulkan();
mainLoop();
cleanup();
}
private:
GLFWwindow* window;
VkInstance instance;
VkSurfaceKHR surface;
VkDebugUtilsMessengerEXT debugMessenger;
VkPhysicalDevice physicalDevice;
VkDevice device;
VkQueue graphicsQueue;
VkQueue presentQueue;
VkSwapchainKHR swapChain;
std::vector<VkImage> swapchainImages;
VkFormat swapChainImageFormat;
VkExtent2D swapChainExtent;
void initWindow() {
glfwInit();
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
window = glfwCreateWindow(WIDTH, HEIGHT, "Learn Vulkan", nullptr, nullptr);
}
void initVulkan() {
createInstance();
setupDebugMessenger(); //设置vulkan使用过程中的调试信息
createSurface();
pickPhysicalDevice();
createLogicalDevice();
createSwapChain();
}
void mainLoop() {
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
}
}
void cleanup() {
if (enableValidationLayers) {
DestroyDebugUtilsMessengerEXT(instance, debugMessenger, nullptr);
}
vkDestroySwapchainKHR(device, swapChain, nullptr);
vkDestroyDevice(device, nullptr);
vkDestroySurfaceKHR(instance, surface, nullptr);
vkDestroyInstance(instance, nullptr);
glfwDestroyWindow(window);
glfwTerminate();
}
void createInstance() {
if (enableValidationLayers && !checkValidationLayerSupport()) {
throw std::runtime_error("validation layers requested, but not available");
}
VkApplicationInfo appInfo = {};
appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
appInfo.pApplicationName = "Hello Vulkan";
appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.pEngineName = "No Engine";
appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.apiVersion = VK_API_VERSION_1_0;
VkInstanceCreateInfo createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
createInfo.pApplicationInfo = &appInfo;
std::vector<const char*> extensions = getRequireExtensions();
createInfo.enabledExtensionCount = extensions.size();
createInfo.ppEnabledExtensionNames = extensions.data();
if (enableValidationLayers) {
createInfo.enabledLayerCount = validationLayers.size();
createInfo.ppEnabledLayerNames = validationLayers.data();
// 调试vkCreateInstance和vkDestroyInstance
VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo{};
populateDebugMessengerCreateInfo(debugCreateInfo);
createInfo.pNext = &debugCreateInfo;
}
else {
createInfo.enabledLayerCount = 0;
createInfo.pNext = nullptr;
}
VkResult result = vkCreateInstance(&createInfo, nullptr, &instance);
if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) {
throw std::runtime_error("Failed to create Vulkan Instance!");
}
}
std::vector<const char*> getRequireExtensions() {
uint32_t glfwExtensionCount = 0;
const char** glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);
std::vector<const char*> extensions(glfwExtensions, glfwExtensions + glfwExtensionCount);
if (enableValidationLayers) {
extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
}
return extensions;
}
void populateDebugMessengerCreateInfo(VkDebugUtilsMessengerCreateInfoEXT& createInfo) {
createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
createInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT;
createInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;
createInfo.pfnUserCallback = debugCallback;
}
void setupDebugMessenger() {
if (!enableValidationLayers) return;
VkDebugUtilsMessengerCreateInfoEXT createInfo;
populateDebugMessengerCreateInfo(createInfo);
if (CreateDebugUtilsMessengerEXT(instance, &createInfo, nullptr, &debugMessenger) != VK_SUCCESS) {
throw std::runtime_error("failed to set up debug messenger!");
}
}
bool checkValidationLayerSupport() {
uint32_t layertCount;
vkEnumerateInstanceLayerProperties(&layertCount, nullptr);
std::vector<VkLayerProperties> availabelLayers(layertCount);
vkEnumerateInstanceLayerProperties(&layertCount, availabelLayers.data());
for (auto& validationlayer : validationLayers) {
bool layerFound = false;
for (auto& availabelayer : availabelLayers) {
if (strcmp(validationlayer, availabelayer.layerName) == 0) {
layerFound = true;
break;
}
}
if (!layerFound) {
return false;
}
}
return true;
}
static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback(VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, VkDebugUtilsMessageTypeFlagsEXT messageType, const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, void* pUserData) {
std::cerr << "validation layer: " << pCallbackData->pMessage << std::endl;
return VK_FALSE;
}
void createSurface() {
if (glfwCreateWindowSurface(instance, window, nullptr, &surface) != VK_SUCCESS) {
throw std::runtime_error("failed to create window surface!");
}
}
void pickPhysicalDevice() {
uint32_t deivceCount = 0;
vkEnumeratePhysicalDevices(instance, &deivceCount, 0);
std::vector<VkPhysicalDevice> Devices(deivceCount);
vkEnumeratePhysicalDevices(instance, &deivceCount, Devices.data());
for (auto& device : Devices) {
if (isDeviceSuitable(device)) {
physicalDevice = device;
break;
}
}
if (physicalDevice == VK_NULL_HANDLE) {
throw std::runtime_error("failed to find a suitable GPU!");
}
}
bool isDeviceSuitable(VkPhysicalDevice device) {
QueueFamiliyIndices indices = findQueueFamilies(device);
bool extensionSupported = checkDeviceExtensionSupport(device);
bool swapChainAdequate = false;
if (extensionSupported) {
SwapChainSupportDetails swapChainSupport = querySwapChainSupport(device);
swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.presentModes.empty();
}
return indices.isCompletion() && extensionSupported && swapChainAdequate;
}
QueueFamiliyIndices findQueueFamilies(VkPhysicalDevice device) {
QueueFamiliyIndices indice;
uint32_t queueFamilyCount = 0;
vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, nullptr);
std::vector<VkQueueFamilyProperties> queueFamilies(queueFamilyCount);
vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, queueFamilies.data());
int i = 0;
for (const auto& queueFamily : queueFamilies) {
VkBool32 presentSupport = false;
vkGetPhysicalDeviceSurfaceSupportKHR(device, i, surface, &presentSupport);
if (presentSupport) {
indice.presentFamily = i;
}
if (queueFamily.queueCount > 0 && queueFamily.queueFlags & VK_QUEUE_GRAPHICS_BIT && presentSupport) {
indice.graphicsFamily = i;
}
if (indice.isCompletion()) {
break;
}
i++;
}
return indice;
}
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();
}
void createLogicalDevice() {
QueueFamiliyIndices indice = findQueueFamilies(physicalDevice);
std::vector<VkDeviceQueueCreateInfo> queueCreateInfos;
std::set<uint32_t> uniqueueFamilies = { indice.graphicsFamily.value(),indice.presentFamily.value() };
float queuePriority = 1.0f;
for (uint32_t queueFamily : uniqueueFamilies) {
VkDeviceQueueCreateInfo queueCreateInfo = {};
queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
queueCreateInfo.queueFamilyIndex = queueFamily;
queueCreateInfo.queueCount = 1;
queueCreateInfo.pQueuePriorities = &queuePriority;
queueCreateInfos.push_back(queueCreateInfo);
}
VkPhysicalDeviceFeatures deviceFeatures{};
VkDeviceCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
createInfo.pQueueCreateInfos = queueCreateInfos.data();
createInfo.queueCreateInfoCount = 1;
createInfo.pEnabledFeatures = &deviceFeatures;
createInfo.enabledExtensionCount = deviceExtensions.size();
createInfo.ppEnabledExtensionNames = deviceExtensions.data();
if (enableValidationLayers) {
createInfo.enabledLayerCount = static_cast<uint32_t>(validationLayers.size());
createInfo.ppEnabledLayerNames = validationLayers.data();
}
else {
createInfo.enabledLayerCount = 0;
}
if (vkCreateDevice(physicalDevice, &createInfo, nullptr, &device) != VK_SUCCESS) {
throw std::runtime_error("failed to create logica device!");
}
vkGetDeviceQueue(device, indice.graphicsFamily.value(), 0, &graphicsQueue);
vkGetDeviceQueue(device, indice.presentFamily.value(), 0, &presentQueue);
}
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;
}
VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector<VkSurfaceFormatKHR>& availabelFormats) {
for (const auto& availabelFormat : availabelFormats) {
if (availabelFormat.format == VK_FORMAT_B8G8R8A8_SRGB && availabelFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) {
return availabelFormat;
}
}
return availabelFormats[0];
}
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;
}
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);
}
}
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 + 1;
if (swapChainSupport.capabilities.minImageCount > 0 && imageCount > swapChainSupport.capabilities.maxImageCount) {
imageCount = swapChainSupport.capabilities.maxImageCount;
}
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;
QueueFamiliyIndices 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;
createInfo.pQueueFamilyIndices = nullptr;
}
createInfo.preTransform = swapChainSupport.capabilities.currentTransform;
createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
createInfo.presentMode = presentMode;
createInfo.clipped = VK_TRUE;
createInfo.oldSwapchain = VK_NULL_HANDLE;
if (vkCreateSwapchainKHR(device, &createInfo, nullptr, &swapChain) != VK_SUCCESS) {
throw std::runtime_error("failed to create swap chain!");
}
vkGetSwapchainImagesKHR(device, swapChain, &imageCount, nullptr);
swapchainImages.resize(imageCount);
vkGetSwapchainImagesKHR(device, swapChain, &imageCount, nullptr);
swapChainImageFormat = surfaceFormat.format;
swapChainExtent = extent;
}
};
int main() {
HelloVulkanApplication helloVulkan;
try {
helloVulkan.run();
}
catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}