Vulkan学习笔记(2):Vulkan介绍

本章将首先介绍Vulkan及其解决的核心问题。随后我们将探讨绘制第一个三角形所需的各个要素,这将帮助您建立整体认知框架,以便理解后续每个章节的定位。最后,我们将解析Vulkan API的结构体系与通用应用模式。

Vulkan的起源

与以往的图形API类似,Vulkan被设计为跨平台的GPU抽象层。这些早期API的主要问题在于,它们诞生的时代中图形硬件大多局限于可配置的固定功能管线。程序员必须以标准化格式提供顶点数据,并在光照与着色效果方面完全受制于GPU制造商的预设方案。

随着显卡架构的演进成熟,它们开始提供日益丰富的可编程功能。这些新增功能必须以某种方式与现有API集成,从而导致抽象设计不尽理想,且图形驱动程序需要大量推测性工作来将开发者的意图映射到现代图形架构上。这也解释了为何游戏性能优化需要频繁的驱动程序更新,有时甚至能带来显著的性能提升。由于驱动程序的复杂性,应用程序开发者还需应对不同厂商之间的差异性问题,比如着色器语法的兼容性。除了这些新增功能外,过去十年间搭载强大图形硬件的移动设备也大量涌现。这些移动GPU基于其能耗与空间限制采用了不同的架构设计。瓦片式渲染就是典型例证——若能为开发者提供对此功能的更多控制权,将显著提升性能表现。这些传统API的另一项时代局限性是多线程支持不足,可能导致CPU端出现性能瓶颈。

Vulkan通过从头构建的方式解决了这些问题,专为现代图形架构而设计。它通过采用更详尽的API让开发者能够清晰表达意图,从而减少驱动程序开销,并支持多线程并行创建与提交指令。通过改用标准化字节码格式配合单一编译器,它消除了着色器编译的不一致性问题。最后,通过将图形与计算功能整合至统一API中,它确认了现代显卡的通用计算处理能力。

绘制三角形所需的步骤

现在我们将概述在规范的Vulkan程序中渲染三角形所需的所有步骤。这里介绍的所有概念将在后续章节详细展开,本节旨在帮助您建立整体认知框架,理解各个组件之间的关联。

步骤1——实例创建与物理设备选择

Vulkan应用程序首先通过VkInstance初始化Vulkan API。创建实例时需要描述应用程序信息及所需使用的API扩展。实例创建完成后,您可以查询支持Vulkan的硬件设备,并选择一个或多个VkPhysicalDevice用于后续操作。通过查询显存容量、设备功能等属性,可以选择符合需求的设备,例如优先选用独立显卡。

步骤2——逻辑设备与队列族

选定要使用的物理硬件设备后,需要创建VkDevice(逻辑设备)。在此过程中需要更具体地指定要使用的VkPhysicalDeviceFeatures功能,例如多视口渲染和64位浮点运算支持。同时还需声明要使用的队列族。Vulkan中的大多数操作(如绘制命令和内存操作)都是通过提交到VkQueue队列实现异步执行的。队列从队列族中分配,每个队列族支持其队列中执行特定的操作集合。例如,可能存在分别用于图形处理、计算任务和内存传输的独立队列族。队列族的可用性也可作为物理设备选择的区分依据。某些支持Vulkan的设备可能不提供图形功能,但目前所有支持Vulkan的显卡通常都支持我们所需的所有队列操作。

步骤3——窗口表面与交换链

除非您只关注离屏渲染,否则需要创建窗口来展示渲染图像。可以通过原生平台API或GLFWSDL等库创建窗口。本教程将使用GLFW,具体细节将在下一章详述。

要向窗口实际渲染图像,还需要两个组件:窗口表面(VkSurfaceKHR)和交换链(VkSwapchainKHR)。请注意KHR后缀,这表示这些对象属于Vulkan扩展功能。Vulkan API本身完全独立于平台,因此我们需要使用标准化的WSI(窗口系统接口)扩展来与窗口管理器交互。表面是对渲染窗口的跨平台抽象,通常通过引用原生窗口句柄(如Windows系统的HWND)来实例化。幸运的是,GLFW库内置了处理这些平台特定细节的函数。

