Vulkan教程 – 06 交换链

Swap chain(交换链)

现在到了Vulkan教程第十章了,学习交换链。Vulkan没有默认帧缓冲的概念,因此它需要一个基础设施,能够在我们通过屏幕看到内容之前,持有我们想要渲染的东西的缓冲。该基础设施就是交换链了,必须显式创建。交换链本质上是一个图像队列,等待显示到屏幕上。我们的应用将会获取这样的一个图片来向其进行绘制,然后将它返回给队列。队列究竟怎么工作的,以及从队列中呈现一个图像的条件是什么,取决于交换链是如何建立的,但是交换链的主要目的还是将呈现的图像和屏幕刷新率进行同步。

先要进行交换链支持检查,不是所有显卡都能直接向屏幕呈现图像。这有很多原因,比如有的是为服务器设计的,根本没有输出接口。另外,由于呈现图像是和窗口系统强绑定的,而表面又和窗口相关联,所以这实际上就不是Vulkan核心内容了。查看支持后要开启VK_KHR_swapchain扩展。

所以我们还是先扩展isDeviceSuitable方法,检查是否支持该扩展。之前我们看到了通过VkPhysicalDevice获取扩展列表,所以做起来就很直白了。Vulkan头文件提供了一个很好的宏VK_KHR_SWAPCHAIN_EXTENSION_NAME,定义为VK_KHR_swapchain。使用该宏的好处是编译器能抓取拼写错误。

首先声明所需设备扩展列表,和验证层列表类似:

const std::vector<const char*> deviceExtensions = {
    VK_KHR_SWAPCHAIN_EXTENSION_NAME
};

接着,创建一个新的方法checkDeviceExtensionSupport,以便从isDeviceSuitable调用:

bool isDeviceSuitable(VkPhysicalDevice device) {
    QueueFamilyIndices indices = findQueueFamilies(device);

    bool extensionsSupported = checkDeviceExtensionSupport(device);

    return indices.isComplete() && extensionsSupported;
}

bool checkDeviceExtensionSupport(VkPhysicalDevice device) {
    return true;
}

接着就修改其主体部分,检查所需扩展是否都支持:

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();
}

启用交换链要求先启用VK_KHR_swapchain扩展,只要对逻辑设备创建部分稍作修改:

createInfo.enabledExtensionCount = static_cast<uint32_t>(deviceExtensions.size());
createInfo.ppEnabledExtensionNames = deviceExtensions.data();

只检查交换链能用还不够,因为实际上它可能与我们的窗口表面不兼容。创建交换链也会涉及到比实例和设备创建更多的设置,因此我们需要查询更多细节信息。主要有三种属性要查询:

基础表面能力(最小/最大交换链图像个数,最小/最大图像宽高);

表面格式(像素格式,颜色空间);

可用呈现模式。

和findQueueFamilies类似,我们需要用一个结构体传这些信息,如下:

struct SwapChainSupportDetails {
    VkSurfaceCapabilitiesKHR capabilities;
    std::vector<VkSurfaceFormatKHR> formats;
    std::vector<VkPresentModeKHR> presentModes;
};

现在创建一个新的方法querySwapChainSupport:

SwapChainSupportDetails querySwapChainSupport(VkPhysicalDevice device) {
    SwapChainSupportDetails details;

    return details;
}

从基础的表面能力开始,这些属性容易查询,返回的是一个VkSurfaceCapabilitiesKHR结构体:

vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface, &details.capabilities);

当确定支持的能力的时候,该方法会将特定的VkPhysicalDevice和VkSurfaceKHR窗口表面考虑进去。所有支持的查询方法都用这两个作为头两个参数,因为它们是交换链的核心组件。接着查询支持的表面格式,他是一组结构体,如下:

uint32_t formatCount;
vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, nullptr);

if (formatCount != 0) {
    details.formats.resize(formatCount);
    vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, details.formats.data());
}

确保vector调用resize来保存所有可用的格式。最后查询支持的呈现模式,一样的操作,用vkGetPhysicalDeviceSurfacePresentModesKHR查询:

uint32_t presentModeCount;
vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, nullptr);

if (presentModeCount != 0) {
    details.presentModes.resize(presentModeCount);
    vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, details.presentModes.data());
}

现在所有的详细信息都在结构体中了,我们再次扩展isDeviceSuitable,检查交换链是否够用。对于我们给定的窗口表面,交换链如果至少有一个支持的图形格式,以及一个支持的呈现模式,那么对本教程来说就够用了:

