Ch1-4 创建交换链

交换链的创建信息

vkCreateSwapchainKHR(...)创建交换链:

VkResult VKAPI_CALL vkCreateSwapchainKHR(...) 的参数说明

VkDevice device

逻辑设备的handle

const VkSwapchainCreateInfoKHR* pCreateInfo

交换链的创建信息

const VkAllocationCallbacks* pAllocator

VkSwapchainKHR* pSwapchain

若执行成功,将交换链的handle写入*pSwapchain

然后来看下VkSwapchainCreateInfoKHR

struct VkSwapchainCreateInfoKHR 的成员说明

VkStructureType sType

结构体的类型,本处必须是VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR

const void* pNext

如有必要,指向一个用于扩展该结构体的结构体

VkSwapchainCreateFlagsKHR flags

VkSurfaceKHR surface

Window surface的handle

uint32_t minImageCount

交换链中图像的最少数量

VkFormat imageFormat

交换链中图像的格式

VkColorSpaceKHR imageColorSpace

交换链中图像的色彩空间

VkExtent2D imageExtent

交换链图像的尺寸

uint32_t imageArrayLayers

对于多视点(multiview)或立体显示设备,需要提供一个视点数,对于普通的2D显示设备,该值为1

VkImageUsageFlags imageUsage

交换链中图像的用途

VkSharingMode imageSharingMode

交换链中图像的分享模式,应优先使用单个队列族独占访问VK_SHARING_MODE_EXCLUSIVE以达到最佳性能

uint32_t queueFamilyIndexCount

若imageSharingMode为VK_SHARING_MODE_CONCURRENT,这里需指定将会访问交换链图像的队列族总数

const uint32_t* pQueueFamilyIndices

续上一条,以及具体的队列族索引

VkSurfaceTransformFlagBitsKHR preTransform

对交换链图像的变换,比如旋转90°、镜像等

VkCompositeAlphaFlagBitsKHR compositeAlpha

指定如何处理交换链图像的透明度

VkPresentModeKHR presentMode

呈现方式

VkBool32 clipped

是否允许舍弃掉交换链图像应有但窗口中不会显示的像素

VkSwapchainKHR oldSwapchain

旧的交换链,在重建交换链时填入

接下来在vulkan::graphicsBase::CreateSwapchain(...)中逐步填写交换链的创建信息。

VkSurfaceCapabilitiesKHR相关的参数

VkSurfaceCapabilitiesKHR相关的参数有:交换链图像的数量、尺寸、视点数、变换、透明通道的方式、图像的用途。

首先用vkGetPhysicalDeviceSurfaceCapabilitiesKHR(...)获取window surface的VkSurfaceCapabilitiesKHR

VkSurfaceCapabilitiesKHR surfaceCapabilities = {};
if (VkResult result = vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physicalDevice, surface, &surfaceCapabilities)) {
    std::cout << std::format("[ graphicsBase ] ERROR\nFailed to get physical device surface capabilities!\nError code: {}\n", int32_t(result));
    return result;
}

vkGetPhysicalDeviceSurfaceCapabilitiesKHR(...)各个参数的含义应当一目了然,来看一下VkSurfaceCapabilitiesKHR

struct VkSurfaceCapabilitiesKHR 的成员说明

uint32_t minImageCount

交换链中图像的数量不得少于minImageCount

uint32_t maxImageCount

交换链中图像的数量不得大于maxImageCount

VkExtent2D currentExtent

当前window surface的尺寸

VkExtent2D minImageExtent

Window surface所对应的交换链图像的尺寸不得小于minImageExtent

VkExtent2D maxImageExtent

Window surface所对应的交换链图像的尺寸不得大于maxImageExtent

uint32_t maxImageArrayLayers

对于多视点(multiview)或立体显示设备,允许的最大视点数

VkSurfaceTransformFlagsKHR supportedTransforms

对于交换链中图像,支持的变形(如旋转90°、镜像等)

VkSurfaceTransformFlagBitsKHR currentTransform

当前变形

VkCompositeAlphaFlagsKHR supportedCompositeAlpha

对于交换链中图像,支持的处理透明通道的方式

VkImageUsageFlags supportedUsageFlags

对于交换链中图像,支持的用途

  • 如果currentExtent的width或height为特殊值-1,说明当前window surface的大小尚未被指定。

对于交换链图像的数量,尽量不要太少,避免阻塞(所谓阻塞,即当需要渲染一张新图像时,所有交换链图像不是正在被呈现引擎读取就是正在被渲染),但也不要太多,避免多余的显存开销。
于是,如果容许的最大数量与最小数量不等,那么使用最小数量+1:

swapchainCreateInfo.minImageCount = surfaceCapabilities.minImageCount + (surfaceCapabilities.maxImageCount > surfaceCapabilities.minImageCount);

对于图像尺寸,若当前尺寸已定,那么直接使用当前尺寸,否则使用一个由你指定的默认大小。

namespace vulkan {
    //全局常量用constexpr修饰定义在类外:
    constexpr VkExtent2D defaultWindowSize = { 1280, 720 };
    class graphicsBase {
        VkResult CreateSwapchain(bool limitFrameRate = true, VkSwapchainCreateFlagsKHR flags = 0) {
            VkSurfaceCapabilitiesKHR surfaceCapabilities = {};
            if (VkResult result = vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physicalDevice, surface, &surfaceCapabilities)) {
                std::cout << std::format("[ graphicsBase ] ERROR\nFailed to get physical device surface capabilities!\nError code: {}\n", int32_t(result));
                return result;
            }
            swapchainCreateInfo.minImageCount = surfaceCapabilities.minImageCount + (surfaceCapabilities.maxImageCount > surfaceCapabilities.minImageCount);
            swapchainCreateInfo.imageExtent =
                surfaceCapabilities.currentExtent.width == -1 ?
                VkExtent2D{
                    glm::clamp(defaultWindowSize.width, surfaceCapabilities.minImageExtent.width, surfaceCapabilities.maxImageExtent.width),
                    glm::clamp(defaultWindowSize.height, surfaceCapabilities.minImageExtent.height, surfaceCapabilities.maxImageExtent.height) } :
                surfaceCapabilities.currentExtent;
            /*待后续填充*/
        }
    };
}
  • 若window surface的大小未定,surfaceCapabilities.currentExtent的width和height应当同时为特殊值-1,因此这里只对width做判断。

视点数为1,变换使用当前变换:

swapchainCreateInfo.imageArrayLayers = 1;
swapchainCreateInfo.preTransform = = surfaceCapabilities.currentTransform;

接着指定处理交换链图像透明通道的方式,先来看一下可选值:

  • VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR表示不透明,每个像素的A通道值皆被视作1.f。

  • VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR表示将颜色值视作预乘透明度(premultiplied alpha)形式。

  • VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR表示将颜色值视作后乘透明度形式,或称直接透明度(straight alpha)形式。

  • VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR表示透明度的处理方式由应用程序的其他部分(Vulkan以外的部分)指定。

将直接透明度形式颜色值的RGB通道乘以其A通道即为预乘透明度形式,举例而言,一个恰好半透明的红色写作直接透明度形式为(1.0f ,0.0f ,0.0f ,0.5f),写作预乘透明度形式为(0.5f ,0.0f ,0.0f ,0.5f)。

在呈现时处理透明通道的方式未必能由Vulkan决定,而窗口系统可能会指定这一方式,这种情况下应当将swapchainCreateInfo.compositeAlpha设置为VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR,该bit说明程序会用窗口系统相关的指令设定处理透明通道的方式,若程序没有执行这样的指令,该bit会确保使用程序的运行平台默认的透明度处理方式(通常为使用不透明背景)。
因此,若surfaceCapabilities.supportedCompositeAlpha具有VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR则优先将swapchainCreateInfo.compositeAlpha设置为VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR,否则选择surfaceCapabilities.supportedCompositeAlpha中首个非0的bit(姑且只确保处理方式有效):

if (surfaceCapabilities.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR)
    swapchainCreateInfo.compositeAlpha = VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR;
else
    for (size_t i = 0; i < 4; i++)
        if (surfaceCapabilities.supportedCompositeAlpha & 1 << i) {
            swapchainCreateInfo.compositeAlpha = VkCompositeAlphaFlagBitsKHR(surfaceCapabilities.supportedCompositeAlpha & 1 << i);
            break;
        }

Note

如果你想使用透明背景的窗口,这里提示一种不具有普遍性的实现方法:
1.在初始化GLFW后,调用glfwWindowHint(GLFW_TRANSPARENT_FRAMEBUFFER, true),在Windows上,GLFW对此的底层实现是使用dwmapi中的DwmEnableBlurBehindWindow(...),你也可以自行调用该函数。
2.检查surfaceCapabilities.supportedCompositeAlpha,指定swapchainCreateInfo.compositeAlphaVK_COMPOSITE_ALPHA_INHERIT_BIT_KHR
3.清屏颜色必须为VkClearColorValue{ 0.f, 0.f, 0.f, 0.f }。
如果是英特尔核显,以上步骤就足够了。
如果是独显,发现背景为黑色而非透明,可能需要将surfaceCapabilities.presentMode指定为VK_PRESENT_MODE_IMMEDIATE_KHRVK_PRESENT_MODE_MAILBOX_KHR(但这么一来就不限制帧数了,可能会耗费多余算力)。
我这里没有足够的硬件做测试来找出一个普遍有效的做法,而且GLFW对此相关的功能也不算完善,因此如果透明背景对你的程序而言不可或缺的话,我推荐你尝试平台特定的图形API(比如DirectX12搭配DirectComposition)而非Vulkan。

接着是图像的用途,图像必须被用作颜色附件(VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT),最好还能被用作数据传送的目标(VK_IMAGE_USAGE_TRANSFER_DST_BIT),这样就能用vkCmdClearColorImage(...)清屏,还可以用vkCmdBlitImage(...)将图像直接搬运(而不必渲染)到屏幕上,此外还可能得被用作数据传送的来源(VK_IMAGE_USAGE_TRANSFER_SRC_BIT),比如实现窗口截屏。

swapchainCreateInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
if (surfaceCapabilities.supportedUsageFlags & VK_IMAGE_USAGE_TRANSFER_SRC_BIT)
    swapchainCreateInfo.imageUsage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
if (surfaceCapabilities.supportedUsageFlags & VK_IMAGE_USAGE_TRANSFER_DST_BIT)
    swapchainCreateInfo.imageUsage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT;
else
    std::cout << std::format("[ graphicsBase ] WARNING\nVK_IMAGE_USAGE_TRANSFER_DST_BIT isn't supported!\n");

指定图像格式

首先来填充vulkan::graphicsBase::GetSurfaceFormats(),用vkGetPhysicalDeviceSurfaceFormatsKHR(...)来取得surface的可用图像格式及色彩空间:

VkResult GetSurfaceFormats() {
    uint32_t surfaceFormatCount;
    if (VkResult result = vkGetPhysicalDeviceSurfaceFormatsKHR(physicalDevice, surface, &surfaceFormatCount, nullptr)) {
        std::cout << std::format("[ graphicsBase ] ERROR\nFailed to get the count of surface formats!\nError code: {}\n", int32_t(result));
        return result;
    }
    if (!surfaceFormatCount)
        std::cout << std::format("[ graphicsBase ] ERROR\nFailed to find any supported surface format!\n"),
        abort();
    availableSurfaceFormats.resize(surfaceFormatCount);
    VkResult result = vkGetPhysicalDeviceSurfaceFormatsKHR(physicalDevice, surface, &surfaceFormatCount, availableSurfaceFormats.data());
    if (result)
        std::cout << std::format("[ graphicsBase ] ERROR\nFailed to get surface formats!\nError code: {}\n", int32_t(result));
    return result;
}

VkSurfaceFormatKHR代表了一组可用的图像格式和色彩空间的组合:

struct VkSurfaceFormatKHR 的成员说明

VkFormat format

Window surface的图像格式

VkColorSpaceKHR colorSpace

Window surface的色彩空间

然后填充vulkan::graphicsBase::SetSurfaceFormat(...),考虑色彩空间确定但图像格式可任意,及两者都确定的情况:

