【VulkanTutorial·一】概述

注:原文地址:https://vulkan-tutorial.com/Overview

Vulkan起源

本部分略(以后有时间再翻译)。

如何绘制一个三角形

现在我们将概述在一个性能良好的Vulkan程序中渲染一个三角形所需的所有步骤。这里介绍的所有概念都将在下一章详细阐述。这只是给你们一个包含所有的独立组件的概览。

步骤1 实例和物理设备选择(Instance and physical device selection)

一个Vulkan程序通过创建一个VkInstance设置Vulkan API来启动。而实例的创建,是根据对您的程序的描述以及将要使用的任何API扩展。实例创建之后,您可以查询支持Vulkan的硬件,并选择一个或多个VkphysicalDevices来使用。您可以查询诸如VRAM大小和设备容量等属性来选择所需的设备,比如您可能更倾向于使用专用的显卡。

步骤2 逻辑设备和队列家族(Logical device and queue families)

选择好要使用的硬件设备之后,就需要创建一个VkDevice(Logical device),这里你可以更具体地描述你将使用哪些VkPhysicalDeviceFeatures,如多视图渲染(multi viewport rendering)和64位浮点数(64 bit floats)等。同时也需要指定你想用哪个队列家族。Vulkan的大多数操作,如绘制指令和内存操作等,都需要提交到VkQueue中异步执行。队列是从队列家族中分配的,队列家族并不止一个,其中每个队列家族在其队列中支持一种特定的操作集。例如,可能有不同的队列家族分别支持图形,计算以及内存传输等操作。队列家族的这种特点也可以作为物理设备选择的一个比较显著的因素。支持Vulkan的设备也可能不提供任何图形功能,不过目前所有支持Vulkan的显卡一般都会支持我们感兴趣的各种队列操作。

步骤3 窗口表面和交换链(Window surface and swap chain)

除非你只对离屏渲染(offscreen rendering)感兴趣,否则你肯定需要创建一个窗口来呈现渲染图像。可以使用本地平台的API或者类似GLFW和SDL这样的库来创建窗口。我们在这篇教程中将选择使用GLFW,这一点你将在后续的章节中见到。

我们还需要另外两个组件来渲染一个窗口:窗口表面(VkSurfaceKHR)和交换链(VkSwapchainKHR)。注意这里的KHR后缀表示这些对象属于Vulkan扩展的一部分。Vulkan API本身是跨平台的,这也是为什么我们需要使用标准化的WSI(Window System Interface)扩展和窗口管理器(window manager)进行交互。这个表面是一个对渲染窗口的跨平台抽象,通常需要提供一个本地窗口句柄的引用来实例化,例如Windows的HWND。幸运的是,GLFW库有个内置函数可以解决不同平台之间的差异。

交换链是渲染目标的一个集合。它最基本的目的就是确保正在渲染的image和当前显示在窗口上的image是两张不同的image。保证只有渲染完成后才会显示是很重要的。每次我们想要绘制一帧,都必须向交换链请求一个image,绘制完一帧后,再将它返回给交换链,以便在某个时间后显示到窗口。渲染目标的数量以及已渲染image的显示条件取决于显示模式。常见的显示模式有双缓存(vsync)和三缓存。我们将在交换链创建的章节详细介绍。

一些平台允许你直接渲染到显示器,而不需要通过VK_KHR_display和VK_KHR_display_swapchain与任何窗口管理器进行交互。这允许你创建一个表面来代替整个屏幕,可以用来,举个例子,实现你自己的窗口管理器。

步骤4 图像视图和帧缓存(Image views and framebuffers)

要绘制一个从交换链中获得的图像,我们需要先将它包装到一个VkImageView和VkFramebuffer中。图像视图引用要使用的图像的特定部分,而帧缓存引用要用于颜色(color)、深度(depth)和模板(stencil)的图像视图。由于交换链中可能有多个不同的图像,我们会提前为每个图像创建一个图像视图和帧缓存,并在绘制时选择正确的那个。

步骤5 渲染遍(Render passes)

Vulkan中的渲染遍描述在渲染操作期间所使用的图像类型,如何使用,以及如何处理这些图像的内容。在我们的示例三角形绘制应用中,我们将告诉Vulkan,我们会使用单个图像作为颜色目标,并希望它在绘制操作之前被清为纯色。渲染遍只描述图像类型,而VkFramebuffer才是实际上将特定图像绑定到这些插槽的。

步骤6 图形管线(Graphics pipeline)

Vulkan中的图形管线是通过创建一个VkPipeline对象建立的。它描述了显卡的可配置状态,如视口大小和深度缓存操作,以及使用VkShaderModule对象的可编程状态。VkShaderModule对象是用shader字节码创建的。驱动需要知道有哪些渲染目标将会在管线中使用,这通过引用渲染遍来指定。