bool isDeviceSuitable(VkPhysicalDevice device) {
    QueueFamilyIndices indices = findQueueFamilies(device);

    bool extensionsSupported = checkDeviceExtensionSupport(device);

    bool swapChainAdequate = false;
    if (extensionsSupported) {
        SwapChainSupportDetails swapChainSupport = querySwapChainSupport(device);
        swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.presentModes.empty();
    }

    return indices.isComplete() && extensionsSupported && swapChainAdequate;
}

这里要注意的是,确认扩展可用之后再去查询交换链支持。如果swapChainAdequate条件满足,那么支持是肯定满足的,但是还有许多不同的模式需要选优。现在写几个方法选择正确的设置,有三种设置需要考虑:

表面格式(颜色深度);

呈现模式(交换图片到屏幕的条件);

交换大小(交换链图像分辨率)

先从表面格式开始,用下面的的代码进行设置,之后会传格式成员作为参数:

VkSurfaceFormatKHR chooseSwapSurfaceFormat(
    const std::vector<VkSurfaceFormatKHR> & awailableFormats) {

}

每个VkSurfaceFormatKHR记录包含一个format和一个colorSpace成员,format成员表明了颜色通道和类型,比如VK_FORMAT_B8G8R8A8_UNORM表示我们以B、G、R和alpha通道顺序进行存储,都是8位,每个像素共占用32位空间。颜色空间colorSpace成员通过VK_COLOR_SPACE_SRGB_NONLINEAR_KHR标记来表示SRGB颜色空间是否支持,这个在以前会叫做VK_COLORSPACE_SRGB_NONLINEAR_KHR。

对于颜色空间来说,如果SRGB可用那么我们就会选它,因为它能让人接受更准确的颜色。直接和SRGB颜色打交道会比较难,因此我们这里就还是用RGB,一般常用的是VK_FORMAT_B8G8R8A8_UNORM。最好的情况就是表面没有偏好格式,这样Vulkan返回就是一个VkSurfaceFormatKHR记录,其格式成员设置为VK_FORMAT_UNDEFINED:

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中有四种模式:

VK_PRESENT_MODE_IMMEDIATE_KHR:你的程序提交的图像会立即传输到屏幕,这可能会导致撕裂;

VK_PRESENT_MODE_FIFO_KHR:交换链是个队列,显示的时候从队列头拿一个图像,程序插入渲染的图像到队列尾。如果队列满了程序就要等待,这差不多像是垂直同步,显示刷新的时刻就是垂直空白;

VK_PRESENT_MODE_FIFO_RELAXED_KHR:在最后一个垂直空白的时候,如果应用迟到,且队列为空,该模式才会和前面的那个有所不同。这样就不等到下一个垂直空白,图像会直接传输到屏幕,可能导致撕裂;

VK_PRESENT_MODE_MAILBOX_KHR:这是第二个模式的又一个变种,当队列满的时候,它不会阻塞应用,已经在队列中的图像会被新的替换。这个模式可以实现三重缓冲,避免撕裂,比使用双重缓冲的垂直同步减少很多延迟。

只有VK_PRESENT_MODE_FIFO_KHR模式是一定有的,所以我们需要写个方法获取最好的模式:

VkPresentModeKHR chooseSwapPresentMode(const std::vector<VkPresentModeKHR>& availablePresentModes) {
    return VK_PRESENT_MODE_FIFO_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;
}

不幸的是,有些驱动还不支持VK_PRESENT_MODE_FIFO_KHR,所以当VK_PRESENT_MODE_MAILBOX_KHR不能用的时候选择VK_PRESENT_MODE_IMMEDIATE_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;
}

交换大小是最后一个主要的属性,最后再添加一个方法:

VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities) {

}

交换大小是交换链图像的分辨率,它基本都等于窗口分辨率,可能的分辨率定义在VkSurfaceCapabilitiesKHR结构体中。通过设置currentExtent成员来匹配窗口分辨率,但是,有的窗口管理器确实允许我们设置不同的值,就是给currentExtent的width和height设置特殊的值,即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;
    }
}

max和min方法用于截断WIDTH和HEIGHT值使其位于实现所支持的最小和最大尺寸之间。确保包含了algorithm头文件。

创建一个函数负责创建交换链,在initVulkan中调用:

void initVulkan() {
    createInstance();
    setupDebugMessenger();
    createSurface();
    pickPhysicalDevice();
    createLogicalDevice();
    createSwapChain();
}

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;

但是,就用最小值意味着我们可能某些时候必须要在驱动上等待其完成内部操作才能获取我们需要渲染的图像。因此推荐:

uint32_t imageCount = swapChainSupport.capabilities.minImageCount + 1;

我们还要确定不能超过最大值,0是个特殊值,表示没有最大值:

if (swapChainSupport.capabilities.maxImageCount > 0 && imageCount > swapChainSupport.capabilities.maxImageCount) {
    imageCount = swapChainSupport.capabilities.maxImageCount;
}

习惯上,Vulkan对象创建交换链需要填写一个很大的结构体信息,和之前的类似:

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,除非你在开发三维立体程序。imageUsage表明了我们用交换链做什么类型的操作。接着我们要明确怎么处理将要用于多个队列族的交换链图像。这也就是我们程序中图形队列族和呈现队列不同的时候的情况。我们会在图形队列的交换链图像上绘制,然后提交到呈现队列。有两种方式处理来自多个队列的图像:

VK_SHARING_MODE_EXCLUSIVE:一个图像在某个时间点就只能被一个队列族占用,在被另一个队列族使用前,它的占用情况一定要显式地进行转移。该选择提供了最好的性能。

VK_SHARING_MODE_CONCURRENT:图像能被多个队列族占用,不用显式进行所属信息切换。如果队列族不一样,我们会用并发模式以避免所属切换。并发模式要求提前用queueFamilyIndexCount和pQueueFamilyIndices参数写明哪个队列族所属性会被分享。如果图形队列族和呈现队列族相同,这其实也基本是大多数硬件的情况,那么我们要用独占模式,因为并发模式要求你明确最少两个不同的队列族:

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;

如果clipped成员设置为真,那表明我们不关心模糊像素的颜色,比如有个窗口在它前面。除非你真的需要能读取这些像素并得到预测结果,不然的话,启用clipping会得到最好的性能:

createInfo.presentMode = presentMode;
createInfo.clipped = VK_TRUE;

这样还要最后一个设置就是oldSwapChain,在程序运行的时候可能交换链会失效或者变得需要优化,比如窗口大小改变了。这样交换链需要重新创建,对旧的的引用也必须在此成员中明确指出。这个很复杂,我们暂时就认为我们一直都用一个交换链:

createInfo.oldSwapchain = VK_NULL_HANDLE;

现在添加一个类成员存储VkSwapchainKHR对象:

VkSwapchainKHR swapChain;

那么创建就很简单了:

if (vkCreateSwapchainKHR(device, &createInfo, nullptr, &swapChain) != VK_SUCCESS) {
    throw std::runtime_error("failed to create swap chain!");
}

 然后需要在cleanup中销毁:

vkDestroySwapchainKHR(device, swapChain, nullptr);

交换链已经创建好了,接着就是获取其中的VkImages句柄了。添加一个类成员:

std::vector<VkImage> swapChainImages;

图像是实现交换链的时候创建的,交换链被销毁的时候它们也会自动清理,所以我们不用添加任何清理代码。我在createSwapChain中添加了一些代码获取句柄,也就是在vkCreateSwapChainKHR之后,获取它们和我们从Vulkan中获取一组对象相似。要记住我们只是在交换链中声明了最小数量的图像,所以实现时可以创建含有更多图像的交换链的。这也就是为什么,我们用vkGetSwapchainImagesKHR先查询最终图像数量,然后重新调整容器大小最终再次调用以获取句柄:

vkGetSwapchainImagesKHR(device, swapChain, &imageCount, nullptr);
swapChainImages.resize(imageCount);
vkGetSwapchainImagesKHR(device, swapChain, &imageCount, swapChainImages.data());

最后存储交换链图像格式和大小到成员变量:

VkSwapchainKHR swapChain;
std::vector<VkImage> swapChainImages;
VkFormat swapChainImageFormat;
VkExtent2D swapChainExtent;

然后创建交换链的最后:

swapChainImageFormat = surfaceFormat.format;
swapChainExtent = extent;

存储这些变量是为了将来用,我们现在已经有一堆图像可以在上面绘制内容然后呈现到屏幕上了,下一章就可以看如何创建图像作为渲染目标然后探究实际的渲染管线和绘制命令了!

© 版权声明
THE END
喜欢就支持一下吧
点赞1 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片