VkResult SetSurfaceFormat(VkSurfaceFormatKHR surfaceFormat) {
    bool formatIsAvailable = false;
    if (!surfaceFormat.format) {
        //如果格式未指定,只匹配色彩空间,图像格式有啥就用啥
        for (auto& i : availableSurfaceFormats)
            if (i.colorSpace == surfaceFormat.colorSpace) {
                swapchainCreateInfo.imageFormat = i.format;
                swapchainCreateInfo.imageColorSpace = i.colorSpace;
                formatIsAvailable = true;
                break;
            }
    }
    else
        //否则匹配格式和色彩空间
        for (auto& i : availableSurfaceFormats)
            if (i.format == surfaceFormat.format &&
                i.colorSpace == surfaceFormat.colorSpace) {
                swapchainCreateInfo.imageFormat = i.format;
                swapchainCreateInfo.imageColorSpace = i.colorSpace;
                formatIsAvailable = true;
                break;
            }
    //如果没有符合的格式,恰好有个语义相符的错误代码
    if (!formatIsAvailable)
        return VK_ERROR_FORMAT_NOT_SUPPORTED;
    //如果交换链已存在,调用RecreateSwapchain()重建交换链
    if (swapchain)
        return RecreateSwapchain();
    return VK_SUCCESS;
}
  • 对于本函数,若只指定色彩空间而不指定格式,则输入的surfaceFormat中format应当为0(即VK_FORMAT_UNDEFINED)。

  • 不考虑不指定色彩空间的情况,一是因为色彩空间VkColorSpaceKHR的零值为VK_COLOR_SPACE_SRGB_NONLINEAR_KHR,是个有效的色彩空间,二是因为不同色彩空间对后续操作(比如贴图的图像格式、片段着色器最后的色调映射)影响很大,没理由允许任意的色彩空间。

  • 这个函数有可能在已经开始渲染的情况下执行(比如运行过程中切换到HDR),因此要考虑交换链已存在的情况。

接着继续填充vulkan::graphicsBase::CreateSwapchain(...),首先判断是否已获取surface格式,若尚未获取则调用GetSurfaceFormats():

if (availableSurfaceFormats.empty())
    if (VkResult result = GetSurfaceFormats())
        return result;

然后,若没有指定图像的格式和色彩空间(即没有在执行本函数前手动调用SetSurfaceFormat(...)),那么用SetSurfaceFormat(...)设置一个默认格式。
默认格式应当满足:RGBA四通道,每个通道8位的UNORM格式。8位表示每个通道有256个色阶,UNORM的U表示其底层数据是无符号整形,NORM表示在着色器中使用时,其数值会被转为[0, 1]区间内的小数(即被标准化)。这类格式最为普通,受到广泛支持且不会导致自动的色调映射/颜色校正。

if (!swapchainCreateInfo.imageFormat)
    //用&&操作符来短路执行
    if (SetSurfaceFormat({ VK_FORMAT_R8G8B8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR }) &&
        SetSurfaceFormat({ VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR })) {
        //如果找不到上述图像格式和色彩空间的组合,那只能有什么用什么,采用availableSurfaceFormats中的第一组
        swapchainCreateInfo.imageFormat = availableSurfaceFormats[0].format;
        swapchainCreateInfo.imageColorSpace = availableSurfaceFormats[0].colorSpace;
        std::cout << std::format("[ graphicsBase ] WARNING\nFailed to select a four-component UNORM surface format!\n");
    }
  • VK_FORMAT_R8G8B8A8_UNORMVK_FORMAT_B8G8R8A8_UNORM是仅有的两种满足前述条件的格式,其底层数据的RGBA通道排列顺序不同,但对后续的代码没有影响(除了需要操作底层数据的情况,比如截图并保存到文件),通常来说,物理设备至少会支持其中一种格式。

  • 显然,因为VK_SUCCESS为0,对多个返回VkResult的函数表达式做逻辑与意味着这堆函数表达式里只需有一个执行成功。

Note

一些早期的Vulkan教程中提到:
如果vkGetPhysicalDeviceSurfaceFormatsKHR(...)返回的VkSurfaceFormatKHR数组只有一个元素且其成员变量format的数值为VK_FORMAT_UNDEFINED时,表示surface没有偏好的图像格式,此时可以使用任何格式。
然而,早在Vulkan1.0.14标准中便已明确: pSurfaceFormats must not contain an entry whose value for format is VK_FORMAT_UNDEFINED.
因此哪怕对于Vulkan1.0而言(不关心patch版本号),前述说法也是不成立的。

指定呈现模式

首先用vkGetPhysicalDeviceSurfacePresentModesKHR(...)获取surface支持的呈现模式:

uint32_t surfacePresentModeCount;
if (VkResult result = vkGetPhysicalDeviceSurfacePresentModesKHR(physicalDevice, surface, &surfacePresentModeCount, nullptr)) {
    std::cout << std::format("[ graphicsBase ] ERROR\nFailed to get the count of surface present modes!\nError code: {}\n", int32_t(result));
    return result;
}
if (!surfacePresentModeCount)
    std::cout << std::format("[ graphicsBase ] ERROR\nFailed to find any surface present mode!\n"),
    abort();
std::vector<VkPresentModeKHR> surfacePresentModes(surfacePresentModeCount);
if (VkResult result = vkGetPhysicalDeviceSurfacePresentModesKHR(physicalDevice, surface, &surfacePresentModeCount, surfacePresentModes.data())) {
    std::cout << std::format("[ graphicsBase ] ERROR\nFailed to get surface present modes!\nError code: {}\n", int32_t(result));
    return result;
}

呈现模式有如下几种(配图改自英特尔的API without Secrets系列教程配图):

_images/IMMEDIATE.png _images/FIFO.png _images/MAILBOX.png
  • VK_PRESENT_MODE_IMMEDIATE_KHR表示立即模式,该模式下不限制帧率且帧率在所有模式中是最高的。该模式不等待垂直同步信号,一旦图片渲染完,用于呈现的图像就会被立刻替换掉,这可能导致画面撕裂。

  • VK_PRESENT_MODE_FIFO_KHR表示先入先出模式,该模式限制帧率与屏幕刷新率一致,这种模式是必定支持的。在该模式下,图像被推送进一个用于待呈现图像的队列,然后等待垂直同步信号,按顺序被推出队列并输出到屏幕,因此叫先入先出。

  • VK_PRESENT_MODE_FIFO_RELAXED_KHRVK_PRESENT_MODE_FIFO_KHR的差别在于,若屏幕上图像的停留时间长于一个刷新间隔,呈现引擎可能在下一个垂直同步信号到来前便试图将呈现队列中的图像输出到屏幕,该模式相比VK_PRESENT_MODE_FIFO_KHR更不容易引起阻塞或迟滞,但在帧率较低时可能会导致画面撕裂。

  • VK_PRESENT_MODE_MAILBOX_KHR是一种类似于三重缓冲的模式。它的待呈现图像队列中只容纳一个元素,在等待垂直同步信号期间若有新的图像入队,那么旧的图像会直接出队而不被输出到屏幕(即出队不需要等待垂直同步信号,因此不限制帧率),出现在屏幕上的总会是最新的图像。

