CSharpGL(56)[译]Vulkan入门
本文是对(http://ogldev.atspace.co.uk/www/tutorial50/tutorial50.html)的翻译,作为学习Vulkan的一次尝试。
不翻译的话,每次都在看第一句,那就学不完了。
Background 背景
You've probably heard by now quite a bit about Vulkan, the new Graphics API from Khronos (the non profit organization responsible for the development of OpenGL).
你可能听过Vulkan,Khronos创建的新的图形API。Khronos是负责开发OpenGL的非盈利组织。
Vulkan was announced in Feb-2016 and after 24 years with OpenGL it is a completely new standard and a departure from the current model.
在OpenGL出现24年后,Vulkan于2016年2月被发布。它放弃了当前的模型,它是全新的标准。
I won't go into many details about the various features of Vulkan only to say that in comparison to OpenGL it is much more low level and provides a lot of power and performance opportunities for the developer.
我不会上来就抛出一堆新的特性的细节。简单来说,相比OpenGL,Vulkan底层得多,给开发者提供了巨大的能量和提升性能的机会。
But with great power comes great responsibility.
但是能力越大,责任就越大。
The developer has to take charge of various aspects such as command buffer, synchronization and memory management that were previously the sole responsibility of the driver.
开发者必须负责各种方面,例如命令缓存、同步、内存管理,这些之前都是驱动的责任。
Through the unique knowledge that the developer has about the way the application is structured, the usage of the Vulkan API can be tailored in a way to increase the overall performance of the system.
基于开发者对应用程序结构的知识,他可以调整Vulkan API的用法,获得更高的系统性能。
The thing that surprises people the most, IMHO, about Vulkan is the amount of code that must be written only to get the first triangle on the screen.
最让人吃惊的,恕我直言,是在屏幕上显示第一个三角形所需的巨大代码量。
Comparing this to the few lines we had to write in OpenGL in the first few tutorials this is a major change and becomes a challenge when one tries to write a tutorial about it.
相比在最初的教程中我们写的那几行OpenGL代码,这是很大的改变,也是写Vulkan教程的难点。
Therefore, as always with OGLDEV, I'll try to present the material step by step.
因此,像OGLDEV往常一样,我将一步一步地展开本教程。
We will develop our first triangle demo in a few tutorials, making additional progress in each one.
我们将在多个教程中完成第一个三角形示例,在每个教程中进展一点点。
In addition, instead of laying out the dozens of APIs in one long piece of code I'll present a simple software design that I hope will make it simpler for you to understand without imposing too much restrictions on your future apps.
另外,我不展示一大段API,而是展示有设计思路的软件。我希望这能便于读者理解。
Consider this an educational design which you are free to throw away later.
这个设计以教学为目的,等你学会了,就可以扔掉了。
We will study the core components of Vulkan one by one as we make progress through the code so at this point I just want to present a diagram of the general picture:
随着代码,我们将一个个地学习Vulkan核心组件。现在我们先看一下概览图:
This diagram is by all means not a complete representation.
当然这个图并没有展示所有的组件。
It includes only the major components that will probably be present in most applications.
它只包含会在大部分应用程序中使用的主要组件。
The connectors between the objects represent the dependencies between them at creation or enumeration time.
对象之间的连线表示在创建或枚举时的依赖关系。
For example, in order to create a surface you need an instance object and when you enumerate the physical devices on your system you also need an instance.
例如,为了创建一个surface,你需要一个instance对象;当你枚举你系统上的物理设备时,你也需要一个instance对象。
The two colors roughly describe the software design that we will use.
两种颜色粗略地描述了我们将使用的软件设计方案。
The dark red objects will go into something I call the "core" and the light green objects will go into the "app".
暗红色对象将属于“core”,浅绿色对象将属于“app”。
We will later see why this makes sense.
我们稍后再看为什么是这样。
The application code that you will write will actually inherit from "app" and all of its members will be available for you for further use.
应用程序代码将继承自app,app的所有成员以后都将可用。
I hope this design will provide a solid base to develop future Vulkan tutorials.
我希望这样的设计能提供一个坚实的基础,用于开发将来的Vulkan教程。
System Setup 系统安装
The first thing we need to do is to make sure your system supports Vulkan and get everything ready for development.
我们要做的第一件事,是确保你的系统支持Vulkan,准备好开发所需的一切。
You need to verify that your graphics card supports Vulkan and install the latest drivers for it.
你需要验证你的图形卡是否支持Vulkan,并安装最新的驱动程序。
Since Vulkan is still new it's best to check for drivers updates often because hardware vendors will probably fix a lot of bugs before everything stabilizes.
由于Vulkan还很新,最好经常检查驱动更新,因为硬件厂商可能会在驱动稳定前修复很多bug。
Since there are many GPUs available I can't provide much help here.
由于有太多种GPU,我这里爱莫能助。
Updating/installing the driver on Windows should be fairly simple.
在Windows上更新/安装驱动应该相当简单。
On Linux the process may be a bit more involved.
在Linux上,就有点难缠。
My main development system is Linux Fedora and I have a GT710 card by NVIDIA.
我的开发系统是Linux的Fedora版本,显卡是NVIDIA的GT710。
NVIDIA provide a binary run file which can only be installed from the command line.
NVIDIA提供一个二进制运行文件,只能从命令行安装。
Other vendors have their own processes.
其他厂商有各自的方式。
On Linux you can use the 'lspci' to scan your system for devices and see what GPU you have.
在Linux上你可以用'lspci'命令扫描你的系统,看看有哪些设备,用的什么GPU。
You can use the '-v', '-vv' and '-vvv' options to get increasingly more info on your devices.
你可以用'-v'、'-vv'、'-vvv'来得到你的设备的越来越详细的信息。
The second thing we need is the Vulkan SDK by Khronos, available here.
第二件事,我们需要Khronos的Vulkan SDK,可在此下载。
The SDK includes the headers and libraries we need as well as many samples that you can use to get more info beyond what this tutorial provides.
SDK包含头文件和库文件,很多示例,比本教程多得多的信息。
At the time of writing this the latest version is 1.0.30.0 and I urge you to update often because the SDK is in active development.
写作本文时最新版本是1.0.30.0,我推荐读者时常更新,因为SDK还处于活跃地开发中。
That version number will be used throughout the next few sections so make sure you change it according to the version you have.
接下来的章节都将使用这个版本号,所以,根据你的版本号,相应地替换之。
Linux
Khronos provides a package only for Ubuntu in the form of an executable run file.
Khronos只给Ubuntu提供了一个可执行文件。
Executing this file should install everything for you but on Fedora I encoutered some difficulties so I used the following procedure (which is also forward looking in terms of writing the code later):
执行这个文件就可以安装所需的一切。但是在Fedora上我遇到了一些困难,所以我用下述步骤(也是预览一下代码):
- bash$ chmod +x vulkansdk-linux-x86_64-1.0.30.0.run
- base$ ./vulkansdk-linux-x86_64-1.0.30.0.run --target VulkanSDK-1.0.30.0 --noexec
- base$ ln -s ~/VulkanSDK-1.0.30/1.0.30.0 ~/VulkanSDK
The above commands extract the contents of the package without running its internal scripts.
上述命令提取出包的内容,并执行里面的脚本。
After extraction the directory VulkanSDK-1.0.30.0 will contain a directory called 1.0.30.0 where the actual content of the package will be located.
提取完成后,文件夹VulkanSDK-1.0.30.0会包含一个子文件夹1.0.30.0,里面是实际的内容。
Let's assume I ran the above commands in my home directory (a.k.a in bash as '~') so we should end up with a '~/VulkanSDK' symbolic link to the directory with the actual content (directories such as 'source', 'samples', etc).
假定我是在home文件夹下运行的上述命令(即'~'),那么会有一个'~/VulkanSDK'符号链接到实际内容(文件夹'source'、'samples'等)。
This link makes it easier to switch your development environment to newer versions of the SDK.
这个链接使得切换到SDK的新版本更容易。
It points to the location of the headers and libraries that we need.
它指向我们需要的头文件和库文件的位置。
We will see later how to connect them to the rest of the system. Now do the following:
稍后我们将看到如何将它们连接到系统的其他部分。现在执行下述命令:
- bash$ cd VulkanSDK/1.0.30.0
- bash$ ./build_examples.sh
If everything went well the examples were built into 'examples/build'.
如果一切顺利,示例会出现在文件夹'examples/build'。
To run the examples you must first cd into that directory.
为运行示例,你首先要进入这个文件夹。
You can now run './cube' and './vulkaninfo' to make sure Vulkan runs on your system and get some useful information on the driver.
你现在可以运行'./cube'和'./vulkaninfo'命令来确认Vulkan跑在你的系统上了,还可以得到一些驱动的有用信息。
Hopefully everything is OK so far so we want to create some symbolic links that will make the files we need for development easily accessible from our working environment.
单元一切顺利,目前我们想创建一些符号链接,方便我们使用开发过程中会用到的文件。
Change to the root user (by executing 'su' and entering the root password) and execute the following:
跳到root用户(执行'su'命令,输入root密码),执行下述命令:
- bash# ln -s /home/<your username>/VulkanSDK/x86_x64/include/vulkan /usr/include
- base# ln -s /home/<your username>/VulkanSDK/x86_x64/lib/libvulkan.so.1 /usr/lib64
- base# ln -s /usr/lib64/libvulkan.so.1 /usr/lib64/libvulkan.so
What we did in the above three commands is to create a symbolic link from /usr/include to the vulkan header directory.
上述3个命令,创建了一个从/usr/include到Vulkan头文件夹的符号链接。
We also created a couple of symbolic links to the shared object files against which we are going to link our executables.
我们还创建了一些共享对象的符号链接,今后会将我们的程序链接到这些共享对象。
From now one whenever we download a new version of the SDK we just need to change the symbolic link '~/VulkanSDK' to the new location in order to keep the entire system up to date.
从现在开始,无论何时我们下载了新版本的SDK,我们只需将符号链接'~/VulkanSDK'修改为指向新位置,就可以让整个系统更新完毕。
To emphasis: the procedure as the root user must only be executed once.
强调一点:root用户执行的过程必须只执行一次。
When you get a newer version of the SDK you will only need to extract it and update the symbolic link from your home directory.
当你得到了新版SDK,你只需提取它的内容,从你的home文件夹更新符号链接。
You are free to place that link anywhere you want but the code I provide will assume it is in the home directory so you will need to fix that.
你可以将这个链接房子任何你喜欢的地方,但是我提供的代码中都假定它在home文件夹。所以你需要相应地修改之。
Windows
Installation on Windows is simpler than on Linux.
在Windows上按照比在Linux上简单。
You just need to get the latest version from here, double click the executable installer and after agreeing to the license agreement and selecting the target directory you are done.
你只需从这里下载最新的版本,双击安装包,同意license,选择目标文件夹,万事大吉。
I suggest you install the SDK under c:\VulkanSDK to make it compatible with the Visual Studio solution that I provide, but it is not a must. If you install it somewhere else make sure you update the include and link directories in the project files.
我建议将SDK按照到文件夹c:\VulkanSDK,这样和我提供的Visual Studio解决方案兼容,但不是必须这样。如果你把它安装到其他位置,确保你更新了项目文件中的include和link文件夹。
See details in the next section.
详见下一节。
Building and Running 建设和运行
Linux
My main development environment on Linux is Netbeans.
在Linux上我的主要开发环境是Netbeans。
The source code that accompanies all my tutorials contains project files which can be used with the C/C++ Netbeans download bundle.
本教程的源代码包含项目文件,可以用C/C++Netbeans打开。
If you followed the above system setup procedure then these projects should work out of the box for you (and please let me know if there are any problems).
如果你遵循上述系统建设步骤,那么, 这些项目应该立即可用了(如果有困难请联系我)。
If you are using a different build system you need to make sure to add the following:
如果你在用不同的系统,你需要确保添加下述步骤:
- To the compile command: -I<path to VulkanSDK/1.0.30.0/x86_64/include>
- 加入编译命令:-I<path to VulkanSDK/1.0.30.0/x86_64/include>
- To the link command: -L<path to VulkanSDK/1.0.30.0/x86_64/lib> -lxcb -lvulkan'
- 加入链接命令:-L<path to VulkanSDK/1.0.30.0/x86_64/lib> -lxcb -lvulkan'
Even if you don't use Netbeans I suggest you go into 'ogldev/tutorial50' after you unzip the tutorial source package and run 'make'.
即使你不使用Netbeans,我也建议你解压tutorial source package后,打开文件夹'ogldev/tutorial50',运行'make'。
I provide the makefiles that Netbeans generates so you can check whether your system is able to build them or something is missing.
我提供Netbeans生成的makefile,这样你就可以检查你的系统能建设它们,或是缺少什么。
If everything was ok you can now run 'dist/Debug/GNU-Linux-x86/tutorial50' from within 'ogldev/tutorial50'.
如果一切顺利, 现在你可以运行文件夹'ogldev/tutorial50'下的'dist/Debug/GNU-Linux-x86/tutorial50'。
Windows
If you installed the SDK under 'c:\VulkanSDK' then the Visual Studio project files I supply should work out of the box.
如果你将SDK安装在文件夹'c:\VulkanSDK',那么我提供的Visual Studio项目文件就已经可用了。
If you haven't or you want to setup a Visual Studio project from scratch then follow the steps below.
如果不是,或者你想从零开始设置Visual Studio项目,那么遵循以下步骤。
To update the include directory right click on the project in the solution explorer, go to 'Properties' and then to 'Configuration Properties -> C/C++ -> General'.
更新include文件夹:在solution explorer面板的项目上右键,点击'Properties',选择'Configuration Properties -> C/C++ -> General'。
Now you must add 'c:\VulkanSDK\<version>\Include' to 'Additional Include Directories'.
现在,必须将'c:\VulkanSDK\<version>\Include'添加到'Additional Include Directories'。
See example below:
示例如下:
To update the link directory right click on
the project in the solution explorer, go to 'Properties' and
then to 'Configuration Properties -> Link -> General'.
更新link文件夹:在solution explorer面板的项目上右键,点击'Properties',选择'Configuration Properties -> Link -> General'。
Now you must add 'c:\VulkanSDK\<version>\Bin32' to 'Additional
Library Directories'.
现在,必须将'c:\VulkanSDK\<version>\Bin32'添加到'Additional
Library Directories'。
See example below:
示例如下:
While you are still in the linker settings
go to 'Input' (just one below 'General') and add 'vulkan-1.lib' to 'Additional
Dependencies".
趁你还在链接器设置面板,选择'Input'(在'General'下面一个),添加'vulkan-1.lib'到'Additional Dependencies"。
General Comments 基础命令
Before we get going I have a few comments
about some of my design choices with regard to Vulkan:
正式开始前,我要对我关于Vulkan的设计选择作几点说明:
- Many Vulkan functions (particularly the ones used to create
objects) take a structure as one of the parameters.
很多Vulkan函数(特别是创建对象的函数)接收一个struct作为参数之一。
This structure usually serve as a wrapper for most of the parameters the
function needs and it helps in keeping the number of parameters to the function
low.
这个struct一般用于封装函数需要的大多数参数,利于使函数的参数数量保持在比较低的水平。
The Vulkan architects decided to place a member called sType as
the first member in all these structures.
Vulkan架构决定将所有这些struct的第一个参数设置为名为sType的成员。
This member is of an enum type and every structure has its own code.
这个成员是个枚举类型,每个struct都有自己的枚举值。
This allows the driver to identify the type of the structure using only
its address.
这可以让驱动程序只需知道struct的地址就可以确定它的类型。
All of these enum code have a VK_STRUCTURE_TYPE_ prefix.
所有这些枚举值都有前缀VK_STRUCTURE_TYPE_。
For example, the code for the structure used in the creation of the
instance is called VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO.
例如,在创建instance中使用的struct的枚举值名字是VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO。
Whenever I declare a variable of one of these structure types the first
member I update will be sType.
无论何时我声明一个这样的struct变量,第一个更新的成员都是sType。
To save time I won't comment about it later in the source walkthrough.
为节省时间,我以后不会提及这一点。 - Another comment on these Vulkan structures - they contain quite
a lot of stuff which we don't need in our first few steps.
关于这些Vulkan的struct的另一个说明——它们包含很多我们初期不需要的东西。
To keep the code as short as possible (as well as the text here...) I
always initialize the memory of all structures to zero (using the struct
= {} notation) and I will only set and describe the structure
members that cannot be zero.
为了让代码和文字尽可能短,我总是将所有成员初始化为0(使用struct = {}概念),只会描述不能为0的成员。
I will discuss the stuff that I skipped in future tutorials as they become
relevant.
我将在以后的教程中讨论这里跳过的东西,等需要用它们的时候。 - Vulkan functions are either void or they return a VkResult which
is the error code.
Vulkan函数要么返回void要么返回一个VkResult,表示错误码。
The error code is an enum where VK_SUCCESS is zero and
everything else is greater than zero.
错误码是个枚举类型,其中VK_SUCCESS 是0,其他枚举值都大于0。
When it is possible I check the return value for errors.
可能的话我会检查返回值,看看有没有错误。
If an error occured I print a message to the console (on Windows there
should be a message box) and exit.
如果发生错误,我打印一个消息到控制台(在Windows上应该有个消息盒),然后退出程序。
Error handling in real world applications tend to make the code more
complex and I want to keep it as simple as possible.
真实世界应用程序的错误处理代码,会更加复杂。作为教程,我希望它尽可能简单。 - Many Vulkan functions (particularly of creation type) can take
a pointer to an allocator function.
很多Vulkan函数(特别是创建型的)接收一个指针作为allocator函数。
These allocators allow you to control the process of allocating memory
that the Vulkan functions need.
这些allocator允许你控制Vulkan函数需要的内存被分配的过程。
I consider this as an advanced topic and will not discuss it.
我认为这是个高级话题,就不讨论它了。
We will pass NULL as the allocators so the driver will use its default.
我们将NULL传给allocator,这样驱动会用默认的allocator。 - Vulkan does not guarantee that its functions will be
automatically exposed by the implementing library.
Vulkan不保证它的函数会被自动地被实现库暴露。
This means that on some platforms you might get a segmentation fault when
you call a Vulkan function because it turns out to be a NULL.
这意味着在某些平台上你在调用Vulkan函数时可能遇到段错误,因为函数指针实际上是NULL。
In these cases you have to use vkGetInstanceProcAddr() to
get the function address before it is used (remember that with OpenGL we
had GLEW to save us from all this hassle).
此时你必须在使用函数前用vkGetInstanceProcAddr()得到函数地址。(回忆在OpenGL中我们用GLEW拯救自己于这些困扰中)
My personal experience with my driver was that only
vkCreateDebugReportCallbackEXT() was not available.
我对我的驱动的经验是,只有vkCreateDebugReportCallbackEXT()不可用。
This function is only required for the optional validation layer.
这个函数只在可选验证层需要。
Therefore, I decided to take a risk and release the tutorial without
fetching the addresses for all the functions that I used.
因此,我决定冒险放出这些教程,不保证所有的函数都能找到地址。
If readers will report problems on their platforms I will update the code.
如果读者反馈他们平台上遇到的问题,我将更新我的代码。 - Every serious software has to deal with object deallocation or
it will eventually run out of memory.
每个正经软件都要处理对象释放问题,否则最终会内存不足。
In this tutorial I'm keeping things simple and not destroying any of the
objects that I allocate.
本教程中一切从简,不负责销毁申请的内存。
They are destroyed anyway when the program shuts down.
反正程序关闭时它们都会被销毁。
I will probably revisit this topic in the future but for now just remember
that almost every <vkCreate*() function has a
corresponding vkDestroy*() and you need to be careful if
you are destroying stuff while the program is running.
将来我可能会重提这个话题,但现在就记住几乎所有函数都有对应的函数,如果你要在程序运行时销毁对象,要十分小心。
You can find more information about it here.
你可以在此找到更多信息。
Code Structure 代码结构
Here's a short summary of the files that
contain the code that we are going to review.
先概述一下我们即将遇到的代码文件。
The path relates to the root of the ogldev
software package:
路径是相对ogldev软件包的根目录的:
-
tutorial50/tutorial50.cpp -
location of the main() function.
tutorial50/tutorial50.cpp - main()函数的位置。 -
include/ogldev_vulkan.h -
primary header for all of our Vulkan code.
include/ogldev_vulkan.h -我们的Vulkan代码的主要头文件。
This is the only place where the Vulkan headers by Khronos are included.
唯一包含Khronos的Vulkan头文件的地方。
You can enable the validation layer here by uncommenting ENABLE_DEBUG_LAYERS.
你可以通过取消注释ENABLE_DEBUG_LAYERS来启用验证层。
This file contains a few Vulkan helper functions and macros as well as the
definition of the VulkanWindowControl class.
这个文件包含一些Vulkan辅助函数、宏定义和类型VulkanWindowControl的定义。 -
Common/ogldev_vulkan.cpp -
implementation of the functions defined in ogldev_vulkan.h
Common/ogldev_vulkan.cpp – 在ogldev_vulkan.h中定义的函数的实现。 -
include/ogldev_vulkan_core.h -
declaration of the OgldevVulkanCore which is the primary
class that we will develop.
include/ogldev_vulkan_core.h - OgldevVulkanCore的声明,是我们要开发的主要类型。 -
Common/ogldev_vulkan_core.cpp -
implementation of the OgldevVulkanCore class.
Common/ogldev_vulkan_core.cpp -类型OgldevVulkanCore的实现。 -
include/ogldev_xcb_control.h -
declaration of the XCBControl class that creates a window
surface on Linux.
include/ogldev_xcb_control.h -在Linux上创建窗口的类型XCBControl的声明。 -
Common/ogldev_xcb_control.cpp -
implementation of XCBControl.
Common/ogldev_xcb_control.cpp -类型XCBControl的实现。 -
include/ogldev_win32_control.h -
declaration of the Win32Control class that creates a
window surface on Windows.
include/ogldev_win32_control.h -在Windows上场景窗口表面的类型Win32Control 的声明。 -
Common/ogldev_win32_control.cpp -
implementation of Win32Control.
Common/ogldev_win32_control.cpp -类型Win32Control的实现。
Note that on both Netbeans and Visual
Studio the files are divided between the 'tutorial50' and 'Common' projects.
注意,在Netbeans和Visual Studio中这些文件被分到'tutorial50'和'Common'项目中。
Source walkthru 源代码浏览
I hope that you successfully completed the
above procedures and you are now ready to dive into the internals of Vulkan
itself.
我希望你已经成功地完成了上述步骤,现在你可以深入Vulkan内部了。
As I said, we are going to develop our
first demo in several steps.
我说过,我们计划分几步来开发第一个示例。
The first step will be to setup four
important Vulkan objects: the instance, surface, physical device and logical
device.
第一步是建设4个重要的Vulkan对象:instance,surface,physical device和logical device。(译者注:关键名词我就不翻译了,这样反而更便于理解。)
I'm going to describe this by walking
through my software design but you are welcomed to throw this away and just
follow the Vulkan calls themselves.
我计划在介绍我的软件设计过程中描述这些对象,但是你完全可以扔掉这一思路,直接面对Vulkan函数。
The first thing we need to do is to include
the Vulkan headers.
首先,我们要include一下Vulkan头文件。
I've added ogldev_vulkan.h as the primary
Vulkan include file in my projects.
我已经在项目的ogldev_vulkan.h中加入了Vulkan的基础include文件。
This will be the only place where I will
include the Vulkan header files and everything else will just include this
file.
这是我唯一include了Vulkan头文件的地方,其他位置都只include这个文件。(从而间接incdlue的Vulkan头文件)
Here's the relevant piece of code:
相关代码如下:
#ifdef _WIN32
#define VK_USE_PLATFORM_WIN32_KHR
#include "vulkan/vulkan.h"
#include "vulkan/vk_sdk_platform.h"
#else
#define VK_USE_PLATFORM_XCB_KHR
#include <vulkan/vulkan.h>
#include <vulkan/vk_sdk_platform.h>
9 #endif
Note that we define different _PLATFORM_ macros for Windows and Linux.
注意,我们为Windows和Linux定义了不同的_PLATFORM_宏。
These macros enable the extensions that support the windowing systems in each OS.
这些宏定义启用了支持各个操作系统的窗口的扩展。
The reason that we include the headers like that is that on Linux they are installed in a system directory ( /usr/include/vulkan ) whereas on Windows they are installed in a standard directory.
我们这样include头文件的原因,是在Linux上它们位于系统文件夹(/usr/include/vulkan )而在Windows上它们位于标准文件夹。
Let's start by reviewing the class OgldevVulkanCore whose job is to create and maintain the core objects (note that I'm using red in order to mark all Vulkan structs, enums, functions, etc):
开始时,我们来了解一下OgldevVulkanCore类型,它负责创建和维护核心对象(注意,我用红色标记所有Vulkan的struct,enum,函数等):
class OgldevVulkanCore
{
public:
OgldevVulkanCore(const char* pAppName);
~OgldevVulkanCore(); bool Init(VulkanWindowControl* pWindowControl); const VkPhysicalDevice& GetPhysDevice() const; const VkSurfaceFormatKHR& GetSurfaceFormat() const; const VkSurfaceCapabilitiesKHR GetSurfaceCaps() const; const VkSurfaceKHR& GetSurface() const { return m_surface; } int GetQueueFamily() const { return m_gfxQueueFamily; } VkInstance& GetInstance() { return m_inst; } VkDevice& GetDevice() { return m_device; } private:
void CreateInstance();
void CreateSurface();
void SelectPhysicalDevice();
void CreateLogicalDevice(); // Vulkan objects
VkInstance m_inst;
VkDevice m_device;
VkSurfaceKHR m_surface;
VulkanPhysicalDevices m_physDevices; // Internal stuff
std::string m_appName;
int m_gfxDevIndex;
int m_gfxQueueFamily;
};
This class has three pure Vulkan members (m_inst, surface and m_device) as well as a vector of Vulkan objects called m_physDevices (see the definition below).
这个类型有3个纯Vulkan成员(m_inst、surface和m_device),一个Vulkan对象的列表m_physDevices。
In addition, we have members to keep the application name, an index to the physical device we will be using and an index to the queue family.
另外,我们分别用一个成员记录应用程序名,对physical device的索引和对queue family的索引。
The class also contains a few getter functions and an Init() function that set's everything up.
这个类还包含几个getter函数和一个初始化函数Init()。
Let's see what it does.
我们看看它做了什么。
void OgldevVulkanCore::Init(VulkanWindowControl* pWindowControl)
{
std::vector<VkExtensionProperties> ExtProps;
VulkanEnumExtProps(ExtProps); CreateInstance(); #ifdef WIN32
assert();
#else
m_surface = pWindowControl->CreateSurface(m_inst);
assert(m_surface);
#endif
printf("Surface created\n"); VulkanGetPhysicalDevices(m_inst, m_surface, m_physDevices);
SelectPhysicalDevice();
CreateLogicalDevice();
}
This function takes a pointer to a VulkanWindowControl object.
这个函数接收一个VulkanWindowControl类型的对象指针。
We will review this object later.
我们稍后会得到这个对象。
For now it suffices to say that this is an OS specific class whose job is to create a window surface where rendering will take place.
现在这里就可以说明,这是个操作系统相关的类型,它的工作是创建一个窗口表面,是渲染的场所。
As in OpenGL, the Vulkan core spec does not include windowing.
和OpenGL中一样,Vulkan核心不包含窗口。
This task is left to extensions and we have windowing extensions for all major operating systems.
这个任务留给了扩展,我们有各个主流操作系统的扩展。
An extension is simply an addition to Vulkan which is not part of the core spec.
扩展,是一个Vulkan的附加内容,不属于其核心。
Members of Khronos can publish their own extensions and add them to the registry.
Khronos的成员可以发布他们自己的扩展,并添加到registry。
Driver vendors can decide which extension they want to implement.
驱动程序开发商可以决定他们想实现哪些扩展。
The developer can then query for the list of available extensions during runtime and proceed accordingly.
开发者可以在运行时查询可用扩展,然后据此开展工作。
We start by enumerating all these extensions.
我们从枚举所有扩展开始。
This is done in the following wrapper function:
这通过下述封装函数完成:
void VulkanEnumExtProps(std::vector& ExtProps)
{
uint NumExt = ;
VkResult res = vkEnumerateInstanceExtensionProperties(NULL, &NumExt, NULL);
CHECK_VULKAN_ERROR("vkEnumerateInstanceExtensionProperties error %d\n", res); printf("Found %d extensions\n", NumExt); ExtProps.resize(NumExt); res = vkEnumerateInstanceExtensionProperties(NULL, &NumExt, &ExtProps[]);
CHECK_VULKAN_ERROR("vkEnumerateInstanceExtensionProperties error %d\n", res); for (uint i = ; i < NumExt ; i++) {
printf("Instance extension %d - %s\n", i, ExtProps[i].extensionName);
}
}
The above function is a wrapper to the Vulkan API vkEnumerateInstanceExtensionProperties() which returns the extensions available on the system.
上述函数封装了Vulkan函数vkEnumerateInstanceExtensionProperties(),它返回系统上可用的扩展。
The way we use this function is very common in Vulkan.
使用这个函数的这种方式,在Vulkan中很常见。
The first call returns the number of extensions which we use to resize the extension vector.
第一个函数调用返回扩展的数目,我们据此修改数组的长度。
The second call retrieves the extensions themselves.
第二个函数调用获取扩展本身。
The first parameter can be used to select a specific layer.
第一个参数可用于选择个特定的层。
Vulkan is structured in a way that allows vendors to add logic layers that do stuff like validation, extra logging, etc.
Vulkan的结构,支持厂商添加逻辑层,做一些例如验证、额外日志等事情。
You can decide at runtime which layer you want to enable.
你可以决定在运行时你想启用哪个层。
For example, while developing your application you can enable the validation layer and when distributing it to your users - disable it.
例如,在开发过程中你可以启用验证层,在发布时则禁用它。
Since we are interested in all the extensions we use NULL as the layer.
由于我们对所有扩展都有兴趣,我们用NULL为层参数。
Once we get the extension list we just print it.
一旦得到了扩展,就打印出来。
If you want to do some additional logic on the extension list you can do it here.
如果你想对扩展列表做点别的什么,现在就可以。
The reason that we print it is to make sure the extensions we enable in the next function are included.
打印出来的目的是确保我们要在下一个函数中启用的扩展是存在的。
Next in the initialization process is the creation of the Vulkan instance:
接下来,要做的初始化工作是创建Vulkan的instance:
void OgldevVulkanCore::CreateInstance()
{
VkApplicationInfo appInfo = {};
appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
appInfo.pApplicationName = m_appName.c_str();
appInfo.engineVersion = ;
appInfo.apiVersion = VK_API_VERSION_1_0; const char* pInstExt[] = {
#ifdef ENABLE_DEBUG_LAYERS
VK_EXT_DEBUG_REPORT_EXTENSION_NAME,
#endif
VK_KHR_SURFACE_EXTENSION_NAME,
#ifdef _WIN32
VK_KHR_WIN32_SURFACE_EXTENSION_NAME,
#else
VK_KHR_XCB_SURFACE_EXTENSION_NAME
#endif
}; #ifdef ENABLE_DEBUG_LAYERS
const char* pInstLayers[] = {
"VK_LAYER_LUNARG_standard_validation"
};
#endif VkInstanceCreateInfo instInfo = {};
instInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
instInfo.pApplicationInfo = &appInfo;
#ifdef ENABLE_DEBUG_LAYERS
instInfo.enabledLayerCount = ARRAY_SIZE_IN_ELEMENTS(pInstLayers);
instInfo.ppEnabledLayerNames = pInstLayers;
#endif
instInfo.enabledExtensionCount = ARRAY_SIZE_IN_ELEMENTS(pInstExt);
instInfo.ppEnabledExtensionNames = pInstExt; VkResult res = vkCreateInstance(&instInfo, NULL, &m_inst);
CHECK_VULKAN_ERROR("vkCreateInstance %d\n", res); #ifdef ENABLE_DEBUG_LAYERS
// Get the address to the vkCreateDebugReportCallbackEXT function
my_vkCreateDebugReportCallbackEXT = reinterpret_cast(vkGetInstanceProcAddr(m_inst, "vkCreateDebugReportCallbackEXT")); // Register the debug callback
VkDebugReportCallbackCreateInfoEXT callbackCreateInfo;
callbackCreateInfo.sType = VK_STRUCTURE_TYPE_DEBUG_REPORT_CREATE_INFO_EXT;
callbackCreateInfo.pNext = NULL;
callbackCreateInfo.flags = VK_DEBUG_REPORT_ERROR_BIT_EXT |
VK_DEBUG_REPORT_WARNING_BIT_EXT |
VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT;
callbackCreateInfo.pfnCallback = &MyDebugReportCallback;
callbackCreateInfo.pUserData = NULL; VkDebugReportCallbackEXT callback;
res = my_vkCreateDebugReportCallbackEXT(m_inst, &callbackCreateInfo, NULL, &callback);
CheckVulkanError("my_vkCreateDebugReportCallbackEXT error %d\n", res);
#endif
}
In order to initialize the Vulkan library we must create an VkInstance object.
为了初始化Vulkan库,我们必须创建一个VkInstance对象。
This object carries all the state of the application.
这个对象持有应用程序的所有状态。
The function that creates it is called vkCreateInstance() and it takes most of its parameters in a VkInstanceCreateInfo structure.
创建它的函数是vkCreateInstance(),此函数的参数大部分都在VkInstanceCreateInfo 这个struct里。
The parameters that we are interested in are the extensions and (optionally) the layers we want to enable.
我们感兴趣的参数是需要启用的扩展和层(如果有的话)。
The extensions are the generic surface extension and the OS specific surface extension.
扩展是通用表面扩展和操作系统相关的表面扩展。
The extensions and layers are identified by their name strings and for some of them the Khronos SDK provides a macro.
扩展和层由字符串格式的名字标识,其中有的由Khronos的SDK提供宏定义。
VkInstanceCreateInfo also takes a pointer to a VkApplicationInfo structure.
VkInstanceCreateInfo还接收一个VkApplicationInfo结构体的指针。
This structure describes the application and allows the developer to put in the application name and some internal engine version.
这个结构体描述了应用程序,允许开发者放进应用程序名和一些内部机器版本。
An important field of VkApplicationInfo is apiVersion.
VkApplicationInfo的一个重要字段是apiVersion。
This is the Vulkan version that the application is requesting and if the driver doesn't support it the call will fail.
这是应用程序请求的Vulkan版本,如果驱动不支持此版本,请求就会失败。
We are requesting version 1.0 so it should be ok.
我们要请求的版本是1.0,应该没问题。
Once we get the handle of the instance object we can register a function in the validation layer that will print warning and error messages.
一旦我们得到instance对象的句柄,我们就可以在验证层注册一个函数,用于打印警告和错误信息。
We must first get a pointer to a function called vkCreateDebugReportCallbackEXT, then we populate a VkDebugReportCallbackCreateInfoEXT structure with flags for the stuff we want the driver to notify us about and a pointer to our debug function.
我们首先必须得到函数vkCreateDebugReportCallbackEXT的指针,然后传入结构体VkDebugReportCallbackCreateInfoEXT (标记着我们想要驱动通知我们的内容)和一个调试函数。
The actual registration is done by calling the function whose pointer we previously acquired.
实际注册通过调用函数(通过函数指针)来完成。
We define the pointer to vkCreateDebugReportCallbackEXT and our debug callback as follows:
我们这样定义vkCreateDebugReportCallbackEXT的指针和我们的调试回调函数:
PFN_vkCreateDebugReportCallbackEXT my_vkCreateDebugReportCallbackEXT = NULL;
VKAPI_ATTR VkBool32 VKAPI_CALL MyDebugReportCallback(
VkDebugReportFlagsEXT flags,
VkDebugReportObjectTypeEXT objectType,
uint64_t object,
size_t location,
int32_t messageCode,
const char* pLayerPrefix,
const char* pMessage,
void* pUserData)
{
printf("%s\n", pMessage);
return VK_FALSE; // Since we don't want to fail the original call
}
The next step is to create a window surface and for that we use the VulkanWindowControl object that the Init() function got as a pointer parameter.
下一步,要创建窗口surface,为此,我们使用VulkanWindowControl对象,它是Init()函数的参数
We will review this class later so let's skip it for now (note that we need an instance in order to create a surface so this is why we do stuff in this order).
我们稍后将细说这个类型,暂时先跳过(注意,我们需要用instatnce来创建surface,这是我们按此顺序做事的原因)。
Once we have an instance and a surface we are ready to get all the information we need on the physical devices on your system.
一旦我们有了instance和surface,我们就可以获取你的系统上的physical device的所有信息。
A physical device is either a discrete or an integrated graphics card on the platform.
一个physical device是平台上的一个独立显卡或继承显卡。
For example, your system may have a couple of NVIDIA cards in a SLI formation and an Intel HD graphics GPU integrated into the CPU.
例如,你的系统里可能有2个NIVDIA显卡(构成SLI阵型)和1个Intel高清图形GPU,集成到CPU上。
In this case you have three physical devices.
这样你就又3个physical device。
The function below retrieves all the physical devices and some of their characteristics and populates the VulkanPhysicalDevices structure.
下面的函数检索所有的physical device及其特性,还给出VulkanPhysicalDevices结构体。
This structure is essentially a database of physical devices and their properties.
这个结构体本质上是physical device及其属性的数据库。
It is made up of several vectors (sometimes vectors of vectors) of various Vulkan objects.
它由若干数组(有时是数组的数组)组成,元素为各种Vulkan对象。
In order to access a specific device you simply go to one of the members and index into that vector using the physical device index.
为了使用某个特定的device,你只需通过physical device索引找到某个成员和向量的索引。
So to get all the information on physical device 2 access m_device[2], m_devProps[2], etc.
即,为得到physical device2的信息,只需使用m_device[2]、m_devProps[2]等。
The reason I structured it like that (and not a structure per device with all the info inside it) is because it matches the way the Vulkan APIs work.
我这样设计它的原因是,这样和Vulkan的API风格匹配。
You provide an array of XYZ and get all the XYZ objects for all physical devices.
你提供XYZ数组,就得到所有physical device的XYZ对象。
Here's the definition of that database structure:
下面是数据库结构体的定义:
struct VulkanPhysicalDevices {
std::vector<VkPhysicalDevice> m_devices;
std::vector<VkPhysicalDeviceProperties> m_devProps;
std::vector< std::vector<VkQueueFamilyProperties> > m_qFamilyProps;
std::vector< std::vector<VkBool32> > m_qSupportsPresent;
std::vector< std::vector<VkSurfaceFormatKHR> > m_surfaceFormats;
std::vector<VkSurfaceCapabilitiesKHR> m_surfaceCaps;
};
Now let's take a look at the function that populates the database.
现在来看看用于填充数据库的函数。
The first two parameters are the instance and surface.
前2个参数是instance和surface。
The third parameter is where the result will go to.
第三个参数是结果保存的位置。
We will review this function step by step.
我们一步步地观察这个函数。
void VulkanGetPhysicalDevices(const VkInstance& inst, const VkSurfaceKHR& Surface, VulkanPhysicalDevices& PhysDevices)
{
uint NumDevices = ; VkResult res = vkEnumeratePhysicalDevices(inst, &NumDevices, NULL);
CHECK_VULKAN_ERROR("vkEnumeratePhysicalDevices error %d\n", res);
printf("Num physical devices %d\n", NumDevices);
The first thing we do is get the number of physical devices. Again we see the usage of dual call - first to get the number of items and then to get the items themselves.
我们要做的第一件事,是获取physical device的数量。我们再次看到了“先获取项目数量,再获取项目内容”的用法。
PhysDevices.m_devices.resize(NumDevices);
PhysDevices.m_devProps.resize(NumDevices);
PhysDevices.m_qFamilyProps.resize(NumDevices);
PhysDevices.m_qSupportsPresent.resize(NumDevices);
PhysDevices.m_surfaceFormats.resize(NumDevices);
PhysDevices.m_surfaceCaps.resize(NumDevices);
We can now resize our database so that we will have enough space to retrieve the info on all devices.
我们现在可以设置数据库的大小,让它有足够的空间来检索所有device的信息。
res = vkEnumeratePhysicalDevices(inst, &NumDevices, &PhysDevices.m_devices[]);
CHECK_VULKAN_ERROR("vkEnumeratePhysicalDevices error %d\n", res);
We do the same call again, this time providing the address of a vector in VkPhysicalDevice as the result.
我们调用了相同的函数,这次提供了VkPhysicalDevice里的数组地址,用于保存结果。
Using STL vectors is handly because they function the same way as standard arrays, so the address of the first element is the address of the array.
使用STL数组很方便,因为它们和标准数组功能相同,所以第一个元素的地址就是数组的地址。
From our point of view VkPhysicalDevice is just a handle that represents the identity of the physical device.
从我们的角度看,VkPhysicalDevice 只是一个代表physical device的句柄。
Now we begin a loop over the number of physical devices where we will extract more info for one device at a time.
现在我们开始一个循环,对每个physical device,我们提取更多的信息。
for (uint i = ; i < NumDevices ; i++) {
const VkPhysicalDevice& PhysDev = PhysDevices.m_devices[i];
vkGetPhysicalDeviceProperties(PhysDev, &PhysDevices.m_devProps[i]);
We start by getting the properties of the current device.
开始,我们获取当前device的属性。
m_devProps is a vector of VkPhysicalDeviceProperties.
m_devProps是VkPhysicalDeviceProperties的数组。
This structure contains information about the device such as a name, versions, IDs, etc.
这个struct包含device的信息,例如名字,版本,编号,等。
We print some of these properties in the next couple of printf statements:
我们打印出一些属性来:
printf("Device name: %s\n", PhysDevices.m_devProps[i].deviceName);
uint32_t apiVer = PhysDevices.m_devProps[i].apiVersion;
printf(" API version: %d.%d.%d\n", VK_VERSION_MAJOR(apiVer),
VK_VERSION_MINOR(apiVer),
VK_VERSION_PATCH(apiVer));
Next we get the properties of all the queue families that the physical device supports.
下一步,我们得到physical device支持的所有queue family的属性。
There are four categories of operations that a GPU can perform :
一个GPU能施展的操作有4种:
- Graphics - 2D/3D rendering (same as OpenGL).
图形
- 2D/3D渲染(和OpenGL相同) - Compute - general processing work which is not rendering in
nature. This can be scientific calculations that need the parallel power
of the GPU but not the 3D pipeline.
计算
- 通用处理工作,不限于渲染。可以是科学计算(需要GPU的并行计算能力而不需要3D管道)。 - Transfer - copying of buffers and images.
转移
- 复制缓存和图像。 - Sparse Memory Management - sparse resources are non
continguous. This category includes operations to process them.
稀疏内存管理
- 稀疏资源是不连续的。这类操作用来处理它们。
The work that we send to the device is
executed in a queue.
我们发送到device的工作会在一个queue里执行。
A device exposes one or more queue families
and each family contains one or more queues.
一个device暴露一个或多个queue family,每个family包含一个或多个queue。
Each family supports some combination of
the four categories above.
每个family都支持这4种操作的组合。
The queues in each family all support the
family functionality.
一个family的queue支持此family的全部功能。
For example, my GPU has two families.
例如,我的GPU有2个family。
The first one contains 16 queues that
support all the four categories and the other has just one queue that only
supports transfer.
第一个包含16个queue,它们支持所有4种操作;另一个只有1个queue,它只支持转移操作。
You can take advantage of the specific
architecture of the device at runtime in order to tailor the behavior of your
app in order to increase performance.
你可以利用device的特定架构,在运行时定制你的app的行为,从而提升性能。
uint NumQFamily = ; vkGetPhysicalDeviceQueueFamilyProperties(PhysDev, &NumQFamily, NULL); printf(" Num of family queues: %d\n", NumQFamily); PhysDevices.m_qFamilyProps[i].resize(NumQFamily);
PhysDevices.m_qSupportsPresent[i].resize(NumQFamily); vkGetPhysicalDeviceQueueFamilyProperties(PhysDev, &NumQFamily, &(PhysDevices.m_qFamilyProps[i][]));
In the code above we get the number of family properties for the current device, resize m_qFamilyProps and m_qSupportsPresent (note that both are vectors of vectors so we must first index into the current device) and after that we get a vector of properties and store it in the database.
上述代码中,我们得到了当前device的family属性的数目,修正m_qFamilyProps和m_qSupportsPresent的大小(注意,它们都是数组的数组,所以必须先索引到当前device),然后我们得到了属性数组,将其保存到数据库。
for (uint q = ; q < NumQFamily ; q++) {
res = vkGetPhysicalDeviceSurfaceSupportKHR(PhysDev, q, Surface, &(PhysDevices.m_qSupportsPresent[i][q]));
CHECK_VULKAN_ERROR("vkGetPhysicalDeviceSurfaceSupportKHR error %d\n", res);
}
While we are still on the queue family subject let's query each family and check whether it supports presentation.
既然我们还在讨论queue family,我们查询一下每个family,检查它是否支持presentation。
vkGetPhysicalDeviceSurfaceSupportKHR() takes a physical device, a surface and a queue family index and returns a boolean value which indicates whether this combination of device and family can present on the specified surface.
函数vkGetPhysicalDeviceSurfaceSupportKHR()接收一个physical device,一个surface和一个queue family索引为参数,返回一个boolean值,表示这个组合是否能表示一个特定的surface。
uint NumFormats = ;
vkGetPhysicalDeviceSurfaceFormatsKHR(PhysDev, Surface, &NumFormats, NULL);
assert(NumFormats > ); PhysDevices.m_surfaceFormats[i].resize(NumFormats); res = vkGetPhysicalDeviceSurfaceFormatsKHR(PhysDev, Surface, &NumFormats, &(PhysDevices.m_surfaceFormats[i][]));
CHECK_VULKAN_ERROR("vkGetPhysicalDeviceSurfaceFormatsKHR error %d\n", res); for (uint j = ; j < NumFormats ; j++) {
const VkSurfaceFormatKHR& SurfaceFormat = PhysDevices.m_surfaceFormats[i][j];
printf(" Format %d color space %d\n", SurfaceFormat.format , SurfaceFormat.colorSpace);
}
Next up is the surface format.
接下来是surface的格式。
Each surface can support one or more formats.
每个surface都可以支持一个或多个格式。
A format is simply the way data is arranged on the surface.
格式是数据在surface上安排的方式。
In general, a format specifies the channels in each pixel and the type of each channel (float, int, etc).
一般的,格式描述了每个像素的通道和每个通道的类型(float、int,等)。
For example, VK_FORMAT_R32G32B32_SFLOAT is a surface format with three channels (red, green and blue) of the 32bit floating point type.
例如,VK_FORMAT_R32G32B32_SFLOAT这个surface格式,有3个通道(RGB),每个都是32位的浮点型数。
The format of the surface is critical because it determines the way data on the surface is converted or interpreted before various operations (e.g. displaying it to the screen).
Surface的格式是严格的,因为它决定了数据在surface上的转换和翻译方式(例如在显示到屏幕时)。
To get the format we need both the physical device and the surface itself because the devices may not be compatible in terms of the surface formats that they can work with.
为了得到格式,我们需要physical device和surface作为参数,因为device可能不兼容surface的格式。
There can be multiple surface formats available which is why again we have a vector of vectors here.
Surface的格式可能有多个,所以我们需要用数组的数组。
We will need the surface format later which is why it is part of the database. Now we query the surface capabilities:
稍后我们将需要surface格式,所以它是数据库的一部分。现在我们查询surface的capabilities:
res = vkGetPhysicalDeviceSurfaceCapabilitiesKHR(PhysDev, Surface, &(PhysDevices.m_surfaceCaps[i]));
CHECK_VULKAN_ERROR("vkGetPhysicalDeviceSurfaceCapabilitiesKHR error %d\n", res); VulkanPrintImageUsageFlags(PhysDevices.m_surfaceCaps[i].supportedUsageFlags);
}
}
The VkSurfaceCapabilitiesKHR structure describes the capabilities of the physical device when working with a specific surface.
VkSurfaceCapabilitiesKHR 结构体描述了physical device与特定surface合作时的capabilities。
This includes the minimum and maximum images that can be created on the swap chain, the minimum and maximum extents (size of the area that can be rendered), supported rotation, etc.
这包含交换链上可创建的最小和最大图像,最小和最大扩展(可被渲染的大小),支持的旋转,等。
There is one such structure for each combination of a physical device and surface and we store it in the m_surfaceCaps vector.
每个physical device和surface的组合都有一个这样的结构体,我们将其保存到m_surfaceCaps数组。
We completed getting all the information on the physical devices! (note that some of it is specific to the combination of a device and surface).
我们完全获取了physical device的所有信息!(注意,某些是针对特定的device和surface组合的)
The next step in the Init() function is to select one of the physical devices and one of the queues to do the processing.
函数Init()中的下一步是,选择一个physical device和一个queue。
The following function does exactly that:
下述函数实现了这个步骤:
void OgldevVulkanCore::SelectPhysicalDevice()
{
for (uint i = ; i < m_physDevices.m_devices.size() ; i++) { for (uint j = ; j < m_physDevices.m_qFamilyProps[i].size() ; j++) {
VkQueueFamilyProperties& QFamilyProp = m_physDevices.m_qFamilyProps[i][j]; printf("Family %d Num queues: %d\n", j, QFamilyProp.queueCount);
VkQueueFlags flags = QFamilyProp.queueFlags;
printf(" GFX %s, Compute %s, Transfer %s, Sparse binding %s\n",
(flags & VK_QUEUE_GRAPHICS_BIT) ? "Yes" : "No",
(flags & VK_QUEUE_COMPUTE_BIT) ? "Yes" : "No",
(flags & VK_QUEUE_TRANSFER_BIT) ? "Yes" : "No",
(flags & VK_QUEUE_SPARSE_BINDING_BIT) ? "Yes" : "No"); if (flags & VK_QUEUE_GRAPHICS_BIT) {
if (!m_physDevices.m_qSupportsPresent[i][j]) {
printf("Present is not supported\n");
continue;
} m_gfxDevIndex = i;
m_gfxQueueFamily = j;
printf("Using GFX device %d and queue family %d\n", m_gfxDevIndex, m_gfxQueueFamily);
break;
}
}
} if (m_gfxDevIndex == -) {
printf("No GFX device found!\n");
assert();
}
}
In a more advanced application you can have multiple queues on multiple devices but we are keeping it very simple.
在高级应用程序中你可以使用多个device上的多个queue,但是我们这里就简单为上。
The nested loop in this function traverses the list of devices and the list of queue families for each device.
函数中的嵌套循环遍历device数组及其每个queue family数组。
We are searching for a physical device with a queue family that support graphics functionality as well as being able to present on the surface for which the database was initialized.
我们要找这样一个physical device及其queue family——它支持图形功能,且能在初始化了数据库的surface上显示。
When we find such device and family we store their corresponding indices and quit the loop.
当我们找到这样的device和family时,我们保存它们的索引,退出循环。
This device and family pair is going to serve us throughout this tutorial.
这个device和family对将为我们整个教程服务。
If no such pair is found the application aborts.
如果没有这样的对,应用程序就中止。
It means the system doesn't meet the minimum requirements to run the code.
这意味着系统不支持运行此代码所需的最低要求。
The last thing we need to do to initialiaze our core object is to create a logical device:
关于初始化核心对象,最后要做的是创建logical device:
void OgldevVulkanCore::CreateLogicalDevice()
{
float qPriorities = 1.0f;
VkDeviceQueueCreateInfo qInfo = {};
qInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
qInfo.queueFamilyIndex = m_gfxQueueFamily;
qInfo.queueCount = ;
qInfo.pQueuePriorities = &qPriorities; const char* pDevExt[] = {
VK_KHR_SWAPCHAIN_EXTENSION_NAME
}; VkDeviceCreateInfo devInfo = {};
devInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
devInfo.enabledExtensionCount = ARRAY_SIZE_IN_ELEMENTS(pDevExt);
devInfo.ppEnabledExtensionNames = pDevExt;
devInfo.queueCreateInfoCount = ;
devInfo.pQueueCreateInfos = &qInfo; VkResult res = vkCreateDevice(GetPhysDevice(), &devInfo, NULL, &m_device); CHECK_VULKAN_ERROR("vkCreateDevice error %d\n", res); printf("Device created\n");
}
The Vulkan architecture separates the concept of a physical device which is part of the real system and the logical device which is an abstraction on top of it.
Vulkan架构区分了physical device和logical device,一个是真实的系统的一部分,另一个是在此基础上的抽象。
The logical device is what we use in the application to create most of the objects that are below the device level (queues, swap chains, etc).
logical device是我们在应用程序中用于创建大多数属于device层对象(queue,交换链,等)的工具。
This design choice enables flexibility in the management of device resource as well as configuration of device behavior to fit our needs.
这个设计决策增加了可扩展性,使得device资源管理和行为配置能灵活适应我们的需求。
The logical device allows us to expose only parts of the physical device.
且logical device允许我们只暴露physical device的一部分。
For example, if the physical device supports both graphics and compute we can expose only graphics via the logical device.
例如,如果physical device同时支持图形和计算,我们可以通过logical device来只暴露图形部分。
In order to create the device we need one VkDeviceCreateInfo structure and one or more VkDeviceQueueCreateInfo structures.
为创建device,我们需要1个VkDeviceCreateInfo 结构体和若干VkDeviceQueueCreateInfo 结构体。
VkDeviceCreateInfo is the main point of device definition.
VkDeviceCreateInfo 是device定义的主要部分。
In that struct we set a pointer to an array of extensions we want to enable.
这个结构体中,我们设置一个需要启用的扩展的数组的指针。
We need to enable the swap chain which is defined as part of an extension and not part of the core spec.
我们需要启用交换链,它是扩展的一部分,不是核心。
A swap chain is an array of surface images that can be presented.
交换链是surface图像数组,可以被显示。
We also need the size of the extension array.
我们还需要扩展数组的大小。
The second thing we need is a pointer to an array of VkDeviceQueueCreateInfo structs (and the size of that array).
我们需要的第二个是VkDeviceQueueCreateInfo 结构体数组的指针(及其大小)。
For each queue family we want to enable we must have one VkDeviceQueueCreateInfo struct which describes it.
想启用一个queue family,就必须有一个描述它的VkDeviceQueueCreateInfo 结构体。
This struct specifies the index of the queue family (which we got earlier in SelectPhysicalDevice()), the number of queues we want to create from that family and for each queue we can specify a different priority.
这个结构体记录了queue family的索引(我们之前在SelectPhysicalDevice()中得到了),我们希望从此family中创建的queue的数量,对每个queue我们都可以指定一个不同的优先级。
In this tutorial we we won't deal with priorities.
本教程中我们不会处理优先级的问题。
We have just one queue and we set the priority to 1.0.
我们只有1个queue,将其优先级设置为1.0。
This completes the initialization process of our OgldevVulkanCore class, but to actually call the Init() function we need a VulkanWindowControl which is a class I created to wrap the management of the window surface.
至此OgldevVulkanCore 类型的初始化就完成了。 但是想调用Init()函数,我们需要一个VulkanWindowControl 类型,这是我创建的峰值了窗口surface的管理工作的类型。
Remember that this job is not part of the core Vulkan spec and since every operating system needs entirely different code here I found that it makes sense to separate all window management to a different class and implement OS specific versions in the derivatives of this class.
记住,这个工作不是Vulkan核心的一部分。由于每种操作系统都需要完全不同的代码,我任务应该将窗口管理单独成立一个类,然后继承之,针对各个操作系统实现特定的程序。
This class is actually an interface and it is defined as follows:
这个类实际上是个接口,其定义如下:
class VulkanWindowControl
{
protected:
VulkanWindowControl() {}; ~VulkanWindowControl() {}; public: virtual bool Init(uint Width, uint Height) = ; virtual VkSurfaceKHR CreateSurface(VkInstance& inst) = ;
};
As you can see, this class is very simple.
如你所见,这个类很简单。
It doesn't have any members.
它没有任何成员。
Its constructor and destructor are protected so you can't create an instance of this class directly.
它的构造函数和析构函数都是protected,所以你不能直接创建此类的实例。
You can only create an instance of a derived class.
你只能创建它的继承类的实例。
There are two public methods.
有2个public的方法。
One to initialize the object and one to create a Vulkan surface.
一个是初始化对象,另一个是创建Vulkan的surface。
So for every OS we are free to do OS specific stuff but we have to return a VkSurfaceKHR which is a handle to the window surface.
所以,对每种操作系统,我们可以*地做特定操作系统的事情,但是必须返回一个VkSurfaceKHR 对象,它是窗口surface的句柄。
Note that CreateSurface() takes an VkInstance reference as a parameter which means we can init this class before we create our VulkanCoreObject but we have to initialize the VulkanCoreObject before we call CreateSurface().
注意,函数CreateSurface()接收一个VkInstance 参数,这意味着我们可以在创建VulkanCoreObject之前初始化这个类,但是我们必须在调用函数CreateSurface()之前初始化VulkanCoreObject 。
We will review this again when we get to the main() function so don't worry about it.
我们将重审这一点,当我们涉及main()函数的时候。所以不用担心它。
The concrete classes which implements VulkanWindowControl are XCBControl on Linux and Win32Control on Windows.
实现VulkanWindowControl 的具体类型,在Linux上是XCBControl ,在Windows上是Win32Control 。
We will review Linux first and then Windows:
我们先来看Linux上的,然后再看Windows上的:
class XCBControl : public VulkanWindowControl
{
public:
XCBControl(); ~XCBControl(); virtual bool Init(uint Width, uint Height); virtual VkSurfaceKHR CreateSurface(VkInstance& inst); private:
xcb_connection_t* m_pXCBConn;
xcb_screen_t* m_pXCBScreen;
xcb_window_t m_xcbWindow;
};
XWindow is the most common windowing system on Unix and Linux.
XWindow是Unix和Linux上最常见的窗口系统。
It works in a client/server model.
它以客户端/服务器模式工作。
The server manages the screen, keyboard and mouse.
服务器管理屏幕,键盘和鼠标。
The client is an application that wants to display something on the server.
客户端是应用程序,它想要显示服务器的上的某些东西。
It connects to the server via the X11 protocol and sends it requests to create a window, control the mouse/keyboard, etc.
它通过X11协议与服务器链接,向一个窗口发送请求,控制鼠标键盘,等。
The two most common implementations of the X11 protocol is Xlib and XCB and Vulkan provides extensions to use both.
X11协议的最常见的2种实现是Xlib和XCB,Vulkan提供了扩展来使用这2种实现。
XCB is more modern so this is what we will use on Linux.
XCB 更现代化,所以我们将在Linux上用它。
XCBControl implements the VulkanWindowControl class using XCB calls.
通过调用XCB,XCBControl 实现了VulkanWindowControl 类。
Remember that the target of this object is to create an OS specific window and connect it to a Vulkan surface so that Vulkan will be able to render to it.
记住,这个对象的目标,是创建操作系统特定的窗口,链接到Vulkan的surface,以便Vulkan能在它上面渲染。
Let's start by creating the window.
开始,让我们来创建窗口。
void XCBControl::Init(uint Width, uint Height)
{
m_pXCBConn = xcb_connect(NULL, NULL); int error = xcb_connection_has_error(m_pXCBConn); if (error) {
printf("Error opening xcb connection error %d\n", error);
assert();
} printf("XCB connection opened\n");
The first thing we need to do is to connect to the XWindow server.
首先要做的,是链接到XWindow服务器。
You are probably running on a GUI environment so the server is already running in the background.
你可能政治运行一个GUI环境,所以服务器已经启动了。
xcb_connect() opens a connection to that server.
函数xcb_connect()会打开一个到服务器的链接。
It takes two parameters - the name of the server and a pointer to a preferred screen number (which will be populated by the XCB library).
它接收2个参数——服务器名字和屏幕编号的指针(由XCB库提供)。
XWindow is very flexible.
XWindow可扩展性很强。
For example, it allows you to run the server on one machine and the client on another.
例如,它允许你在一台机器上运行服务器,在领另一台机器上运行客户端。
You can also run multiple instances of the same server on the same machine.
你也可以在同一台机器上运行服务器的多个实例。
To connect remotely you need to specify the name or IP as well as a display number in some specific string format and provide it in the first parameter.
为了链接到远程服务器,你需要标明服务器名字或IP地址以及显示编号到第一个参数(字符串格式)里。
To run the application locally it is enough to use NULL for both parameters.
如果是在本地运行应用程序,使用NULL为参数值即可。
xcb_connect() returns a connection pointer which we store in the class.
xcb_connect()返回链接指针,我们将其保存到类里。
It always returns something so we have to check for errors using xcb_connectionn_has_error() in the manner that we see above.
它总会返回一些东西,所以我们必须检查是否有错误。方法是,用类似上文的方式,使用函数xcb_connectionn_has_error()。
const xcb_setup_t *pSetup = xcb_get_setup(m_pXCBConn); xcb_screen_iterator_t iter = xcb_setup_roots_iterator(setup); m_pXCBScreen = iter.data; printf("XCB screen %p\n", m_pXCBScreen);
A XWindow server can control multiple monitors and potentially run multiple screens on each monitor.
一个XWindow服务器可以控制多个显示器,在每个显示器上都可以运行多个窗口。
A screen is where applications are eventually executed.
一个窗口是应用程序执行(显示)的地方。
It is defined by a width and height, a color depth, etc.
它由宽度,高度,颜色深度,等定义。
We want to get a handle to the current screen and there are two steps that we need to do.
我们想要得到当前窗口的句柄,这需要2步。
The first one is to use the xcb_get_setup() function to get access to the xcb_setup_t structure of the connection.
第一步是使用xcb_get_setup()函数得到链接的结构体xcb_setup_t。
This struct contains a lot of info about the server.
这个结构体包括服务器的很多信息。
One of the things it includes is a list of screens.
其中之一是窗口列表。
To access this list we setup an iterator using the xcb_setup_roots_iterator() function.
为读取这个列表,我们用xcb_setup_roots_iterator()函数设置一个枚举器。
On a more robust piece of code what you will now see is a loop that traverses the list of screens, searching for the one the applications wants.
在一个更健壮的程序中,你将看到的是,在一个循环里遍历窗口列表,搜索应用程序需要的那个。
In our case it is enough to extract the first screen.
在我们的例子中,只需突起第一个窗口即可。
The screen can be retrieved from the 'data' member of the iterator.
窗口可以通过成员'data'来得到。
m_xcbWindow = xcb_generate_id(m_pXCBConn);
We are now ready to create the actual window.
我们现在可以创建实际的窗口了。
The first step to do that is to generate a XID for it.
首先要生成一个XID。
The XID is an unsigned integer identifier of many XWindow resources.
XID是个无符号整数的标识符,用于标记很多XWindow资源。
When the client connects to a server it allocates a range of XIDs for it from a global range in the server.
当客户端链接到服务器时,它在服务器全局范围内申请若干XID。
When the client wants to create some resource on the server it starts by locally allocating an XID from the range it was given.
当客户端想在服务器上创建某资源时,它从这若干XID上开始。
The following function calls can use that XID to identify the resource to be created.
下述函数可以使用XID来标志创建的资源。
This is somewhat unique in the approach where the server doesn't say "here's your new resource and its identifer is XYZ".
这和那种服务器说“这是你的心资源,它的标识符是XYZ”的方式不同。
Instead, the client says "hey server - I want to create a resource and here's the identifier for it".
相反,是客户端说“嘿服务器,我想创建一个资源,这是它的标识符”。
xcb_generate_id() generates the XID for the window and we store it in the member variable m_xcbWindow.
xcb_generate_id()函数为窗口生成XID,我们将其保存到成员变量m_xcbWindow中。
xcb_create_window( m_pXCBConn, // the connection to the XWindow server
XCB_COPY_FROM_PARENT, // color depth - copy from parent window
m_xcbWindow, // XID of the new window
m_pXCBScreen->root, // parent window of the new window
, // X coordinate
, // Y coordinate
Width, // window width
Height, // window height
, // border width
XCB_WINDOW_CLASS_INPUT_OUTPUT, // window class - couldn't find any documentation on it
m_pXCBScreen->root_visual, // the visual describes the color mapping
,
);
xcb_create_window() does the window creation and takes no less than 13 parameters.
函数xcb_create_window()执行了创建窗口的任务,它接收不少于13个参数。
I have some comments on the parameters above and most of them are self-explanatory.
我给了这些参数注释,其中有些参数是自说明的。
I won't go deeper than that.
我就不再深入了。
You can google for more info.
你可以百度到更多信息。
xcb_map_window(m_pXCBConn, m_xcbWindow);
xcb_flush (m_pXCBConn);
}
In order to make the window visible we have to map it and flush the connection which is exactly what the above calls do.
为了让窗口可见,我们必须把它映射和推进链接(译者注:我也看不懂)。上述代码就是做这些的。
VkSurfaceKHR XCBControl::CreateSurface(VkInstance& inst)
{
VkXcbSurfaceCreateInfoKHR surfaceCreateInfo = {};
surfaceCreateInfo.sType = VK_STRUCTURE_TYPE_XCB_SURFACE_CREATE_INFO_KHR;
surfaceCreateInfo.connection = m_pXCBConn;
surfaceCreateInfo.window = m_xcbWindow; VkSurfaceKHR surface; VkResult res = vkCreateXcbSurfaceKHR(inst, &surfaceCreateInfo, NULL, &surface);
CHECK_VULKAN_ERROR("vkCreateXcbSurfaceKHR error %d\n", res); return surface;
}
The last function from the XCBControl class which we want to review is CreateSurface().
我们要评析的XCBControl 类的最后一个函数是CreateSurface()。
This is basically a wrapper around the Vulkan function from the XCB extension vkCreateXcbSurfaceKHR().
它基本上是对来自XCB扩展的vkCreateXcbSurfaceKHR()函数的封装。
We populate the VkXcbSurfaceCreateInfoKHR struct with the XWindow server connection pointer and the window which we created earlier.
我们填入VkXcbSurfaceCreateInfoKHR 结构体,XWindow服务器链接指针和之前创建的窗口。
In return we get a generic handle to a Vulkan surface which we return back to the caller.
我们得到的是一个Vulkan的surface句柄,我们将其返回给调用者。
Now let's review the corresponding class for Windows:
现在我们来评析Windows对应的类型:
class Win32Control : public VulkanWindowControl
{
public:
Win32Control(const char* pAppName); ~Win32Control(); virtual void Init(uint Width, uint Height); virtual VkSurfaceKHR CreateSurface(VkInstance& inst); private: HINSTANCE m_hinstance;
HWND m_hwnd;
std::wstring m_appName;
};
As you can see, the interface is very similar for both operating systems.
如你所见,两种操作系统的接口很相似。
In fact, Init() and CreateSurface() are identical because they are virtual functions.
实际上,Init()函数和CreateSurface()函数是相同的,因为它们都是虚函数。
We also have private members to store two Windows specific handles - HINSTANE and HWND.
我们还用private成员保存2个Windows特有的句柄——HINSTNACE和HWND。
Win32Control::Win32Control(const char* pAppName)
{
m_hinstance = GetModuleHandle(NULL);;
assert(m_hinstance);
m_hwnd = ;
std::string s(pAppName);
m_appName = std::wstring(s.begin(), s.end()); }
Above you can see the constructor for the Win32Control class and I'm only showing it here so that you can see the way that the app name which is provided as an array of char is transformed into a std::wstring.
如上述代码所示,你可以看到Win32Control 类的构造函数。我将其展示在此,是为了让你看到app的名字是如何由char数组转换为字符串std::string的。
We do this for the CreateWindowEx() function below that needs a window name parameter with the LPCTSTR type.
我们为CreateWindowEx()函数搞这么一出,它需要LPCTSTR类型的名称作参数。
The standard wstring class helps us with that.
标准wstring类帮助我们完成这一任务。
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
return DefWindowProc(hwnd, uMsg, wParam, lParam);
} void Win32Control::Init(uint Width, uint Height)
{
WNDCLASSEX wndcls = {}; wndcls.cbSize = sizeof(wndcls);
wndcls.lpfnWndProc = WindowProc;
wndcls.hInstance = m_hinstance;
wndcls.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wndcls.lpszClassName = L"ogldev"; if (!RegisterClassEx(&wndcls)) {
DWORD error = GetLastError();
OGLDEV_ERROR("RegisterClassEx error %d", error);
} m_hwnd = CreateWindowEx(,
L"ogldev", // class name
m_appName.c_str(),
WS_OVERLAPPEDWINDOW | WS_VISIBLE, // window style
, , // window start
Width,
Height,
NULL,
NULL,
m_hinstance,
NULL); if (m_hwnd == ) {
DWORD error = GetLastError();
OGLDEV_ERROR("CreateWindowEx error %d", error);
} ShowWindow(m_hwnd, SW_SHOW);
}
The code above is straightforward window creation stuff which I got from MSDN so I won't go too deeply into it.
上述代码一目了然,是创建窗口相关的东西,我从MSDN拿来的,所以我就不深入了。
We have to register the window class using RegisterClassEx().
我们必须用RegisterClassEx()函数注册窗口。
This class will be associated with the WindowProc() function that serve as the event handler for our window.
这个类将和WindowProc()函数关联,它用作窗口的事件处理器。
Right now we are just calling the default handler of the system but on the next tutorial we will add more details to it.
目前我们调用系统默认的处理器,下个教程我们将添加更多细节。
The window is then created using CreateWindowEx() and finally displayed using ShowWindow().
现在窗口就被CreateWindowEx()创建好了,通过ShowWindow()显示了出来。
VkSurfaceKHR Win32Control::CreateSurface(VkInstance& inst)
{
VkWin32SurfaceCreateInfoKHR surfaceCreateInfo = {};
surfaceCreateInfo.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR;
surfaceCreateInfo.hinstance = m_hinstance;
surfaceCreateInfo.hwnd = m_hwnd; VkSurfaceKHR surface; VkResult res = vkCreateWin32SurfaceKHR(inst, &surfaceCreateInfo, NULL, &surface);
CHECK_VULKAN_ERROR("vkCreateXcbSurfaceKHR error %d\n", res); return surface;
}
CreateSurface() is also very similar to its Linux counterpart.
函数也和它的Linux版本很像。
The surfaceCreateInfo param just takes the instance instead of the XCB connection (and of course - the window handles are of different types).
参数接收instance,而不是XCB链接(当然还有不同类型的窗口句柄)。
int main(int argc, char** argv)
{
VulkanWindowControl* pWindowControl = NULL;
#ifdef WIN32
pWindowControl = new Win32Control(pAppName);
#else
pWindowControl = new XCBControl();
#endif
pWindowControl->Init(WINDOW_WIDTH, WINDOW_HEIGHT); OgldevVulkanCore core("tutorial 50");
core.Init(pWindowControl); return ;
}
At last, we have reached the glue code in the form of the main() function.
最后,我们抵达了main()函数中的胶水代码。
If you are interested, you may start here and create the building blocks step by step so that you can check the return values of each Vulkan function call one at a time.
如果你有兴趣,你可以从此开始,一步步创建building block,这样就可以检查每个Vulkan函数的返回值了。
What happens in this function has already been generally discussed.
这个函数中的事我们已经讨论过了。
We allocate a derivative of the VulkanWindowControl class (be it Linux or Windows).
我们申请一个VulkanWindowControl 类的子类(根据在Linux还是Windows上)。
We initialize it (thus creating the OS specific window) and then create and init the OgldevVulkanCore object.
我们初始化它(即创建操作系统特定的窗口),然后创建和初始化OgldevVulkanCore 对象。
We now have a Vulkan surface connected to an OS window, a Vulkan instance and device and a database with all the physical devices in the system.
我们现在有了Vulkan的surface(链接到操作系统窗口),Vulkan的instance、device和系统中所有physical device的数据库。
I hope you will find this tutorial useful.
我希望你发现这个教程有用。
The T-shirt that should go along with it says "I've written tons of Vulkan code and all I got was this lousy window".
T恤上写着“我已经写了成吨的Vulkan代码,只得到了这个糟糕的窗口”。
Indeed, we have accomplished a lot but we didn't get any rendering in return.
确实,我们做了很多,但是没有得到任何渲染结果。
But don't despair.
但是不要绝望。
You now have a basic structure with a few of the core Vulkan objects.
你现在有了一些基础结构和Vulkan核心对象。
On the next tutorials we will build on this further so stay tuned.
下一篇教程我们将走得更远,所以敬请关注。