Vulkan与现有的API相比最显著的一个特征就是,几乎所有图形管线的配置都需要提前设置。这意味着如果你想切换到一个不同的shader或者仅是改变你的顶点布局(vertex layout),你需要重新创建图形管线。这也意味着你需要提前创建很多VkPipeline,来应对在渲染操作中的所有你需要的不同组合。只有一些基础配置,如视口大小和清除颜色(clear color),可以动态地改变。所有状态都需要显式描述,比如就没有提供默认的颜色混合状态。

比较好的一点就是这些工作相当于提前编译(ahead-of-time compilation)和即时编译(just-in-time compilation),因此驱动就有了更多的优化机会和更易预测实时性能,因为大型状态改变比如切换到一个不同的图形管线变得非常明确。

步骤7 指令池和指令缓存(Command pools and command buffers)

之前提到过,Vulkan中我们想要执行的很多指令,比如绘制操作,需要提交到一个队列。在提交这些操作之前,首先需要将它们记录到一个VkCommandBuffer中。这些指令缓存是从VkCommandPool中分配并关联到一个指定的队列家族。为了绘制一个简单的三角形,我们需要以下操作来记录一个指令缓存:

  1. 开始渲染遍
  2. 绑定图形管线
  3. 绘制三个顶点
  4. 结束渲染遍

由于帧缓存中的图像取决于交换链提供给我们的是哪一个特定图像,我们需要为每一个可能的图像都记录一个指令缓存,并在绘制时选择正确的那个。另一种方法时为每帧都重新记录指令缓存,这并不是很有效。

步骤8 主循环(Main loop)

现在绘制指令已经被包装到指令缓存中了,主循环就变得非常简单。我们首先使用vkAcquireNextImageKHR从交换链中请求一张图像。然后为这个图像选择对应的指令缓存,并使用vkQueueSubmit来执行。最后,返回图像到交换链,使用vkQueuePresentKHR显示到窗口。

提交到队列中的操作是异步执行的。因此我们就需要使用同步对象如信号量(semaphores)来保证正确的执行顺序。绘制指令缓存的执行必须等到图像请求完成之后才可以进行,否则可能会出现我们开始渲染一帧图像了,而该图像却在被读取到屏幕展示。vkQueuePresentKHR调用需要等待渲染结束,为此,我们需要使用第二个信号量在渲染完成时发出信号。

总结

以上这些应该能为你提供一个对绘制第一个三角形的工作的基本的了解。而实际的程序包含更多的步骤,比如分配顶点缓存(vertex buffer),创建uniform buffer,以及上传纹理图像等,我们将在后续的章节中提及。Vulkan的学习曲线比较陡峭,我们将先从简单入手。注意,我们将使用一些小技巧,在顶点着色器中直接嵌入顶点坐标,而不是使用顶点缓存,这是因为管理顶点缓存需要首先对指令缓存有一些了解。

简而言之,绘制第一个三角形,我们需要:

  • 创建一个VkInstance
  • 选择一个显卡(VkPhysicalDevice)
  • 为绘制和显示创建一个VkDevice和VkQueue
  • 创建一个窗口(window),窗口表面(window surface)和交换链(swap chain)
  • 将交换链的图像包装到VkImageView
  • 创建一个渲染遍(render pass)指定渲染目标和用法
  • 为渲染遍创建帧缓存(framebuffers)
  • 建立图形管线(graphics pipeline)
  • 为每个可能的交换链图像分配和记录一个包含绘制指令(draw commands)的指令缓存(command buffer)
  • 根据请求图像,提交正确的绘制指令缓存和将图像返回到交换链来绘制帧

这里步骤很多,但每个独立步骤的目的都将在之后的章节中变得简单和清晰。如果之后你为单个步骤和整个程序之间的关系感到困惑,建议再看一下本章节。

API 概念

这部分将简要概述Vulkan API在较低层级上的结构。

编码规范(Coding conventions)

所有Vulkan函数,枚举和结构体都在vulkan.h头文件中定义,包含在LunarG开发的Vulkan 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,要么为错误码。该规范描述了每个函数可以返回的错误代码及其含义。

验证层(Validation layers)

如之前描述的,Vulkan是为高性能和低驱动开销而设计的,因此默认情况下它将包含非常有限的错误检查和调试功能。如果你哪里做的不对,驱动程序通常会崩溃而不是返回错误代码,或者更糟糕的是,它可能只在你的显卡上能运行,在其他显卡上就完全失败。

Vulkan允许通过一个称为验证层(Validation layers)的特性来启用检查扩展。验证层是可以插入到API和显卡之间的代码片段,用于执行额外的功能参数检查和跟踪内存问题。好处就是,你可以在开发期间启用它们,而在发布应用时完全禁用它们,不会有任何的开销。任何人都可以编写他们自己的验证层,我们在该教程中将使用LunarG开发的Vulkan SDK中提供的一个标准的验证层。你同样需要注册一个回调函数来为这些验证层接收调试信息。

因为Vulkan对每个操作都非常明确,而且验证层非常广泛,所以与OpenGL和Direct3D相比,它可以更容易地找出为什么你的屏幕是黑色(画不出图像)的原因。

你可能感兴趣的:(Vulkan教程)