由于VK_PRESENT_MODE_IMMEDIATE_KHRVK_PRESENT_MODE_FIFO_RELAXED_KHR可能导致画面撕裂,在不需要限制帧率时应当选择VK_PRESENT_MODE_MAILBOX_KHR,需要限制帧率使其最大不超过屏幕刷新率时应选择VK_PRESENT_MODE_FIFO_KHR

swapchainCreateInfo.presentMode = VK_PRESENT_MODE_FIFO_KHR;
if (!limitFrameRate)
    for (size_t i = 0; i < surfacePresentModeCount; i++)
        if (surfacePresentModes[i] == VK_PRESENT_MODE_MAILBOX_KHR) {
            swapchainCreateInfo.presentMode = VK_PRESENT_MODE_MAILBOX_KHR;
            break;
        }

填写剩余的参数

swapchainCreateInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
swapchainCreateInfo.flags = flags;
swapchainCreateInfo.surface = surface;
swapchainCreateInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
swapchainCreateInfo.clipped = VK_TRUE;

创建交换链

填充vulkan::graphicsBase::CreateSwapchain_Internal(),首先加入以下代码以创建交换链:

if (VkResult result = vkCreateSwapchainKHR(device, &swapchainCreateInfo, nullptr, &swapchain)) {
    std::cout << std::format("[ graphicsBase ] ERROR\nFailed to create a swapchain!\nError code: {}\n", int32_t(result));
    return result;
}

然后在vulkan::graphicsBase::CreateSwapchain(...)中调用CreateSwapchain_Internal():

if (VkResult result = CreateSwapchain_Internal())
    return result;
/*待后续填充*/
return VK_SUCCESS;

获取交换链图像

继续填充vulkan::graphicsBase::CreateSwapchain_Internal(),用vkGetSwapchainImagesKHR(...)获取交换链图像:

uint32_t swapchainImageCount;
if (VkResult result = vkGetSwapchainImagesKHR(device, swapchain, &swapchainImageCount, nullptr)) {
    std::cout << std::format("[ graphicsBase ] ERROR\nFailed to get the count of swapchain images!\nError code: {}\n", int32_t(result));
    return result;
}
swapchainImages.resize(swapchainImageCount);
if (VkResult result = vkGetSwapchainImagesKHR(device, swapchain, &swapchainImageCount, swapchainImages.data())) {
    std::cout << std::format("[ graphicsBase ] ERROR\nFailed to get swapchain images!\nError code: {}\n", int32_t(result));
    return result;
}

为交换链图像创建Image View

图像视图(VkImageView)定义了图像的使用方式 。
注意这里所谓的“视图”不过是个编程术语,并不是实际用来观察图像的所谓视图,正如C++中的std::string_view定义字符串的访问方式一样。为消歧义,后文以英语image view称呼。
关于image view的具体说明见Image View(暂时不用对其做封装)。

填写VkImageViewCreateInfo,用vkCreateImageView(...)创建image view,整个vulkan::graphicsBase::CreateSwapchain_Internal()如下:

VkResult CreateSwapchain_Internal() {
    if (VkResult result = vkCreateSwapchainKHR(device, &swapchainCreateInfo, nullptr, &swapchain)) {
        std::cout << std::format("[ graphicsBase ] ERROR\nFailed to create a swapchain!\nError code: {}\n", int32_t(result));
        return result;
    }

    //获取交换连图像
    uint32_t swapchainImageCount;
    if (VkResult result = vkGetSwapchainImagesKHR(device, swapchain, &swapchainImageCount, nullptr)) {
        std::cout << std::format("[ graphicsBase ] ERROR\nFailed to get the count of swapchain images!\nError code: {}\n", int32_t(result));
        return result;
    }
    swapchainImages.resize(swapchainImageCount);
    if (VkResult result = vkGetSwapchainImagesKHR(device, swapchain, &swapchainImageCount, swapchainImages.data())) {
        std::cout << std::format("[ graphicsBase ] ERROR\nFailed to get swapchain images!\nError code: {}\n", int32_t(result));
        return result;
    }

    //创建image view
    swapchainImageViews.resize(swapchainImageCount);
    VkImageViewCreateInfo imageViewCreateInfo = {
        .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
        .viewType = VK_IMAGE_VIEW_TYPE_2D,
        .format = swapchainCreateInfo.imageFormat,
        //.components = {},//四个成员皆为VK_COMPONENT_SWIZZLE_IDENTITY
        .subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }
    };
    for (size_t i = 0; i < swapchainImageCount; i++) {
        imageViewCreateInfo.image = swapchainImages[i];
        if (VkResult result = vkCreateImageView(device, &imageViewCreateInfo, nullptr, &swapchainImageViews[i])) {
            std::cout << std::format("[ graphicsBase ] ERROR\nFailed to create a swapchain image view!\nError code: {}\n", int32_t(result));
            return result;
        }
    }
    return VK_SUCCESS;
}

重建交换链

有些情况下会需要重建交换链,比如开关HDR,或者窗口大小改变。
改变色彩空间的情况已经在vulkan::graphicsBase::SetSurfaceFormat(...)中处理了,现在来处理窗口大小改变的情况,填充vulkan::graphicsBase::RecreateSwapchain():

VkSurfaceCapabilitiesKHR surfaceCapabilities = {};
if (VkResult result = vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physicalDevice, surface, &surfaceCapabilities)) {
    std::cout << std::format("[ graphicsBase ] ERROR\nFailed to get physical device surface capabilities!\nError code: {}\n", int32_t(result));
    return result;
}
if (surfaceCapabilities.currentExtent.width == 0 ||
    surfaceCapabilities.currentExtent.height == 0)
    return VK_SUBOPTIMAL_KHR;
swapchainCreateInfo.imageExtent = surfaceCapabilities.currentExtent;
  • 如果窗口显示区域的长或宽为0(通常发生在最小化到任务栏窗口时),不重建交换链(若所创建图像的大小为0,特定显卡驱动可能报错),留待窗口大小非0时重建,函数执行视为不完全成功返回VK_SUBOPTIMAL_KHR(该返回值意味着交换链图像与window surface的属性不匹配,与之后获取交换链图像索引时可能遭遇的情况相符)。