交换链是一组渲染目标集合。其基本作用是确保当前正在渲染的图像与屏幕上显示的图像不同,这对于保证只显示完整图像至关重要。每次绘制帧时,都需要向交换链请求获取待渲染图像。完成帧绘制后,图像将返回交换链,以便在适当时机呈现到屏幕。渲染目标的数量以及将完成图像呈现到屏幕的条件取决于呈现模式。常见的呈现模式包括双重缓冲(垂直同步)和三级缓冲,我们将在交换链创建章节深入探讨。

某些平台允许通过VK_KHR_displayVK_KHR_display_swapchain扩展直接渲染到显示器,无需与窗口管理器交互。例如,这可用于创建代表整个屏幕的表面,从而实现自定义窗口管理器。

步骤4——图像视图与帧缓冲区

要绘制从交换链获取的图像,必须将其包装到VkImageView(图像视图)和VkFramebuffer(帧缓冲区)中。图像视图引用图像中待使用的特定部分,而帧缓冲区则引用将用作颜色、深度和模板目标的图像视图。由于交换链中可能存在多个不同图像,我们将预先为每个图像创建对应的图像视图和帧缓冲区,并在绘制时选择正确的组合。

步骤5——渲染通道

Vulkan中的渲染通道用于描述渲染操作期间使用的图像类型、图像使用方式以及图像内容的处理规则。在我们最初的三角形渲染应用程序中,将告知Vulkan使用单一图像作为颜色目标,并指定在绘制操作前将其清空为纯色。需要注意的是,渲染通道仅描述图像类型,而VkFramebuffer(帧缓冲区)则负责将具体的图像绑定到这些插槽。

步骤6——图形管线

Vulkan中的图形管线通过创建VkPipeline对象来配置。它描述了显卡的可配置状态(如视口尺寸和深度缓冲操作),以及通过VkShaderModule对象实现的可编程状态。VkShaderModule对象由着色器字节码创建。驱动程序还需要知道管线中将使用哪些渲染目标,我们通过引用渲染通道来指定这一点。

相较于现有API,Vulkan最显著的特征之一是:几乎所有图形管线的配置都需要预先设置。这意味着,如果要切换到不同的着色器或稍微修改顶点布局,就必须完全重新创建图形管线。因此,您需要为渲染操作所需的所有不同组合预先创建多个VkPipeline对象。只有视口尺寸和清除颜色等基本配置可以动态修改。所有状态都需要显式描述,例如没有默认的颜色混合状态。

好消息是,由于这种相当于"预编译"(而非即时编译)的方式,驱动程序获得了更多优化机会,运行时性能也更具可预测性——因为切换到不同图形管线这类重大状态变更会变得非常明确。

步骤7——命令池与命令缓冲区

如前所述,我们希望在Vulkan中执行的许多操作(例如绘制操作)都需要提交到队列。这些操作在提交前必须先记录到VkCommandBuffer(命令缓冲区)中。命令缓冲区从与特定队列族关联的VkCommandPool(命令池)分配。要绘制一个简单的三角形,我们需要记录包含以下操作的命令缓冲区:

  • 启动渲染通道
  • 绑定图形管线
  • 绘制3个顶点
  • 结束渲染通道

由于帧缓冲区中的图像取决于交换链提供的具体图像,我们需要为每个可能的图像记录一个命令缓冲区,并在绘制时选择正确的那个。另一种方法是每帧重新记录命令缓冲区,但效率较低。

步骤8——主循环

在将绘制命令封装到命令缓冲区后,主循环的逻辑变得相当清晰。首先使用vkAcquireNextImageKHR从交换链获取图像,接着为该图像选择合适的命令缓冲区,并通过vkQueueSubmit提交执行。最后,通过vkQueuePresentKHR将图像交还给交换链以呈现到屏幕。

