[译]Vulkan教程(02)概况
这是我翻译(https://vulkan-tutorial.com)上的Vulkan教程的第2篇。
This chapter will start off with an introduction of Vulkan and the problems it addresses. After that we're going to look at the ingredients that are required for the first triangle. This will give you a big picture to place each of the subsequent chapters in. We will conclude by covering the structure of the Vulkan API and the general usage patterns.
本章首先将介绍Vulkan和它解决的问题。然后我们要看一下绘制第一个三角形需要的材料。这会给你一个全局观念,以便(在头脑中)安放后续章节。最后,我们将总结Vulkan API的结构和一般的使用模式。
Origin of Vulkan Vulkan起源
Just like the previous graphics APIs, Vulkan is designed as a cross-platform abstraction over GPUs. The problem with most of these APIs is that the era in which they were designed featured graphics hardware that was mostly limited to configurable fixed functionality. Programmers had to provide the vertex data in a standard format and were at the mercy of the GPU manufacturers with regards to lighting and shading options.
和之前的图形API一样,Vulkan被设计为一个跨平台的对GPU的抽象。在之前的API被设计的年代,它们都是针对当时的图形硬件而设计了可配置的固定功能,这是它们的问题。程序员不得不用标准的格式提供顶点数据,听命于GPU厂商提供的光照和着色选项。
As graphics card architectures matured, they started offering more and more programmable functionality. All this new functionality had to be integrated with the existing APIs somehow. This resulted in less than ideal abstractions and a lot of guesswork on the graphics driver side to map the programmer's intent to the modern graphics architectures. That's why there are so many driver updates for improving the performance in games, sometimes by significant margins. Because of the complexity of these drivers, application developers also need to deal with inconsistencies between vendors, like the syntax that is accepted for shaders. Aside from these new features, the past decade also saw an influx of mobile devices with powerful graphics hardware. These mobile GPUs have different architectures based on their energy and space requirements. One such example is tiled rendering, which would benefit from improved performance by offering the programmer more control over this functionality. Another limitation originating from the age of these APIs is limited multi-threading support, which can result in a bottleneck on the CPU side.
随着图形卡架构的成熟,它们开始提供越来越多的可编程功能。所有的这些新功能都必须以某种方式加入现有的API中。这导致了抽象不够理想,图形driver端(在将程序员的意图映射到现代图形架构时)要做很多判断。这就是驱动频繁更新以提升游戏性能(有时提升十分明显)的原因。由于driver的复杂性,app开发者也需要处理不同厂商之间的不一致问题,例如shader的语法。除了这些新特性,过去几十年也涌现了带有强大图形硬件的移动设备。由于能量和空间限制,这些移动设备的GPU有不同的架构。一个例子是排列式成像,它通过提供给程序员更多的控制权得到了更高的性能。这些API的年纪带来的另一个限制它们是对多线程的支持有限,这会导致CPU端的瓶颈。
Vulkan solves these problems by being designed from scratch for modern graphics architectures. It reduces driver overhead by allowing programmers to clearly specify their intent using a more verbose API, and allows multiple threads to create and submit commands in parallel. It reduces inconsistencies in shader compilation by switching to a standardized byte code format with a single compiler. Lastly, it acknowledges the general purpose processing capabilities of modern graphics cards by unifying the graphics and compute functionality into a single API.
Vulkan是从零开始为现代图形架构设计的,它解决了上述问题。它允许程序员用一个冗繁得多的API,清楚地表明他们的意图,还允许在多线程中并发地创建和提交命令,从而减少了driver开销。它用一个唯一的编译器得到标准的字节码,从而减少了在shader编译方面的不一致性。最后,它用统一的图形和计算功能API来调用现代图形卡的处理能力。(译者注:Vulkan既可以用于图形渲染,又可以用于非图形计算。)
What it takes to draw a triangle 画一个三角形的代价
We'll now look at an overview of all the steps it takes to render a triangle in a well-behaved Vulkan program. All of the concepts introduced here will be elaborated on in the next chapters. This is just to give you a big picture to relate all of the individual components to.
现在我们将概览在一个表现良好的Vulkan程序中渲染一个三角形所需的所有步骤。这里介绍的所有概念都将在接下来的章节中详述。这里只是给你个全局概念,让你将各个独立的组件关联起来。
Step 1 - Instance and physical device selection 步骤1 - 选择instance和物理设备
A Vulkan application starts by setting up the Vulkan API through a VkInstance
. An instance is created by describing your application and any API extensions you will be using. After creating the instance, you can query for Vulkan supported hardware and select one or more VkPhysicalDevice
s to use for operations. You can query for properties like VRAM size and device capabilities to select desired devices, for example to prefer using dedicated graphics cards.
Vulkan应用程序开始时要通过一个VkInstance来设置Vulkan API。一个instance通过(描述你的app和你将要使用的所有API扩展)来创建。创建完instance后,你可以查询Vulkan支持的硬件,选择一个或多个VkPhysicalDevice,用于后续操作。你可以查询VRAM大小、设备功能等,用以选择想要的设备,例如选择某种专用图形卡。
Step 2 - Logical device and queue families 步骤2 – 逻辑设备和queue家族
After selecting the right hardware device to use, you need to create a VkDevice
(logical device), where you describe more specifically which VkPhysicalDeviceFeatures
you will be using, like multi viewport rendering and 64 bit floats. You also need to specify which queue families you would like to use. Most operations performed with Vulkan, like draw commands and memory operations, are asynchronously executed by submitting them to a VkQueue
. Queues are allocated from queue families, where each queue family supports a specific set of operations in its queues. For example, there could be separate queue families for graphics, compute and memory transfer operations. The availability of queue families could also be used as a distinguishing factor in physical device selection. It is possible for a device with Vulkan support to not offer any graphics functionality, however all graphics cards with Vulkan support today will generally support all queue operations that we're interested in.
选择了正确的硬件设备后,你需要创建一个VkDevice (逻辑设备),它描述了你要使用哪些VkPhysicalDeviceFeatures ,例如多视口渲染和64位浮点数。你也需要标明你想使用哪个queue家族。Vulkan实施的大多数操作,例如绘制命令和内存操作,都是通过提交它们到一个VkQueue,来异步执行的。Queue是从queue家族分配的,每个queue家族里的queue都支持特定的一些操作(这些操作构成一个集合)。例如,有的queue家族支持图形操作,有的支持计算操作,有的支持内存转移操作。Queue家族的能力也可以用于选择物理设备的区分因素。可能存在完全不支持图形功能的Vulkan设备,但是当今所有的Vulkan图形卡一般都支持我们感兴趣的所有queue操作。
Step 3 - Window surface and swap chain 步骤3 – 窗口surface和交换链
Unless you're only interested in offscreen rendering, you will need to create a window to present rendered images to. Windows can be created with the native platform APIs or libraries like GLFW and SDL. We will be using GLFW in this tutorial, but more about that in the next chapter.
你将需要创建一个窗口来呈现渲染的图像,除非你只对离屏渲染感兴趣。窗口可以用本地平台API或GLFW和SDL这样的库创建。本教程中我们将使用GLFW,但是下一章再细说。
We need two more components to actually render to a window: a window surface (VkSurfaceKHR
) and a swap chain (VkSwapchainKHR
). Note the KHR
postfix, which means that these objects are part of a Vulkan extension. The Vulkan API itself is completely platform agnostic, which is why we need to use the standardized WSI (Window System Interface) extension to interact with the window manager. The surface is a cross-platform abstraction over windows to render to and is generally instantiated by providing a reference to the native window handle, for example HWND
on Windows. Luckily, the GLFW library has a built-in function to deal with the platform specific details of this.
我们还需要2个组件来渲染到窗口:一个窗口surface(VkSurfaceKHR)和一个交换链(VkSwapchainKHR)。注意,后缀KHR 意思是这些对象是Vulkan扩展的一部分。Vulkan API是完全的平台不可知论者,这就是我们需要用标准化WSI(窗口系统接口)扩展与窗口管理器交互的原因。Surface是对可渲染窗口的跨平台抽象,一般通过提供一个本地句柄的方式来实例化,例如在Windows上提供的句柄是HWND 。幸运的是,GLFW库有个内建函数处理平台相关的细节。
The swap chain is a collection of render targets. Its basic purpose is to ensure that the image that we're currently rendering to is different from the one that is currently on the screen. This is important to make sure that only complete images are shown. Every time we want to draw a frame we have to ask the swap chain to provide us with an image to render to. When we've finished drawing a frame, the image is returned to the swap chain for it to be presented to the screen at some point. The number of render targets and conditions for presenting finished images to the screen depends on the present mode. Common present modes are double buffering (vsync) and triple buffering. We'll look into these in the swap chain creation chapter.
交换链是渲染目标的集合。它的基本目的是确保当前正在渲染的image(图像)与当前正在呈现到屏幕的,不是同一个。为确保只有完整的image被呈现,这很重要。每次我们想绘制一帧时,我们不得不请求交换链提供给我们一个用于渲染的image。当我们完成了绘制这一帧,这个image就返回到交换链,准备在某时呈现到屏幕。渲染目标的数量,呈现image到屏幕的条件,都依赖于呈现模式。常见的呈现模式是双缓存(垂直同步)和三缓存。我们将在交换链创建章节详述这些。
Some platforms allow you to render directly to a display without interacting with any window manager through the VK_KHR_display
and VK_KHR_display_swapchain
extensions. These allow you to create a surface that represents the entire screen and could be used to implement your own window manager, for example.
有的平台允许你直接渲染到显示器,无需与窗口管理器交互,只要使用VK_KHR_display 和VK_KHR_display_swapchain 扩展即可。这样你就可以创建一个代表整个显示器区域的surface,用其实现自己的窗口管理器。
Step 4 - Image views and framebuffers 步骤4 – image视图和帧缓存
To draw to an image acquired from the swap chain, we have to wrap it into a VkImageView
and VkFramebuffer
. An image view references a specific part of an image to be used, and a framebuffer references image views that are to be used for color, depth and stencil targets. Because there could be many different images in the swap chain, we'll preemptively create an image view and framebuffer for each of them and select the right one at draw time.
为了在一个从交换链上得到的image上绘制,我们不得不将其封装到VkImageView 和VkFramebuffer。一个image视图指定image的哪一部分被使用,一个帧缓存指定image视图是被用作颜色、深度还是模板目标。因为交换链上可能有很多不同的image,我们将先发制人地为每个image创建一个image视图和帧缓存,然后在绘制时选择正确的那个。
Step 5 - Render passes 步骤5 – 渲染pass
Render passes in Vulkan describe the type of images that are used during rendering operations, how they will be used, and how their contents should be treated. In our initial triangle rendering application, we'll tell Vulkan that we will use a single image as color target and that we want it to be cleared to a solid color right before the drawing operation. Whereas a render pass only describes the type of images, a VkFramebuffer
actually binds specific images to these slots.
Vulkan中的渲染pass描述用于渲染操作的image类型,它们将被如何使用,它们的内容将被用于何处。在我们最初的渲染三角形app中,我们会告诉Vulkan我们将用一个image作为颜色目标,将其清空为一个固定颜色,之后再执行绘制操作。一个渲染pass只描述image的类型,但VkFramebuffer 才会实际绑定到具体的image。
Step 6 - Graphics pipeline 步骤6 – 图形管道
The graphics pipeline in Vulkan is set up by creating a VkPipeline
object. It describes the configurable state of the graphics card, like the viewport size and depth buffer operation and the programmable state using VkShaderModule
objects. The VkShaderModule
objects are created from shader byte code. The driver also needs to know which render targets will be used in the pipeline, which we specify by referencing the render pass.
Vulkan中的图形管道通过创建VkPipeline 对象来构建。它描述了图形卡的可配置的状态,例如视口大小、深度缓存操作和VkShaderModule 对象的可编程状态。VkShaderModule 对象从shader字节码创建。Driver也需要知道哪个渲染目标会被用于管道中,这一点,我们通过引用渲染pass来标明。
One of the most distinctive features of Vulkan compared to existing APIs, is that almost all configuration of the graphics pipeline needs to be set in advance. That means that if you want to switch to a different shader or slightly change your vertex layout, then you need to entirely recreate the graphics pipeline. That means that you will have to create many VkPipeline
objects in advance for all the different combinations you need for your rendering operations. Only some basic configuration, like viewport size and clear color, can be changed dynamically. All of the state also needs to be described explicitly, there is no default color blend state, for example.
Vulkan与原有API区别最大的特性之一是,几乎所有的图形管道配置工作都需要提前做好。意思是,如果你想切换到不同的shader或稍微改变你的顶点布局,那么你需要整个重建图形管道。这意味着你不得不提前创建很多VkPipeline 对象,以用于不同的渲染操作的组合。只有一些基本的配置,例如视口大小和清空颜色,可以被动态地改变。所有状态都需要被显式地描述才会有,例如,不存在默认的颜色混合状态。
The good news is that because you're doing the equivalent of ahead-of-time compilation versus just-in-time compilation, there are more optimization opportunities for the driver and runtime performance is more predictable, because large state changes like switching to a different graphics pipeline are made very explicit.
好消息是,由于你做的提前编译(而不是即时编译),driver有更多的优化机会,运行时性能也更加可预测,因为重量级状态改变(例如切换到另一个图形管道)被显式地指出了。
Step 7 - Command pools and command buffers 步骤7 – 命令池和命令缓存
As mentioned earlier, many of the operations in Vulkan that we want to execute, like drawing operations, need to be submitted to a queue. These operations first need to be recorded into a VkCommandBuffer
before they can be submitted. These command buffers are allocated from a VkCommandPool
that is associated with a specific queue family. To draw a simple triangle, we need to record a command buffer with the following operations:
- Begin the render pass
- Bind the graphics pipeline
- Draw 3 vertices
- End the render pass
如前所述,在Vulkan中,我们想要执行的很多操作,例如绘制操作,需要被提交到一个queue里。在提交前,这些操作首先需要被记录到VkCommandBuffer 中。这些命令缓存是从一个命令池VkCommandPool 中申请的,命令池与一个特定的queue家族关联。为了绘制一个三角形,我们需要记录一个有下述操作的命令缓存:
- 开始渲染pass
- 绑定图形管道
- 绘制3个顶点
- 结束渲染pass
Because the image in the framebuffer depends on which specific image the swap chain will give us, we need to record a command buffer for each possible image and select the right one at draw time. The alternative would be to record the command buffer again every frame, which is not as efficient.
因为帧缓存中的image依赖于交换链给我们哪个image,我们需要对每个可能的image记录一个命令缓存,并在绘制时选择正确的那个。另一个方式是,每一帧都记录一次命令缓存,但这就不效率了。
Step 8 - Main loop 步骤8 – 主循环
Now that the drawing commands have been wrapped into a command buffer, the main loop is quite straightforward. We first acquire an image from the swap chain with vkAcquireNextImageKHR
. We can then select the appropriate command buffer for that image and execute it with vkQueueSubmit
. Finally, we return the image to the swap chain for presentation to the screen with vkQueuePresentKHR
.
既然绘制命令已经被封装到命令缓存里,主循环就相当直截了当了。我们首先用函数vkAcquireNextImageKHR从交换链请求一个image。然后我们就可以为此image选择恰当的命令缓存,并用函数vkQueueSubmit执行它。最后,我们用函数vkQueuePresentKHR将image返回到交换链,以使之被呈现到屏幕。
Operations that are submitted to queues are executed asynchronously. Therefore we have to use synchronization objects like semaphores to ensure a correct order of execution. Execution of the draw command buffer must be set up to wait on image acquisition to finish, otherwise it may occur that we start rendering to an image that is still being read for presentation on the screen. The vkQueuePresentKHR
call in turn needs to wait for rendering to be finished, for which we'll use a second semaphore that is signaled after rendering completes.
提交到queue的操作是被异步执行的。因此我们不得不使用同步对象(例如信号)来确保执行的正确顺序。必须等待image的请求结束后,才能执行绘制命令缓存的操作。否则,可能发生我们开始渲染到image了但是image还在被用于呈现到屏幕上的情况。依序,调用函数vkQueuePresentKHR 前需要等待渲染操作完成,为此我们将用另一个信号对象(在渲染完成后发信号)。
Summary 总结
This whirlwind tour should give you a basic understanding of the work ahead for drawing the first triangle. A real-world program contains more steps, like allocating vertex buffers, creating uniform buffers and uploading texture images that will be covered in subsequent chapters, but we'll start simple because Vulkan has enough of a steep learning curve as it is. Note that we'll cheat a bit by initially embedding the vertex coordinates in the vertex shader instead of using a vertex buffer. That's because managing vertex buffers requires some familiarity with command buffers first.
这次旋风之旅应该给你一个基础的理解,知道绘制第一个三角形之前的工作有哪些。实际的app包含更多的步骤,例如申请顶点缓存、创建uniform缓存和上传texture image,这些会在后续章节介绍。但我们从简单的开始,因为Vulkan的学习曲线已经很陡峭了。注意,我们将耍个滑头,在vertex shader中初始化一些内嵌的顶点坐标,而非使用顶点缓存。这是因为管理顶点缓存的前提条件之一是熟悉命令缓存。
So in short, to draw the first triangle we need to:
- Create a
VkInstance
- Select a supported graphics card (
VkPhysicalDevice
) - Create a
VkDevice
andVkQueue
for drawing and presentation - Create a window, window surface and swap chain
- Wrap the swap chain images into
VkImageView
- Create a render pass that specifies the render targets and usage
- Create framebuffers for the render pass
- Set up the graphics pipeline
- Allocate and record a command buffer with the draw commands for every possible swap chain image
- Draw frames by acquiring images, submitting the right draw command buffer and returning the images back to the swap chain
简单来说,为了绘制第一个三角形,我们需要:
- 创建一个VkInstance对象
- 选择一个图形卡(VkPhysicalDevice)
- 为绘制和呈现创建一个VkDevice 和VkQueue 。
- 创建一个窗口,窗口surface和交换链
- 将交换链的image封装进VkImageView
- 创建渲染pass,它标明渲染目标和用法
- 创建帧缓存,它引用渲染pass
- 构建图形管道
- 申请命令缓存,为交换链的每个image记录绘制命令
- 渲染一帧:请求image,提交正确的绘制命令缓存,将image返回到交换链
It's a lot of steps, but the purpose of each individual step will be made very simple and clear in the upcoming chapters. If you're confused about the relation of a single step compared to the whole program, you should refer back to this chapter.
步骤很多啊,但是每个独立步骤的目的将会十分简单清楚地展现在后续章节中。如果你不明白某一步骤在整个程序中的作用,你就应该重读本章。
API concepts API概念
This chapter will conclude with a short overview of how the Vulkan API is structured at a lower level.
本章最后将简要介绍Vulkan API在低层上的结构。
Coding conventions 编码约定
All of the Vulkan functions, enumerations and structs are defined in the vulkan.h
header, which is included in the Vulkan SDK developed by LunarG. We'll look into installing this SDK in the next chapter.
所有的Vulkan函数、枚举和结构体都在头文件vulkan.h 中定义,由LunarG开发的Vulkan SDK里有这个文件。
Functions have a lower case vk
prefix, types like enumerations and structs have a Vk
prefix and enumeration values have a VK_
prefix. The API heavily uses structs to provide parameters to functions. For example, object creation generally follows this pattern:
函数带有小写的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;
}
Many structures in Vulkan require you to explicitly specify the type of structure in the sType
member. The pNext
member can point to an extension structure and will always be nullptr
in this tutorial. Functions that create or destroy an object will have a VkAllocationCallbacks
parameter that allows you to use a custom allocator for driver memory, which will also be left nullptr
in this tutorial.
Vulkan中的许多结构体要求你显式地在成员sType标明结构体的类型。成员pNext 可能指向一个扩展结构体,在本教程中它将始终为nullptr 。
Almost all functions return a VkResult
that is either VK_SUCCESS
or an error code. The specification describes which error codes each function can return and what they mean.
几乎所有函数都返回一个VkResult ,它要么是VK_SUCCESS ,要么是一个错误码。说明书里描述了每个函数可能返回哪些错误码及其含义。
Validation layers 验证层
As mentioned earlier, Vulkan is designed for high performance and low driver overhead. Therefore it will include very limited error checking and debugging capabilities by default. The driver will often crash instead of returning an error code if you do something wrong, or worse, it will appear to work on your graphics card and completely fail on others.
如前所述,设计Vulkan是为了更高的性能和更低的driver开销。因此它默认只有很有限的错误检查和调试能力。如果你做错了什么,driver会经常崩溃,而不是返回错误码,甚至更糟,它在你的图形卡上能工作但是在其他的图形卡上就完全不行。
Vulkan allows you to enable extensive checks through a feature known as validation layers. Validation layers are pieces of code that can be inserted between the API and the graphics driver to do things like running extra checks on function parameters and tracking memory management problems. The nice thing is that you can enable them during development and then completely disable them when releasing your application for zero overhead. Anyone can write their own validation layers, but the Vulkan SDK by LunarG provides a standard set of validation layers that we'll be using in this tutorial. You also need to register a callback function to receive debug messages from the layers.
Vulkan允许你通过一个被称为验证层的特性来启用额外的检查。验证层是一段代码,可以插进API和图形驱动之间,做一些例如额外检查函数参数和追踪内存管理问题的事。好处是你可以在开发期间启用它,在发布app时彻底禁用它,以消除此开销。任何人都可以写自己的验证层,但是LunarG开发的Vulkan SDK提供了一个验证层的标准集,我们在本教程中就用这个。你还需要注册一个回调函数来接收这些层送来的调试信息。
Because Vulkan is so explicit about every operation and the validation layers are so extensive, it can actually be a lot easier to find out why your screen is black compared to OpenGL and Direct3D!
因为Vulkan的每个操作都是如此的显式,验证层又是如此的可扩展,想要找到为什么你的屏幕是一片漆黑,这要比OpenGL和Direct3D简单得多!
There's only one more step before we'll start writing code and that's setting up the development environment.
距离开始写代码还差一步,那就是配置开发环境。
- Previous上一章
- Next下一章