之后向swapchainCreateInfo.oldSwapchain填入旧交换链的handle,这样可能会有利于重用一些资源。

swapchainCreateInfo.oldSwapchain = swapchain;

在重建交换链前,须确保程序没有正在使用旧的交换链,在渲染循环(rendering loop)中,交换链的使用并没有明确的停滞时机,因此需要等待逻辑设备闲置,或者更精细点,等待图形和呈现队列闲置(交换链图像被图形队列写入,被呈现队列读取),这么一来计算队列就可以在重建交换链时继续其任务。
vkQueueWaitIdle(...)等待队列闲置:

VkResult result = vkQueueWaitIdle(queue_graphics);
//仅在等待图形队列成功,且图形与呈现所用队列不同时等待呈现队列
if (!result &&
    queue_graphics != queue_presentation)
    result = vkQueueWaitIdle(queue_presentation);
if (result) {
    std::cout << std::format("[ graphicsBase ] ERROR\nFailed to wait for the queue to be idle!\nError code: {}\n", int32_t(result));
    return result;
}

接着用vkDestroyImageView(...)销毁旧有的image view:

void VKAPI_CALL vkDestroyImageView(...) 的参数说明

VkDevice device

逻辑设备的handle

VkImageView imageView

被销毁的image view的handle

const VkAllocationCallbacks* pAllocator

如有必要,指向描述自定义内存分配方式的结构体

/*待后续填充*/
for (auto& i : swapchainImageViews)
    if (i)
        vkDestroyImageView(device, i, nullptr);
swapchainImageViews.resize(0);
  • 类似各种vkCreate函数,各种vkDestroy函数也有个pAllocator,本套教程中一概为nullptr

  • 销毁前验证handle的值,销毁后将swapchainImageViews清空,防止本函数执行失败(虽然不太可能)后再次调用时发生重复销毁,此外,之后将0大小的swapchainImageViews重新扩容到可容纳swapchainImageCount个元素时,会填充swapchainImageCountVK_NULL_HANDLE(零值)作为默认值。

值得注意的是,这里只销毁了image view,而没销毁其对应的交换链图像,这是因为在销毁旧交换链时会被一并销毁交换链图像。而尽管图形和呈现队列都没有在使用旧图像,无法确保呈现引擎在CPU上做的操作或不会使用旧图像,因此销毁旧交换链的时机需要延后,之后会在Ch2-1 Rendering Loop中进行相应处理。

最后调用CreateSwapchain_Internal()来创建交换链:

if (result = CreateSwapchain_Internal())
    return result;
/*待后续填充*/
return VK_SUCCESS;

创建和销毁交换链时的回调函数

重建交换链时,与之相关的各种资源也要一并重建,这种情况下使用回调函数是较好的做法。
使用回调函数有助于提升程序的可维护性,举例而言,不使用回调函数的话,若你想写一个可选择是否使用的渲染管线(且该管线直接使用交换链图像,或所用图像大小与交换链图像一致),你的代码可能会变成这样:

/*...省略一堆相关Vulkan对象(着色器、管线布局、管线等)*/
bool useDeferredRendering = false;
void PreInitialization_UseDeferredRendering() { useDeferredRendering = true; }
bool CreateDeferredRenderingPipeline() { /*...*/ }
void DestroyDeferredRenderingPipeline() { /*...*/ }

void InitializeVulkanApplication(){
    InitializeWindow(defaultWindowSize);
    if (useDeferredRendering)
        CreateDeferredRenderingPipeline();
}
void CleanUpVulkanApplication(){
    if (useDeferredRendering)
        DestroyDeferredRenderingPipeline();
    TerminateWindow();
}

瞧见问题在哪了吗?按这种写法,如果你写了一堆这种管线的话,你需要在InitializeVulkanApplication()和CleanUpVulkanApplication()里做一堆判断,并且每增加一个新的可选管线都要修改这俩函数,并且为了在函数间传递信息,还不可避免地需要一个布尔值。

如果使用回调函数:

std::vector<void(*)()> callbacks_createSwapchain;
std::vector<void(*)()> callbacks_destroySwapchain;
VkResult RecreateSwapchain() {
    /*...前面略*/
    for (auto& i : callbacks_destroySwapchain)
        i();
    for (auto& i : callbacks_createSwapchain)
        i();
}
/*...省略管线对象的定义*/
void PreInitialization_UseDeferredRendering() {
    static bool executed = false;
    if (executed)
        return;
    /*...省略一堆声明为静态变量的相关Vulkan对象(着色器、管线布局等)*/
    auto CreateDeferredRenderingPipeline = [] { /*...*/ };
    auto DestroyDeferredRenderingPipeline = [] { /*...*/ };
    callbacks_createSwapchain.push_back(InitializeDeferredRendering);
    callbacks_destroySwapchain.push_back(UninitializeDeferredRendering);
}

创建、销毁相关对象的函数用lambda表达式定义在一个函数内,并将这俩lambda作为回调函数存放在两个vector中,至于那些不必暴露给外部的Vulkan对象则可以作为静态变量定义在同一个函数内。之后,RecreateSwapchain()每次执行时都会调用你设置的回调,也就是说每次你新增一套渲染管线,只需要在外部定义管线对象,并增加这样一个函数,而不需要修改程序的其他地方,也不必定义一些用于标明是否使用该管线的变量。

在vulkan::graphicsBase中加入以下内容:

private:
std::vector<void(*)()> callbacks_createSwapchain;
std::vector<void(*)()> callbacks_destroySwapchain;

public:
void AddCallback_CreateSwapchain(void(*function)()) {
    callbacks_createSwapchain.push_back(function);
}
void AddCallback_DestroySwapchain(void(*function)()) {
    callbacks_destroySwapchain.push_back(function);
}

然后在vulkan::graphicsBase::CreateSwapchain(...)和vulkan::graphicsBase::RereateSwapchain(...)中调用相应回调,最后两个函数如下:

