[译]Vulkan教程(04)基础代码

General structure 通用结构

In the previous chapter you've created a Vulkan project with all of the proper configuration and tested it with the sample code. In this chapter we're starting from scratch with the following code:

在之前的章节你已经创建了一个Vulkan项目,配置好了项目属性,用示例代码进行了测试。本章我们将从零开始写代码,先从下面的代码开始:



1 #include <vulkan/vulkan.h>
 2  
 3 #include <iostream>
 4 #include <stdexcept>
 5 #include <functional>
 6 #include <cstdlib>
 7  
 8 class HelloTriangleApplication {
 9 public:
10     void run() {
11         initVulkan();
12         mainLoop();
13         cleanup();
14     }
15  
16 private:
17     void initVulkan() {
18  
19     }
20  
21     void mainLoop() {
22  
23     }
24  
25     void cleanup() {
26  
27     }
28 };
29  
30 int main() {
31     HelloTriangleApplication app;
32  
33     try {
34         app.run();
35     } catch (const std::exception& e) {
36         std::cerr << e.what() << std::endl;
37         return EXIT_FAILURE;
38     }
39  
40     return EXIT_SUCCESS;
41 }



We first include the Vulkan header from the LunarG SDK, which provides the functions, structures and enumerations. The stdexcept and iostream headers are included for reporting and propagating errors. The functional headers will be used for a lambda functions in the resource management section. The cstdlibheader provides the EXIT_SUCCESS and EXIT_FAILURE macros.

首先,我们include了Vulkan头文件,它来自LunarG SDK,他提供函数、结构体和枚举。stdexcept 和iostream 头文件用于报告和传播错误。functional 头文件用于资源管理小节中要介绍到的lambda函数。cstdlib头文件提供了EXIT_SUCCESS 和EXIT_FAILURE 宏。

The program itself is wrapped into a class where we'll store the Vulkan objects as private class members and add functions to initiate each of them, which will be called from the initVulkan function. Once everything has been prepared, we enter the main loop to start rendering frames. We'll fill in the mainLoop function to include a loop that iterates until the window is closed in a moment. Once the window is closed and mainLoop returns, we'll make sure to deallocate the resources we've used in the cleanup function.

这个程序封装了一个class,我们将Vulkan对象保存为private成员,添加了初始化它们的函数,在initVulkan 函数中调用这些初始化函数。一旦一切就绪,我们就进入主循环,开始渲染帧。我们将在mainLoop 函数中循环,直到窗口关闭为止。一旦窗口关闭,mainLoop 返回时,我们将确保在cleanup 函数中销毁使用过的资源。

If any kind of fatal error occurs during execution then we'll throw a std::runtime_error exception with a descriptive message, which will propagate back to the main function and be printed to the command prompt. To handle a variety of standard exception types as well, we catch the more general std::exception. One example of an error that we will deal with soon is finding out that a certain required extension is not supported.

如果执行过程中发生任何error,那么我们将抛出一个std::runtime_error 异常和描述性消息,它会被传回main 函数,打印到控制台。为了处理各种标准异常类型,我们捕捉更泛型的std::exception。我们很快就要处理的一个error的例子是,发现某个要求的扩展不被支持。

Roughly every chapter that follows after this one will add one new function that will be called from initVulkanand one or more new Vulkan objects to the private class members that need to be freed at the end in cleanup.

粗略地说,本章之后的每篇教程都会添加被initVulkan函数调用的新函数,以及添加1个或多个Vulkan对象(作为private成员),这些成员最后都要在cleanup函数中被释放。

Resource management 资源管理

Just like each chunk of memory allocated with malloc requires a call to free, every Vulkan object that we create needs to be explicitly destroyed when we no longer need it. In modern C++ code it is possible to do automatic resource management through the utilities in the <memory> header, but I've chosen to be explicit about allocation and deallocation of Vulkan objects in this tutorial. After all, Vulkan's niche is to be explicit about every operation to avoid mistakes, so it's good to be explicit about the lifetime of objects to learn how the API works.

正如每块用malloc 申请的内存都需要用free释放,当我们不需要它们的时候,我们创建的每个Vulkan对象都需要显式地被销毁。在现代C++代码中,可以用<memory>头文件实现自动化的资源管理,但在本教程中我选择了显式地申请和销毁Vulkan对象。毕竟,Vulkan的利基是,无论什么操作都是显式的,以避免错误。所以显式地处理对象的生命周期,对于学习这个API的工作方式是有益的。

After following this tutorial, you could implement automatic resource management by overloading std::shared_ptr for example. Using RAII to your advantage is the recommended approach for larger Vulkan programs, but for learning purposes it's always good to know what's going on behind the scenes.

学习完本教程后,你可以实现自动资源管理(例如通过std::shared_ptr )。对于大型的Vulkan程序,使用 RAII是一种推荐方式,但是为了学习,知道场面背后在发生什么,总是好的。

Vulkan objects are either created directly with functions like vkCreateXXX, or allocated through another object with functions like vkAllocateXXX. After making sure that an object is no longer used anywhere, you need to destroy it with the counterparts vkDestroyXXX and vkFreeXXX. The parameters for these functions generally vary for different types of objects, but there is one parameter that they all share: pAllocator. This is an optional parameter that allows you to specify callbacks for a custom memory allocator. We will ignore this parameter in the tutorial and always pass nullptr as argument.

