探索 Vulkan 音视频技术(4):交换链与呈现

这个系列文章我们来介绍一位海外工程师如何探索 Vulkan 音视频技术,对于想要开始学习音视频技术的朋友,这些文章是份不错的入门资料,本篇介绍 Vulkan 交换链与呈现。

——来自公众号“关键帧Keyframe”的分享

交换链是 Vulkan 渲染管线与窗口系统之间的基本桥梁,使应用程序能够向用户显示渲染内容。在本综合指南中,我们将探讨 SaschaWillems/Vulkan 仓库如何实现交换链管理和呈现,为你提供将这些概念集成到自己的 Vulkan 应用程序中的知识。

交换链本质上是一组图像(帧缓冲区)的集合,Vulkan 在将其呈现到屏幕之前使用这些图像来渲染内容。可以将其视为增强版的双重缓冲系统——当一个图像正在显示时,你可以渲染到另一个图像,然后无缝地交换它们。这可以防止撕裂等视觉伪影,并确保流畅的动画。

该仓库的实现将这一复杂功能封装在 VulkanSwapChain 类中,该类处理所有平台特定的细节,同时为应用程序提供干净、统一的接口。来源:base/VulkanSwapChain.h#L4

1、平台特定的表面创建

在创建交换链之前,Vulkan 需要一个表面——一个连接到窗口系统的平台特定抽象。该仓库展示了卓越的跨平台支持,实现了以下平台的实现:

  • Windows (Win32)
  • Linux (XCB, Wayland, DirectFB)
  • macOS/iOS (MoltenVK, Metal)
  • Android
  • QNX Screen
  • 无头渲染
// Windows 表面创建示例
VkWin32SurfaceCreateInfoKHR surfaceCreateInfo = {};
surfaceCreateInfo.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR;
surfaceCreateInfo.hinstance = (HINSTANCE)platformHandle;
surfaceCreateInfo.hwnd = (HWND)platformWindow;
err = vkCreateWin32SurfaceKHR(instance, &surfaceCreateInfo, nullptr, &surface);

来源:base/VulkanSwapChain.cpp#L38-L42

2、队列族选择

创建表面后,下一个关键步骤是找到支持图形操作和呈现的队列族。该实现智能地搜索可以处理两者的队列:

// 查找具有呈现支持的队列
for (uint32_t i = 0; i < queueCount; i++) {
    if ((queueProps[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) != 0) {
        if (supportsPresent[i] == VK_TRUE) {
            graphicsQueueNodeIndex = i;
            presentQueueNodeIndex = i;
            break;
        }
    }
}

来源:base/VulkanSwapChain.cpp#L134-L146

这通过尽可能使用单个队列进行图形和呈现来确保最佳性能,避免了多个队列同步的开销。

3、交换链创建过程

交换链创建过程涉及几个影响渲染质量和性能的重要决策:

3.1、表面格式选择

该实现优先考虑特定颜色格式以获得最佳兼容性:

std::vector<VkFormat> preferredImageFormats = {
    VK_FORMAT_B8G8R8A8_UNORM,
    VK_FORMAT_R8G8B8A8_UNORM,
    VK_FORMAT_A8B8G8R8_UNORM_PACK32
};

来源:base/VulkanSwapChain.cpp#L187-L191

3.2、呈现模式选择

该仓库根据垂直同步需求实现了智能呈现模式选择:

// 如果未请求垂直同步,尝试查找邮箱模式
if (!vsync) {
    for (size_t i = 0; i < presentModeCount; i++) {
        if (presentModes[i] == VK_PRESENT_MODE_MAILBOX_KHR) {
            swapchainPresentMode = VK_PRESENT_MODE_MAILBOX_KHR;
            break;
        }
    }
}

来源:base/VulkanSwapChain.cpp#L255-L269

可用的呈现模式包括:

  • VK_PRESENT_MODE_FIFO_KHR: 启用垂直同步(默认)
  • VK_PRESENT_MODE_MAILBOX_KHR: 三重缓冲,防止撕裂
  • VK_PRESENT_MODE_IMMEDIATE_KHR: 无垂直同步,可能显示撕裂

3.3、图像数量和大小

该实现计算最佳图像数量并处理表面大小调整:

// 确定图像数量
uint32_t desiredNumberOfSwapchainImages = surfCaps.minImageCount + 1;
if ((surfCaps.maxImageCount > 0) && (desiredNumberOfSwapchainImages > surfCaps.maxImageCount)) {
    desiredNumberOfSwapchainImages = surfCaps.maxImageCount;
}

来源:base/VulkanSwapChain.cpp#L272-L276

4、渲染循环

实际的渲染过程遵循在基础示例类中实现的明确定义的模式:

4.1、图像获取

每帧从交换链获取下一个可用图像开始:

VkResult VulkanSwapChain::acquireNextImage(VkSemaphore presentCompleteSemaphore, uint32_t& imageIndex) {
    return vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, presentCompleteSemaphore, (VkFence)nullptr, &imageIndex);
}