VkResult CreateSwapchain(bool limitFrameRate = true, VkSwapchainCreateFlagsKHR flags = 0) {
    //VkSurfaceCapabilitiesKHR相关的参数
    VkSurfaceCapabilitiesKHR surfaceCapabilities = {};
    if (VkResult result = vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physicalDevice, surface, &surfaceCapabilities)) {
        std::cout << std::format("[ graphicsBase ] ERROR\nFailed to get physical device surface capabilities!\nError code: {}\n", int32_t(result));
        return result;
    }
    //指定图像数量
    swapchainCreateInfo.minImageCount = surfaceCapabilities.minImageCount + (surfaceCapabilities.maxImageCount > surfaceCapabilities.minImageCount);
    //指定图像大小
    swapchainCreateInfo.imageExtent =
        surfaceCapabilities.currentExtent.width == -1 ?
        VkExtent2D{
            glm::clamp(defaultWindowSize.width, surfaceCapabilities.minImageExtent.width, surfaceCapabilities.maxImageExtent.width),
            glm::clamp(defaultWindowSize.height, surfaceCapabilities.minImageExtent.height, surfaceCapabilities.maxImageExtent.height) } :
        surfaceCapabilities.currentExtent;
    //swapchainCreateInfo.imageArrayLayers = 1;//跟其他不需要判断的参数一起扔后面去
    //指定变换方式
    swapchainCreateInfo.preTransform = surfaceCapabilities.currentTransform;
    //指定处理透明通道的方式
    if (surfaceCapabilities.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR)
        swapchainCreateInfo.compositeAlpha = VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR;
    else
        for (size_t i = 0; i < 4; i++)
            if (surfaceCapabilities.supportedCompositeAlpha & 1 << i) {
                swapchainCreateInfo.compositeAlpha = VkCompositeAlphaFlagBitsKHR(surfaceCapabilities.supportedCompositeAlpha & 1 << i);
                break;
            }
    //指定图像用途
    swapchainCreateInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
    if (surfaceCapabilities.supportedUsageFlags & VK_IMAGE_USAGE_TRANSFER_SRC_BIT)
        swapchainCreateInfo.imageUsage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
    if (surfaceCapabilities.supportedUsageFlags & VK_IMAGE_USAGE_TRANSFER_DST_BIT)
        swapchainCreateInfo.imageUsage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT;
    else
        std::cout << std::format("[ graphicsBase ] WARNING\nVK_IMAGE_USAGE_TRANSFER_DST_BIT isn't supported!\n");

    //指定图像格式
    if (!availableSurfaceFormats.size())
        if (VkResult result = GetSurfaceFormats())
            return result;
    if (!swapchainCreateInfo.imageFormat)
        //用&&操作符来短路执行
        if (SetSurfaceFormat({ VK_FORMAT_R8G8B8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR }) &&
            SetSurfaceFormat({ VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR })) {
            //如果找不到上述图像格式和色彩空间的组合,那只能有什么用什么,采用availableSurfaceFormats中的第一组
            swapchainCreateInfo.imageFormat = availableSurfaceFormats[0].format;
            swapchainCreateInfo.imageColorSpace = availableSurfaceFormats[0].colorSpace;
            std::cout << std::format("[ graphicsBase ] WARNING\nFailed to select a four-component UNORM surface format!\n");

    //指定呈现模式
    uint32_t surfacePresentModeCount;
    if (VkResult result = vkGetPhysicalDeviceSurfacePresentModesKHR(physicalDevice, surface, &surfacePresentModeCount, nullptr)) {
        std::cout << std::format("[ graphicsBase ] ERROR\nFailed to get the count of surface present modes!\nError code: {}\n", int32_t(result));
        return result;
    }
    if (!surfacePresentModeCount)
        std::cout << std::format("[ graphicsBase ] ERROR\nFailed to find any surface present mode!\n"),
        abort();
    std::vector<VkPresentModeKHR> surfacePresentModes(surfacePresentModeCount);
    if (VkResult result = vkGetPhysicalDeviceSurfacePresentModesKHR(physicalDevice, surface, &surfacePresentModeCount, surfacePresentModes.data())) {
        std::cout << std::format("[ graphicsBase ] ERROR\nFailed to get surface present modes!\nError code: {}\n", int32_t(result));
        return result;
    }
    swapchainCreateInfo.presentMode = VK_PRESENT_MODE_FIFO_KHR;
    if (!limitFrameRate)
        for (size_t i = 0; i < surfacePresentModeCount; i++)
            if (surfacePresentModes[i] == VK_PRESENT_MODE_MAILBOX_KHR) {
                swapchainCreateInfo.presentMode = VK_PRESENT_MODE_MAILBOX_KHR;
                break;
            }

    //剩余参数
    swapchainCreateInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
    swapchainCreateInfo.flags = flags;
    swapchainCreateInfo.surface = surface;
    swapchainCreateInfo.imageArrayLayers = 1;
    swapchainCreateInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
    swapchainCreateInfo.clipped = VK_TRUE;

    //创建交换链
    if (VkResult result = CreateSwapchain_Internal())
        return result;
    //执行回调函数
    for (auto& i : callbacks_createSwapchain)
        i();
    return VK_SUCCESS;
}
VkResult RecreateSwapchain() {
    VkSurfaceCapabilitiesKHR surfaceCapabilities = {};
    if (VkResult result = vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physicalDevice, surface, &surfaceCapabilities)) {
        std::cout << std::format("[ graphicsBase ] ERROR\nFailed to get physical device surface capabilities!\nError code: {}\n", int32_t(result));
        return result;
    }
    if (surfaceCapabilities.currentExtent.width == 0 ||
        surfaceCapabilities.currentExtent.height == 0)
        return VK_SUBOPTIMAL_KHR;
    swapchainCreateInfo.imageExtent = surfaceCapabilities.currentExtent;
    swapchainCreateInfo.oldSwapchain = swapchain;
    VkResult result = vkQueueWaitIdle(queue_graphics);
    if (result == VK_SUCCESS &&
        queue_graphics != queue_presentation)
        result = vkQueueWaitIdle(queue_presentation);
    if (result) {
        std::cout << std::format("[ graphicsBase ] ERROR\nFailed to wait for the queue to be idle!\nError code: {}\n", int32_t(result));
        return result;
    }
    //销毁旧交换链相关对象
    for (auto& i : callbacks_destroySwapchain)
        i();
    for (auto& i : swapchainImageViews)
        if (i)
            vkDestroyImageView(device, i, nullptr);
    swapchainImageViews.resize(0);
    //创建新交换链及与之相关的对象
    if (VkResult result = CreateSwapchain_Internal())
        return result;
    for (auto& i : callbacks_createSwapchain)
        i();
    return VK_SUCCESS;
}

需要注意的是,因为RecreateSwapchain()会被其他看似与创建交换链无关的函数调用,因而没有参数,在调用RecreateSwapchain()重建交换链时,创建信息中的pNext仍旧为之前的值,因此但凡有重建交换链的必要,必须确保传入CreateSwapchain(...)的pNext具有够长的生存期。