Vulkan对象要么是用vkCreateXXX这样的函数直接创建,要么是用vkAllocateXXX这样的函数通过其他对象申请。在确定一个对象不会再被使用后,你需要用对应的vkDestroyXXX 或vkFreeXXX函数销毁它。不同对象的这些函数的参数区别很大,但是它们都有一个共同的参数:pAllocator。这是一个可选参数,它允许你标识一个回调函数,用于以自定义的方式分配内存。本教程中我们将忽略这个参数,总是传给它nullptr 。

Integrating GLFW 集成GLFW

Vulkan works perfectly fine without a creating a window if you want to use it off-screen rendering, but it's a lot more exciting to actually show something! First replace the #include <vulkan/vulkan.h> line with

如果你想用Vulkan搞离屏渲染,那么不用创建窗口,Vulkan也可以工作得很好。但是显示点东西才刚令人兴奋!首先用下述代码替换#include <vulkan/vulkan.h>



1 #define GLFW_INCLUDE_VULKAN
2 #include <GLFW/glfw3.h>



That way GLFW will include its own definitions and automatically load the Vulkan header with it. Add a initWindow function and add a call to it from the run function before the other calls. We'll use that function to initialize GLFW and create a window.

这样,GLFW将包含它自己的定义,且自动加载Vulkan头文件。添加initWindow 函数,在run 函数调用其他函数前,先调用它。我们将用这个函数初始化GLFW,并创建窗口。



1 void run() {
 2     initWindow();
 3     initVulkan();
 4     mainLoop();
 5     cleanup();
 6 }
 7  
 8 private:
 9     void initWindow() {
10  
11     }



The very first call in initWindow should be glfwInit(), which initializes the GLFW library. Because GLFW was originally designed to create an OpenGL context, we need to tell it to not create an OpenGL context with a subsequent call:

initWindow 函数中第一个函数调用应该是glfwInit(),它初始化GLFW库。因为GLFW最初是被设计为创建OpenGL上下文而用的,我们需要在接下来的函数调用中告诉它不要创建OpenGL上下文:



glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);



Because handling resized windows takes special care that we'll look into later, disable it for now with another window hint call:

因为处理大小可变的窗口要消耗更多精力,我们稍后再谈它,现在暂且禁用它:



glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);



All that's left now is creating the actual window. Add a GLFWwindow* window; private class member to store a reference to it and initialize the window with:

现在剩下的就是创建实际的窗口了。添加private成员GLFWwindow* window;,以保存对窗口的引用。用下述代码初始化窗口:



window = glfwCreateWindow(800, 600, "Vulkan", nullptr, nullptr);



The first three parameters specify the width, height and title of the window. The fourth parameter allows you to optionally specify a monitor to open the window on and the last parameter is only relevant to OpenGL.

前3个参数标明窗口的宽度、高度和标题。第4个参数允许你可选地标明在哪个显示器上打开窗口,最后一个参数只与OpenGL有关。

It's a good idea to use constants instead of hardcoded width and height numbers because we'll be referring to these values a couple of times in the future. I've added the following lines above the HelloTriangleApplicationclass definition:

用常量代替硬编码的宽度和高度数值是个好主意,因为我们将多次引用这些数据。我在类定义之前添加了下述2行:



1 const int WIDTH = 800;
2 const int HEIGHT = 600;



and replaced the window creation call with

用下述代码代替了创建窗口的函数调用:



window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr);



You should now have a initWindow function that looks like this:

现在你应该有个长这样的initWindow 函数:



1 void initWindow() {
2     glfwInit();
3  
4     glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
5     glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
6  
7     window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr);
8 }



To keep the application running until either an error occurs or the window is closed, we need to add an event loop to the mainLoop function as follows:

为了让app持续运行,直到发生错误或窗口关闭,我们需要在mainLoop 函数中添加一个事件循环,如下代码所示:



1 void mainLoop() {
2     while (!glfwWindowShouldClose(window)) {
3         glfwPollEvents();
4     }
5 }



This code should be fairly self-explanatory. It loops and checks for events like pressing the X button until the window has been closed by the user. This is also the loop where we'll later call a function to render a single frame.

这个代码是不言自明的。它循环检查事件(例如点击X按钮),直到窗口被用户关闭为止。在这个循环里,我们稍后会调用另一个函数来渲染一帧。

Once the window is closed, we need to clean up resources by destroying it and terminating GLFW itself. This will be our first cleanup code:

一旦窗口被关闭,我们需要销毁资源,关闭GLFW。下面是我们最初的cleanup 代码:



1 void cleanup() {
2     glfwDestroyWindow(window);
3  
4     glfwTerminate();
5 }



When you run the program now you should see a window titled Vulkan show up until the application is terminated by closing the window. Now that we have the skeleton for the Vulkan application, let's create the first Vulkan object!

运行此程序,现在你可以看到一个标题为Vulkan 的窗口显示出来,直到你通过关闭按钮终结它。既然我们有了Vulkan程序的骨架,我们来创建第一个Vulkan对象吧!