来源:base/VulkanSwapChain.cpp#L377-L380

此函数会阻塞,直到图像可用,确保正确的同步。

4.2、命令缓冲区提交

渲染到获取的图像后,命令缓冲区将随适当的信号量同步一起提交到队列:

// 设置提交信息结构
submitInfo = vks::initializers::submitInfo();
submitInfo.pWaitDstStageMask = &submitPipelineStages;
submitInfo.waitSemaphoreCount = 1;
submitInfo.pWaitSemaphores = &semaphores.presentComplete;
submitInfo.signalSemaphoreCount = 1;
submitInfo.pSignalSemaphores = &semaphores.renderComplete;

4.3、图像呈现

最后,渲染的图像将呈现到屏幕:

VkResult VulkanSwapChain::queuePresent(VkQueue queue, uint32_t imageIndex, VkSemaphore waitSemaphore) {
    VkPresentInfoKHR presentInfo = {};
    presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
    presentInfo.swapchainCount = 1;
    presentInfo.pSwapchains = &swapChain;
    presentInfo.pImageIndices = &imageIndex;
    if (waitSemaphore != VK_NULL_HANDLE) {
        presentInfo.pWaitSemaphores = &waitSemaphore;
        presentInfo.waitSemaphoreCount = 1;
    }
    return vkQueuePresentKHR(queue, &presentInfo);
}

来源:base/VulkanSwapChain.cpp#L383-L398

5、交换链重新创建

交换链管理的一个关键方面是处理表面变化,例如窗口大小调整。该实现包括健壮的重新创建逻辑:

// 存储当前交换链句柄,以便稍后使用它来简化重新创建
VkSwapchainKHR oldSwapchain = swapChain;

// 使用 oldSwapchain 参数创建新的交换链
swapchainCI.oldSwapchain = oldSwapchain;

来源:base/VulkanSwapChain.cpp#L218-L320

这种方法确保在创建新交换链时仍可呈现现有图像,从而防止视觉中断。

6、资源清理

在 Vulkan 中,正确的资源管理至关重要。该实现包括全面的清理:

void VulkanSwapChain::cleanup() {
    if (swapChain != VK_NULL_HANDLE) {
        for (auto i = 0; i < images.size(); i++) {
            vkDestroyImageView(device, imageViews[i], nullptr);
        }
        vkDestroySwapchainKHR(device, swapChain, nullptr);
    }
    if (surface != VK_NULL_HANDLE) {
        vkDestroySurfaceKHR(instance, surface, nullptr);
    }
}

来源:base/VulkanSwapChain.cpp#L401-L414

7、最佳实践和性能考虑

该仓库实现展示了交换链管理的几个最佳实践

  1. 最佳图像数量:使用 minImageCount + 1 以获得更好的流水线化
  2. 传输支持:在可用时启用传输功能以进行屏幕截图和后处理
  3. 合成 Alpha:处理透明度以实现正确的窗口合成
  4. 资源重用:在重新创建期间使用旧交换链以最小化中断

Syntax error in textmermaid version 11.6.0

8、与示例集成

基础类结构使各个示例能够专注于其特定的渲染逻辑,而交换链管理则自动处理:

class VulkanExampleBase {
private:
    // 包装交换链以向窗口系统呈现图像
    VulkanSwapChain swapChain;

    // 同步信号量
    struct {
        VkSemaphore presentComplete;  // 交换链图像呈现
        VkSemaphore renderComplete;   // 命令缓冲区提交
    } semaphores{};
};

来源:base/vulkanexamplebase.h#L151-L159

这种架构允许示例继承自 VulkanExampleBase 并立即访问功能齐全的交换链和呈现系统。

9、结论

SaschaWillems/Vulkan 仓库为 Vulkan 中的交换链管理和呈现提供了出色的参考实现。通过封装平台特定表面创建、队列族选择和交换链配置的复杂性,它使开发者能够专注于应用程序的独特渲染需求,同时确保健壮的跨平台兼容性。

无论您是构建简单的三角形渲染器还是复杂的 3D 应用程序,理解这些交换链概念对于创建流畅、高性能的 Vulkan 应用程序至关重要。该仓库的实现既可作为学习资源,也可作为您自己 Vulkan 项目的实用基础。

学习和提升音视频开发技术,推荐你加入我们的知识星球:【关键帧的音视频开发圈】

探索 Vulkan 音视频技术(4):交换链与呈现

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。

(0)

相关推荐

发表回复

登录后才能评论