Warning

注意,如果迭代器可能会失效,那么用for (auto& i : callbacks) i();,即range-based for循环执行回调函数并不安全。
你可以将其改成for (size_t size = callbacks.size(), i = 0; i < size; i++) callbacks[i]();,或者留着range-based for循环以发现并避免“在回调函数执行过程中添加新的回调函数”这种情形。

若需要重建逻辑设备

如果你需要重建逻辑设备(比如,运行过程中更换物理设备需要重建逻辑设备),也会需要相应的回调函数,在vulkan::graphicsBase中加入以下内容::

private:
std::vector<void(*)()> callbacks_createDevice;
std::vector<void(*)()> callbacks_destroyDevice;

public:
void AddCallback_CreateDevice(void(*function)()) {
    callbacks_createDevice.push_back(function);
}
void AddCallback_DestroyDevice(void(*function)()) {
    callbacks_destroyDevice.push_back(function);
}
//该函数用于等待逻辑设备空闲
VkResult WaitIdle() const {
    VkResult result = vkDeviceWaitIdle(device);
    if (result)
        std::cout << std::format("[ graphicsBase ] ERROR\nFailed to wait for the device to be idle!\nError code: {}\n", int32_t(result));
    return result;
}
//该函数用于重建逻辑设备
VkResult RecreateDevice(VkDeviceCreateFlags flags = 0) {
    if (VkResult result = WaitIdle())
        return result;
    /*待后续填充*/
}
  • 销毁逻辑设备前,当然要等它空闲,用vkDeviceWaitIdle(...)等待逻辑设备空闲,用法应该一目了然,在此不做赘述。

  • RecreateSwapchain()不同,RecreateDevice(...)会被手动调用,且其创建信息未被保存(考虑因更换物理设备而重建逻辑设备的情况,物理设备特性和队列创建信息都必须重新获取,所以没必要保存),所以带有参数。

如果你不需要重建逻辑设备,书写WaitIdle()后即可看下一节。

填充RecreateDevice(...),待逻辑设备空闲后首先销毁交换链相关的对象:

if (swapchain) {
    //调用销毁交换链时的回调函数
    for (auto& i : callbacks_destroySwapchain)
        i();
    //销毁交换链图像的image view
    for (auto& i : swapchainImageViews)
        if (i)
            vkDestroyImageView(device, i, nullptr);
    swapchainImageViews.resize(0);
    //销毁交换链
    vkDestroySwapchainKHR(device, swapchain, nullptr);
    //重置交换链handle
    swapchain = VK_NULL_HANDLE;
    //重置交换链创建信息
    swapchainCreateInfo = {};
}

接着调用销毁逻辑设备时的回调,并调用vkDestroyDevice(...)销毁逻辑设备相关的对象:

void VKAPI_CALL vkDestroyDevice(...) 的参数说明

VkDevice device

逻辑设备的handle

const VkAllocationCallbacks* pAllocator

如有必要,指向描述自定义内存分配方式的结构体

for (auto& i : callbacks_destroyDevice)
    i();
if (device)//防函数执行失败后再次调用时发生重复销毁
    vkDestroyDevice(device, nullptr),
    device = VK_NULL_HANDLE;

最后调用CreateDevice(...)重建逻辑设备,整个vulkan::graphicsBase::RecreateDevice()如下:

VkResult RecreateDevice(VkDeviceCreateFlags flags = 0) {
    if (VkResult result = WaitIdle())
        return result;
    if (swapchain) {
        for (auto& i : callbacks_destroySwapchain)
            i();
        for (auto& i : swapchainImageViews)
            if (i)
                vkDestroyImageView(device, i, nullptr);
        swapchainImageViews.resize(0);
        vkDestroySwapchainKHR(device, swapchain, nullptr);
        swapchain = VK_NULL_HANDLE;
        swapchainCreateInfo = {};
    }
    for (auto& i : callbacks_destroyDevice)
        i();
    if (device)
        vkDestroyDevice(device, nullptr),
        device = VK_NULL_HANDLE;
    return CreateDevice(flags);
}

注意应该在CreateDevice(...)中执行创建逻辑设备时的回调:

VkResult CreateDevice(VkDeviceCreateFlags flags = 0) {
    /*...*/
    std::cout << std::format("Renderer: {}\n", physicalDeviceProperties.deviceName);
    for (auto& i : callbacks_createDevice)
        i();
    return VK_SUCCESS;
}

于是如果你要重建逻辑设备,那么剧本应该是这样的:
1.调用RecreateDevice(...)重建逻辑设备。
2.调用CreateSwapchain(...)创建交换链。

销毁目前为止创建的对象

当应用程序结束时,通常来讲,操作系统会回收未释放的内存,而硬件驱动会回收未释放的显存,因此并非必须手动销毁目前为止创建的这些Vulkan对象。
不过,这里姑且还是填写一下graphicsBase的析构器,一来确保debug messenger能在期望的时机析构(可以在销毁Vulkan实例前的任意位置销毁它,将其保留到销毁Vulkan实例前以便捕捉在销毁过程中可能因Vulkan函数引发的异常),二来也方便你今后可能会有在运行过程中终止Vulkan的需求(比如,切换图形API)。
首先判断是否创建了Vulkan实例:

if (!instance)
    return;

然后在有逻辑设备的情况下等待逻辑设备空闲:

if (device) {
    WaitIdle();
    /*待后续填充*/;
}

销毁交换链和逻辑设备,基本如前一小节所写的那样,不过各类handle先不必置零:

if (device) {
    WaitIdle();
    if (swapchain) {
        for (auto& i : callbacks_destroySwapchain)
            i();
        for (auto& i : swapchainImageViews)
            if (i)
                vkDestroyImageView(device, i, nullptr);
        vkDestroySwapchainKHR(device, swapchain, nullptr);
    }
    for (auto& i : callbacks_destroyDevice)
        i();
    vkDestroyDevice(device, nullptr);
}

vkDestroySurfaceKHR(...)销毁surface:

void VKAPI_CALL vkDestroySurfaceKHR(...) 的参数说明

VkInstance instance

Vulkan实例的handle

VkSurfaceKHR surface

被销毁的window surface的handle

const VkAllocationCallbacks* pAllocator

如有必要,指向描述自定义内存分配方式的结构体

if (surface)
    vkDestroySurfaceKHR(instance, surface, nullptr);

