Vulkan教程翻译之六 创建 Swapchain

Vulkan Swapchain 创建详解
本文详细介绍了在 Vulkan 图形 API 中创建 Swapchain 的过程,包括初始化、Surface 创建、Surface 能力查询、图像格式选择及 Swapchain 实例化等关键步骤。Swapchain 是连接 GPU 渲染和屏幕显示的重要桥梁。

原文链接:https://vulkan.lunarg.com/doc/sdk/1.2.131.2/windows/tutorial/html/05-init_swapchain.html

创建 Swapchain

这一章节的代码文件是 05-init_swapchain.cpp

本章节描述了如何创建 swapchain,它是最终显示给用户的图像缓冲区列表。这是设置渲染需要的所有缓冲区的第一步。

这是一张 Vulkan swapchain 的视图,它关联着系统的其他各个部分。其中一些部分我们很熟悉,剩下的你将会在这一节里进行学习。

Vulkan 和 窗口系统

像其他的图形API一样,Vulkan 保持窗口系统和核心图形API分离。在Vulkan里,窗口系统通过 WSI(Window System Integration) extension 部分暴露出来。你可以在包含WSI内容的 Vulkan 规范文档里找到这些 extension 的文档。在 Vulkan SDK download website 和 Khronos Vulkan Registry 中可以找到包含这些内容的 Vulkan 规范。

WSI extension 支持各种不同平台。可以通过以下定义激活特定平台的 extension :

  • VK_USE_PLATFORM_ANDROID_KHR - Android
  • VK_USE_PLATFORM_WAYLAND_KHR - Wayland
  • VK_USE_PLATFORM_WIN32_KHR - Microsoft Windows
  • VK_USE_PLATFORM_XCB_KHR - X Window System, using the XCB library
  • VK_USE_PLATFORM_XLIB_KHR - X Window System, using the Xlib library

名称里的 "KHR" 部分表明该符号是在 Khronos extension 里定义的。

Surface 抽象

Vulkan 使用 VkSurfaceKHR 对象来抽象出原生平台的 surface 或 window。该符号定义为 VK_KHR_surface 扩展的一部分。WSI extension 的各种函数用来创建,操作和销毁这些 surface 对象。

回顾 Instance 和 Device Extensions

因为你在本教程前面的章节中跳过了使用 extension,现在是回顾它们的时候了,好让你可以激活接入窗口系统所要用的WSI extension。

Instance Extensions

为了使用 WSI extension,你需要激活通用的 surface 扩展。在该示例中 init_instance_extension_name() 函数里,找到把 VK_KHR_SURFACE_EXTENSION_NAME 插入要加载的 instance extension 列表的代码。

info.instance_extension_names.push_back(VK_KHR_SURFACE_EXTENSION_NAME);

也要注意到,相同的函数在特定平台的 extension 里,依赖于代码要编译的平台。例如,如果为 Windows 编译,该函数就要把 VK_KHR_WIN32_SURFACE_EXTENSION_NAME 插入到要加载的 instance extension 列表里。

info.instance_extension_names.push_back(VK_KHR_WIN32_SURFACE_EXTENSION_NAME);

当创建 instance 的时候,在 init_instance() 函数里加载这些 extension。

Device Extensionss

swapchain 是一个 GPU 用来绘制的图像缓存区的列表,并且提供给显示硬件来扫描到显示屏上。因为是GPU硬件在写这些 image,所以设备层面的 extension 需要和 swapchain 协同工作。因此,示例代码把 VK_KHR_SWAPCHAIN_EXTENSION_NAME 添加到 init_device_extension_names() 加载的 device extension 列表里。

info.device_extension_names.push_back(VK_KHR_SWAPCHAIN_EXTENSION_NAME);

这个 extension 稍后在本节中用来创建 swapchain。

回顾一下:

  • 本示例调用 init_instance_extension_names() 函数来加载常用的和特定平台的 surface extension 作为 instance extension。
  • 本示例调用 init_device_extension_names() 函数来加载 swapchain extension 作为 device extension。

通过查看在 Vulkan SDK download site 里的文档,你可以学习更多关于 instance extension 和 device extension。

Queue Family 和 Present

"present" 操作包括把其中一个 swapchain image 传给物理显示器以使它可以被看见。当应用想要显示一张 image 到显示器上,调用 vkQueuePresentKHR() 函数把一个 present 请求放到其中一个GPU queue里。因此,该函数引用的 queue 必须能支持 present 请求,或者是 graphics 和 present 请求。示例为此做了如下的检查:

// Iterate over each queue to learn whether it supports presenting:
VkBool32 *pSupportsPresent =
    (VkBool32 *)malloc(info.queue_family_count * sizeof(VkBool32));
for (uint32_t i = 0; i < info.queue_family_count; i++) {
    vkGetPhysicalDeviceSurfaceSupportKHR(info.gpus[0], i, info.surface,
                                         &pSupportsPresent[i]);
}

// Search for a graphics and a present queue in the array of queue
// families, try to find one that supports both
info.graphics_queue_family_index = UINT32_MAX;
info.present_queue_family_index = UINT32_MAX;
for (uint32_t i = 0; i < info.queue_family_count; ++i) {
    if ((info.queue_props[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) != 0) {
        if (info.graphics_queue_family_index == UINT32_MAX)
            info.graphics_queue_family_index = i;

        if (pSupportsPresent[i] == VK_TRUE) {
            info.graphics_queue_family_index = i;
            info.present_queue_family_index = i;
            break;
        }
    }
}

if (info.present_queue_family_index == UINT32_MAX) {
    // If didn't find a queue that supports both graphics and present, then
    // find a separate present queue.
    for (size_t i = 0; i < info.queue_family_count; ++i)
        if (pSupportsPresent[i] == VK_TRUE) {
            info.present_queue_family_index = i;
            break;
        }
}
free(pSupportsPresent);

这段代码里复用了之前获取的 info.queue_family_count,因为 vkGetPhysicalDeviceSurfaceSupportKHR() 为每一个 queue family 返回一个标记。然后搜索同时支持 present 和 graphics 的 queue family。如果没有 queue family 能同时支持两个,程序就会先记录一个支持 graphics 的 queue family,然后再搜索一个支持 present 的。因为这段代码里同时设置了 graphics_queue_family_index 和 present_queue_family_index,后续示例必须使用 graphics_queue_family_index 里的队列放 graphics 命令,用 present_queue_family_index 里的队列放 present 命令。

是的,前面在 init_device 里已经执行了搜索仅支持 graphics 的 queue family,这里有点多余,这么做仅仅是出于说明的目的。真正的应用也许用不同的顺序做这些步骤来避免重复。

如果找不到这些 queue family,应用就会退出。

Swapchain 创建信息

本节中其余大部分工作都是为了填充这个创建信息的结构体,用来创建 swapchain:

typedef struct VkSwapchainCreateInfoKHR {
    VkStructureType                  sType;
    const void*                      pNext;
    VkSwapchainCreateFlagsKHR        flags;
    VkSurfaceKHR                     surface;
    uint32_t                         minImageCount;
    VkFormat                         imageFormat;
    VkColorSpaceKHR                  imageColorSpace;
    VkExtent2D                       imageExtent;
    uint32_t                         imageArrayLayers;
    VkImageUsageFlags                imageUsage;
    VkSharingMode                    imageSharingMode;
    uint32_t                         queueFamilyIndexCount;
    const uint32_t*                  pQueueFamilyIndices;
    VkSurfaceTransformFlagBitsKHR    preTransform;
    VkCompositeAlphaFlagBitsKHR      compositeAlpha;
    VkPresentModeKHR                 presentMode;
    VkBool32                         clipped;
    VkSwapchainKHR                   oldSwapchain;
} VkSwapchainCreateInfoKHR;

创建 Surface

有了在 instance 和 device 里加载的 WSI extension,你就可以创建一个 VkSurface 来使你可以继续创建 swapchain。你需要再次使用特定平台的代码,在 05-init_swapchain.cpp 文件顶部显示的:

#ifdef _WIN32
    VkWin32SurfaceCreateInfoKHR createInfo = {};
    createInfo.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR;
    createInfo.pNext = NULL;
    createInfo.hinstance = info.connection;
    createInfo.hwnd = info.window;
    res = vkCreateWin32SurfaceKHR(info.inst, &createInfo, NULL, &info.surface);
#endif

info.connection 和 info.window 值是被包含在 init_connection() 和 inti_window()里的更多的特定平台代码设置的,这些你也可以在示例代码里找到。

请注意,init_connection() 和 inti_window() 函数还负责特定平台操作,包括连接到显示层和创建实际窗口。你刚用 vkCreateWin32SurfaceKHR() 函数创建的 VkSurfaceKHR surface 用句柄表示,该句柄可通过 Vulkan 用于平台窗口对象。

然后你把返回的 surface 加到 swapchain 创建信息结构体里:

VkSwapchainCreateInfoKHR swapchain_ci = {};
swapchain_ci.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
swapchain_ci.pNext = NULL;
swapchain_ci.surface = info.surface;

设备 Surface Format

在创建 swapchain 的时候,你还需要指定 format。在当前上下文中,“format”指如 VkFormat 枚举所描述的像素格式,在这里 VK_FORMAT_B8G8R8A8_UNORM 是设备 surface format 的一个常用例子。

示例代码中的下一段获取一个 VkSurfaceFormatKHR 结构体列表,它包含了显示器支持的 VkFormat 格式和其他信息。因为示例不用关心显示屏 image 和 surface 使用什么 format,示例就选取了第一个可用的 format,如果未指定 format,会回退到一个通用任意的。

查看示例中的代码,可以看到它最后在创建信息结构体里设置了 format:

swapchain_ci.imageFormat = info.format;

Surface Capabilities

为了继续填充 swapchain 创建信息的结构体,示例里调用了 vkGetPhysicalDeviceSurfaceCapabilitiesKHR() 和vkGetPhysicalDeviceSurfacePresentModesKHR() 来获取所需的信息。然后填到以下字段里:

uint32_t desiredNumberOfSwapChainImages = surfCapabilities.minImageCount;

swapchain_ci.minImageCount = desiredNumberOfSwapChainImages;
swapchain_ci.imageExtent.width = swapChainExtent.width;
swapchain_ci.imageExtent.height = swapChainExtent.height;
swapchain_ci.preTransform = preTransform;
swapchain_ci.presentMode = swapchainPresentMode;

你应该给 minImageCount 成员设置一个表明你的应用使用的缓冲区策略的值,比如双缓冲或者三重缓冲。该示例调用 vkGetPhysicalDeviceSurfaceCapabilitiesKHR() 函数请求可以用在 swapchain 里的最小 image 数量,结果存在 surfCapabilities 里。请求 image 的最小数量值,保证了我们在尝试申请另一个 image 之前,可以只获取一个可显示的 image。这表示的就是双缓冲配置,因为你将能够申请一个image来渲染,同时另一个image正在显示。如果你想要三重缓冲,那么你就得再申请一个image,然后在你显示之前,能够申请两个缓冲区来渲染。

Graphics 和 Present 的不同 Queue Family

你之前确定了graphics 和 present 队列的 queue family。如果它们是不同的,你需要做些额外的工作来允许 image 在 queue family 之间能共享。

swapchain_ci.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
swapchain_ci.queueFamilyIndexCount = 0;
swapchain_ci.pQueueFamilyIndices = NULL;
uint32_t queueFamilyIndices[2] = {
    (uint32_t)info.graphics_queue_family_index,
    (uint32_t)info.present_queue_family_index};
if (info.graphics_queue_family_index != info.present_queue_family_index) {
    // If the graphics and present queues are from different queue families,
    // we either have to explicitly transfer ownership of images between the
    // queues, or we have to create the swapchain with imageSharingMode
    // as VK_SHARING_MODE_CONCURRENT
    swapchain_ci.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
    swapchain_ci.queueFamilyIndexCount = 2;
    swapchain_ci.pQueueFamilyIndices = queueFamilyIndices;
}

以上字段提供了更多的创建 swapchain 所需基本信息。你可以检查示例中其余代码,来看看这些信息是如何获取的,以及创建信息结构体里其他部分是如何填充的。

创建 Swapchain

填完了 swapchain 创建信息结构体,现在你可以创建 swapchain 了:

res = vkCreateSwapchainKHR(info.device, &swapchain_ci, NULL, &info.swap_chain);

该调用创建了一个 image 的集合并构建成了 swapchain。在某些时刻,你需要各个 image 的句柄,来告诉GPU用哪一个 image 来渲染。vkCreateSwapchainKHR() 函数本身创建了 image,所以就一直追踪着句柄。示例使用了熟悉模式来获取 image 句柄,先查询存在多少个句柄,通过调用

vkGetSwapchainImagesKHR(info.device, info.swap_chain,
                        &info.swapchainImageCount, NULL);

获取了计数,然后再次调用函数获取 image 句柄的列表,并且把它们保存在 info.buffers[]。现在你拥有了一个目标 image 的句柄列表。

创建 Image View

你需要通过创建 image view 告诉 Vulkan 你想如何使用这些 swapchain。"view" 是附加给资源的必须的额外信息,描述这些资源如何使用。

属于一个 image 的内存区域可以被安排成多种方式,这取决于该 image 的用途。例如,一个 image 可以是1D,2D或3D。或者也可以是一个 image 数组等等。描述 image 格式(例如 VK_FORMAT_R8G8B8A8_UNORM),组件的顺序 和 layer 信息也很有用。所有这些信息是包含在 VkImageView 里的 image 元数据。

创建一个 image view 很简单。找到 05-init_swapchain.cpp 文件里的 VkImageViewCreateInfo 结构体,看一下它是用 你所期望的2D framebuffer 值来填充的。注意,image 句柄本身是存在 image view 里的。

然后,你可以存储 image view 句柄到 info 数据结构体以备之后使用,以此结束 swapchain 的创建。

<think> 我们正在讨论Vulkan中通过Swapchain截取屏幕原始数据的方法。 根据用户的问题,我们需要解释如何使用Vulkan的交换链(Swapchain)来获取屏幕的原始数据(即图像数据)。 步骤: 1. 创建交换链(Swapchain)时,我们需要设置图像的使用方式为`VK_IMAGE_USAGE_TRANSFER_SRC_BIT`,这样我们才能将交换链图像作为传输操作的源,用于复制图像数据。 2. 在渲染循环中,当我们渲染完一帧,将图像呈现到屏幕之前,我们可以将交换链中的图像复制到一个缓冲(Buffer)中。 3. 然后,我们可以将这个缓冲映射到CPU可访问的内存中,从而读取原始像素数据。 详细步骤: a. 创建交换链时,确保交换链图像的创建标志包含`VK_IMAGE_USAGE_TRANSFER_SRC_BIT`。 b. 创建一个足够大的缓冲(Buffer)来存储图像数据(宽度×高度×每个像素的字节数),并设置其使用标志为`VK_BUFFER_USAGE_TRANSFER_DST_BIT`,内存属性为`VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT`(这样我们可以映射它并保证一致性)。 c. 在渲染循环中,当渲染完成(在vkQueuePresentKHR之前),我们需要将图像从交换链图像布局转换为传输源布局(如果当前布局不是`VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL`,则需要转换布局)。 d. 使用`vkCmdCopyImageToBuffer`命令将图像复制到缓冲。 e. 提交命令并等待命令执行完成(使用栅栏或事件确保复制完成)。 f. 映射缓冲的内存,读取数据。 注意事项: - 由于交换链图像通常处于`VK_IMAGE_LAYOUT_PRESENT_SRC_KHR`布局,我们需要将其转换为`VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL`,并在复制后转换回`VK_IMAGE_LAYOUT_PRESENT_SRC_KHR`(如果还要呈现的话)。 - 复制操作需要在一个命令缓冲区中完成,并提交到队列执行。 - 为了性能,通常我们不会每帧都这样做,因为从GPU读取数据到CPU会有性能损耗。 下面是一个简化的示例代码,展示如何将交换链图像复制到缓冲: ```cpp // 假设我们有一个交换链图像,我们将其复制到一个缓冲中 // 1. 转换图像布局 VkImageMemoryBarrier barrier = {}; barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; barrier.oldLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.image = swapchainImage; barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; barrier.subresourceRange.baseMipLevel = 0; barrier.subresourceRange.levelCount = 1; barrier.subresourceRange.baseArrayLayer = 0; barrier.subresourceRange.layerCount = 1; barrier.srcAccessMask = VK_ACCESS_MEMORY_READ_BIT; barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; vkCmdPipelineBarrier( commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier ); // 2. 复制图像到缓冲 VkBufferImageCopy region = {}; region.bufferOffset = 0; region.bufferRowLength = 0; // 紧密排列 region.bufferImageHeight = 0; region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; region.imageSubresource.mipLevel = 0; region.imageSubresource.baseArrayLayer = 0; region.imageSubresource.layerCount = 1; region.imageOffset = {0, 0, 0}; region.imageExtent = {width, height, 1}; vkCmdCopyImageToBuffer( commandBuffer, swapchainImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, buffer, 1, &region ); // 3. 将图像布局转换回呈现布局 barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; barrier.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; barrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; barrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT; vkCmdPipelineBarrier( commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier ); // 提交命令缓冲区并等待完成(这里省略了同步细节) // 然后就可以映射缓冲读取数据了 ``` 注意:在实际应用中,我们需要管理多个交换链图像,并确保在读取时不会干扰正在呈现的图像。因此,可能需要使用多个缓冲,并妥善同步。 另外,还有一种方法是通过创建线性布局的图像(而不是交换链图像通常使用的最优布局)来直接映射图像内存,但是交换链图像通常是最优布局(tiled),不能直接映射。因此,复制到缓冲是标准做法。 引用说明:上述方法参考了Vulkan规范中关于图像传输的操作[^1]。 §§相关问题§§ 1. 在Vulkan中,如何创建用于存储图像数据的缓冲? 2. 除了交换链图像,还有其他方式获取屏幕图像数据吗? 3. 如何优化Vulkan中图像数据从GPU到CPU的传输性能? </think> ### Vulkan 使用 Swapchain 截取屏幕原始数据的方法 在 Vulkan 中通过 Swapchain 截取屏幕原始数据需要以下关键步骤: #### 1. **配置 Swapchain 支持传输** 创建 Swapchain 时启用 `VK_IMAGE_USAGE_TRANSFER_SRC_BIT` 标志: ```cpp VkSwapchainCreateInfoKHR swapchainInfo{}; swapchainInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; // 关键 ``` 此标志允许将 Swapchain 图像作为数据传输源。 #### 2. **创建目标缓冲区和内存** ```cpp // 计算图像数据大小 (RGBA 8位格式) VkDeviceSize imageSize = width * height * 4; // 创建缓冲区 VkBufferCreateInfo bufferInfo{}; bufferInfo.size = imageSize; bufferInfo.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT; vkCreateBuffer(device, &bufferInfo, nullptr, &stagingBuffer); // 分配内存 (CPU可见) VkMemoryRequirements memReq; vkGetBufferMemoryRequirements(device, stagingBuffer, &memReq); VkMemoryAllocateInfo allocInfo{}; allocInfo.allocationSize = memReq.size; allocInfo.memoryTypeIndex = // 选择HOST_VISIBLE内存类型 vkAllocateMemory(device, &allocInfo, nullptr, &stagingMemory); vkBindBufferMemory(device, stagingBuffer, stagingMemory, 0); ``` #### 3. **执行图像复制操作** ```cpp VkCommandBuffer cmdBuf = // 开始单次命令缓冲区 // 转换图像布局 VkImageMemoryBarrier barrier{}; barrier.oldLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; barrier.image = swapchainImage; vkCmdPipelineBarrier(cmdBuf, ... , 1, &barrier); // 复制图像到缓冲区 VkBufferImageCopy region{}; region.imageExtent = {width, height, 1}; vkCmdCopyImageToBuffer( cmdBuf, swapchainImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, stagingBuffer, 1, &region ); // 恢复图像布局 barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; barrier.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; vkCmdPipelineBarrier(cmdBuf, ... , 1, &barrier); // 提交命令缓冲区并等待完成 vkEndCommandBuffer(cmdBuf); VkSubmitInfo submitInfo{...}; vkQueueSubmit(queue, 1, &submitInfo, fence); vkWaitForFences(device, 1, &fence, VK_TRUE, UINT64_MAX); ``` #### 4. **访问原始像素数据** ```cpp void* mappedData; vkMapMemory(device, stagingMemory, 0, imageSize, 0, &mappedData); // 保存为文件或处理数据 std::ofstream file("screenshot.raw", std::ios::binary); file.write(static_cast<char*>(mappedData), imageSize); vkUnmapMemory(device, stagingMemory); ``` #### 关键注意事项: 1. **同步处理**:必须使用栅栏(fence)确保复制操作在呈现前完成 2. **布局转换**:Swapchain 图像需要从 `VK_IMAGE_LAYOUT_PRESENT_SRC_KHR` 转为传输源布局 3. **内存类型**:必须使用 `VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT` 类型的内存 4. **性能影响**:频繁截图会降低渲染性能,建议仅在需要时执行 > 此方法直接获取 Swapchain 的后缓冲数据,可捕获屏幕原始 RGBA 像素流。实际应用中需处理图像方向(如有些系统会旋转图像)和颜色空间转换[^1]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值