提交到队列的操作是异步执行的,因此必须使用信号量等同步对象来确保正确的执行顺序。绘制命令缓冲区的执行需要设置为等待图像获取完成,否则可能会出现我们开始渲染的图像仍在被读取用于屏幕显示的情况。相应的,vkQueuePresentKHR调用也需要等待渲染完成,为此我们将使用第二个信号量,该信号量在渲染完成后触发。

总结

通过这次概览性介绍,您应该对绘制第一个三角形所需的基础工作有了基本认识。实际程序会包含更多步骤,例如分配顶点缓冲区、创建统一缓冲区以及上传纹理图像等(这些将在后续章节详述),但我们将从简入手——毕竟Vulkan本身的学习曲线已经足够陡峭。需要说明的是,初始阶段我们会将顶点坐标直接嵌入顶点着色器(而非使用顶点缓冲区),这是因为管理顶点缓冲区需要先熟悉命令缓冲区的使用。

简而言之,绘制第一个三角形需要完成以下步骤:

  • 创建VkInstance实例
  • 选择支持的显卡(VkPhysicalDevice)
  • 创建用于绘制和呈现的VkDevice与VkQueue
  • 创建窗口、窗口表面和交换链
  • 将交换链图像包装为VkImageView
  • 创建指定渲染目标及其用途的渲染通道
  • 为渲染通道创建帧缓冲区
  • 设置图形管线
  • 为每个可能的交换链图像分配并记录包含绘制命令的命令缓冲区
  • 通过获取图像、提交对应绘制命令缓冲区、将图像交还交换链来完成帧绘制

步骤虽多,但每个步骤的具体作用将在后续章节中变得简明清晰。若您对单个步骤在整个程序中的关联感到困惑,可随时回顾本章内容。

API 核心概念

本章最后将对 Vulkan API 的底层结构进行简要概述。

代码规范

所有 Vulkan 函数、枚举和结构体均在 vulkan.h 头文件中定义,该文件包含在 LunarG 开发的Vulkan SDK中。我们将在下一章探讨如何安装此 SDK。

函数名使用小写 vk 前缀,类型(如枚举和结构体)使用 Vk 前缀,枚举值使用 VK_ 前缀。该 API 大量使用结构体向函数传递参数。例如,对象创建通常遵循以下模式:

VkXXXCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_XXX_CREATE_INFO;
createInfo.pNext = nullptr;
createInfo.foo = ...;
createInfo.bar = ...;

VkXXX object;
if (vkCreateXXX(&createInfo, nullptr, &object) != VK_SUCCESS) {
    std::cerr << "failed to create object" << std::endl;
    return false;
}

Vulkan中许多结构体要求您在sType成员中显式指定结构体类型。pNext成员可指向扩展结构体,在本教程中将始终设为nullptr。创建或销毁对象的函数会包含VkAllocationCallbacks参数,该参数允许您为驱动程序内存使用自定义分配器,本教程中该参数也将保持为nullptr

几乎所有函数都会返回VkResult类型的结果,其值要么是VK_SUCCESS,要么是错误代码。规范文档详细说明了每个函数可能返回的错误代码及其含义。

验证层

如前所述,Vulkan专为高性能与低驱动开销而设计。因此默认情况下仅包含非常有限的错误检查与调试功能。如果您操作不当,驱动程序通常会直接崩溃而非返回错误代码,更糟的情况是:它可能在您的显卡上正常运行,却在其他硬件上完全失败。

Vulkan允许您通过名为"验证层"的功能启用全面检查。验证层是插入在API与图形驱动程序之间的代码模块,可用于执行函数参数的额外检查、追踪内存管理问题等任务。其优点是您可以在开发阶段启用它们,而在发布应用程序时完全禁用以实现零开销。任何人都可以编写自定义验证层,但LunarG提供的Vulkan SDK包含一套标准验证层,本教程将使用这些标准层。您还需要注册回调函数来接收验证层的调试信息。

由于Vulkan对每个操作都有明确规范,且验证层功能全面,相比OpenGL和Direct3D,实际上更容易排查屏幕显示全黑等问题的根源!

在我们开始编写代码之前,只剩下最后一个步骤——配置开发环境。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值