销毁debug messenger的函数vkDestroyDebugUtilsMessengerEXT(...)同 vkCreateDebugUtilsMessengerEXT(...)一样,都需要用vkGetInstanceProcAddr(...)取得,vkDestroyDebugUtilsMessengerEXT(...)用法与vkDestroySurfaceKHR(...)类似,不做赘述:

if (debugMessenger) {
    PFN_vkDestroyDebugUtilsMessengerEXT DestroyDebugUtilsMessenger =
        reinterpret_cast<PFN_vkDestroyDebugUtilsMessengerEXT>(vkGetInstanceProcAddr(instance, "vkDestroyDebugUtilsMessengerEXT"));
    if (DestroyDebugUtilsMessenger)
        DestroyDebugUtilsMessenger(instance, debugMessenger, nullptr);
}

最后销毁Vulkan实例,整个vulkan::graphicsBase::~graphicsBase(...)函数如下:

~graphicsBase() {
    if (!instance)
        return;
    if (device) {
        WaitIdle();
        if (swapchain) {
            for (auto& i : callbacks_destroySwapchain)
                i();
            for (auto& i : swapchainImageViews)
                if (i)
                    vkDestroyImageView(device, i, nullptr);
            vkDestroySwapchainKHR(device, swapchain, nullptr);
        }
        for (auto& i : callbacks_destroyDevice)
            i();
        vkDestroyDevice(device, nullptr);
    }
    if (surface)
        vkDestroySurfaceKHR(instance, surface, nullptr);
    if (debugMessenger) {
        PFN_vkDestroyDebugUtilsMessengerEXT DestroyDebugUtilsMessenger =
            reinterpret_cast<PFN_vkDestroyDebugUtilsMessengerEXT>(vkGetInstanceProcAddr(instance, "vkDestroyDebugUtilsMessengerEXT"));
        if (DestroyDebugUtilsMessenger)
            DestroyDebugUtilsMessenger(instance, debugMessenger, nullptr);
    }
    vkDestroyInstance(instance, nullptr);
}

因为vulkan::graphicsBase是单例,其析构器是private的,上述函数也仅仅是用于在程序退出时确保期望的销毁顺序。
如果你要在运行过程中终止Vulkan,那么为了防止程序退出时重复销毁相关对象,还需要把相关对象重置为零值,你可以在graphicsBase中加入以下函数:

public:
void Terminate() {
    this->~graphicsBase();
    instance = VK_NULL_HANDLE;
    physicalDevice = VK_NULL_HANDLE;
    device = VK_NULL_HANDLE;
    surface = VK_NULL_HANDLE;
    swapchain = VK_NULL_HANDLE;
    swapchainImages.resize(0);
    swapchainImageViews.resize(0);
    swapchainCreateInfo = {};
    debugMessenger = VK_NULL_HANDLE;
}

对于需要在运行过程中切换图形API的情况,我更推荐你单独写一个专用的函数,销毁其他对象但保留Vulkan实例,原因在于创建Vulkan实例是初始化过程中最耗时的一步(Release Bulid可能消耗1.4秒上下),而保留Vulkan实例并不会耗费你太多内存(30到40MB),方便来回切换。

完成初始化

InitializeWindow(...)中调用vulkan::graphicsBase::Base().CreateSwapchain(...):

bool InitializeWindow(VkExtent2D size, bool fullScreen = false, bool isResizable = true, bool limitFrameRate = true) {
    using namespace vulkan;

    if (!glfwInit()) {
        std::cout << std::format("[ InitializeWindow ] ERROR\nFailed to initialize GLFW!\n");
        return false;
    }
    glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
    glfwWindowHint(GLFW_RESIZABLE, isResizable);
    pMonitor = glfwGetPrimaryMonitor();
    const GLFWvidmode* pMode = glfwGetVideoMode(pMonitor);
    pWindow = fullScreen ?
        glfwCreateWindow(size.width, size.height, windowTitle, pMonitor, nullptr) :
        glfwCreateWindow(size.width, size.height, windowTitle, nullptr, nullptr);
    if (!pWindow) {
        std::cout << std::format("[ InitializeWindow ]\nFailed to create a glfw window!\n");
        glfwTerminate();
        return false;
    }

#ifdef _WIN32
    graphicsBase::Base().AddInstanceExtension(VK_KHR_SURFACE_EXTENSION_NAME);
    graphicsBase::Base().AddInstanceExtension("VK_KHR_win32_surface");
#else
    uint32_t extensionCount = 0;
    const char** extensionNames;
    extensionNames = glfwGetRequiredInstanceExtensions(&extensionCount);
    if (!extensionNames) {
        std::cout << std::format("[ InitializeWindow ]\nVulkan is not available on this machine!\n");
        glfwTerminate();
        return false;
    }
    for (size_t i = 0; i < extensionCount; i++)
        graphicsBase::Base().AddInstanceExtension(extensionNames[i]);
#endif
    graphicsBase::Base().AddDeviceExtension(VK_KHR_SWAPCHAIN_EXTENSION_NAME);
    graphicsBase::Base.UseLatestApiVersion()
    if (graphicsBase::Base.CreateInstance())
        return false;

    VkSurfaceKHR surface = VK_NULL_HANDLE;
    if (VkResult result = glfwCreateWindowSurface(graphicsBase::Base().Instance(), pWindow, nullptr, &surface)) {
        std::cout << std::format("[ InitializeWindow ] ERROR\nFailed to create a window surface!\nError code: {}\n", int32_t(result));
        glfwTerminate();
        return false;
    }
    graphicsBase::Base.Surface(surface);

    if (graphicsBase::Base.GetPhysicalDevices() ||
        graphicsBase::Base.DeterminePhysicalDevice(0, true, false) ||
        graphicsBase::Base.CreateDevice())
        return false;

    //本节新增--------------------------------
    if (graphicsBase::Base().CreateSwapchain(limitFrameRate))
        return false;
    //----------------------------------------

    return true;
}

终止窗口前应该确保Vulkan没有与窗口系统的呈现引擎进行交互,在TerminateWindow(...)中调用vulkan::graphicsBase::Base().WaitIdle():

void TerminateWindow() {
    graphicsBase::Base().WaitIdle()
    glfwTerminate();
}

到此为止便完成了Vulkan程序的初始化代码,这意味着所有对于运行Vulkan程序而言必备的对象皆已创建,以及,销毁这些对象的函数也已经在书写在了正确的位置。