Vulkan Tutorial

Welcome to the Vulkan Samples Tutorial

本教程分为多个部分,对应于创建一个简单的Vulkan程序所需要的基本步骤。教程的每一部分直接对应于LunarG示例完成进度中的一个示例程序,并且被设计成可以根据示例的开发进度查看并测试实际代码。

Tutorial Index(教程索引)

  • Introduction
  • Instance
  • Enumerate Devices
  • Device
  • Command Buffer
  • Swapchain
  • Depth Buffer
  • Uniform Buffer
  • Pipeline Layout
  • Descriptor Set
  • Render Pass
  • Shaders
  • Framebuffers
  • Vertex Buffer
  • Pipeline
  • Draw Cube

The Goal(学习目标)

完成本教程的最后一节,我们将会生成一个Vulkan应用程序,显示结果如下:
Vulkan Tutorial_第1张图片
© Copyright 2016 LunarG, Inc

Introduction to Vulkan

Vulkan是由Khronos组织开发的一种高级的图形API。其他的图形APIs(如OpenGL和Direct3D)都要求由驱动程序把高层(应用层)API转化适用于底层硬件的指令。当时设计的目的是使开发人员不必管理更复杂的图形硬件细节。

随着这些旧的图形API不断发展,他们慢慢地将越来越多的底层硬件功能直接暴露给程序员。这种方式要求程序员访问底层硬件功能,从而减少了使用较高开销和较低性能的高层API功能的方便性和安全性。

Vulkan的设计就是为了避免由更高层的API导致的更高的开销。因此,Vulkan编程人员在构建Vulkan应用程序时需要考虑更多的细节。但是这允许编程人员更有效地管理应用程序的资源及GPU硬件。这是因为编程人员对应用程序的资源使用模式有更多的了解,因此不必进行其他API被迫做出的代价高昂的假设。

此外,Vulkan还致力于成为比其他图形API跨更多平台的API,不仅支持高端系统,还支持低端移动设备。

About this Tutorial(关于本教程)

Purpose

本教程的目的是介绍如何一步步创建一个简单的Vulkan应用程序,并在此过程中学习Vulkan的基本知识。本教程与LunarG开发的示例进度的代码同步。在阅读本教程过程中,将会指引你查看这些示例程序中的实际代码,以说明开发简单Vulkan应用程序所需的每个步骤。在本教程结束时,你将有完成一个完整的Vulkan程序的创建,并以此开始学习更多关于Vulkan的知识。

要了解该示例的进展情况,请访问LunarG Samples Progression Index。

How to use the Samples Tutorial

学习本教程最高效的方法是同时查看示例进度中的对应代码。我们建议你设置开发环境,以使你能够下载并构建LunarG Vulkan Samples GitHub repository。请阅读该repository主页README文件中的说明来安装构建示例所需要的工具和软件包。示例程序位于该repository的API-Samples文件夹中。一旦构建并成功运行了示例,就可以继续学习本教程。

在查看示例代码时,能够轻松访问作为参考的vulkan.h头文件也很有用。该文件位于示例repository的include目录下。

Vulkan规范文档是Vulkan信息的宝贵来源。你可以在LunarG LunarXchange website官方网站或Khronos Vulkan Registry找到该规范。尽管在阅读示例时没有严格要求要参考规范,但你会发现阅读该规范有助于更深入地理解Vulkan。

Samples Coding Strategy

示例程序重点用于实现某个特定的主题,因此只会显示与该主题相关的代码。与先前主题相关的代码通常被组织成从主程序中调用的函数,以使得与“旧”主题相关的代码不妨碍学习与当前主题相关的代码。你可以随时回顾这些函数,以加深对以前主题的理解。

如下代码中在“旧”主题相关的函数之间的注释部分,描述了示例程序中重点实现某个特定主题的部分代码:

init_instance(info, sample_title);
init_enumerate_device(info);
init_window_size(info, 500, 500);
init_connection(info);
init_window(info);
init_swapchain_extension(info);
init_device(info);
...
/* VULKAN_KEY_START */

... code of interest

/* VULKAN_KEY_END */
...
destroy_device(info);
destroy_window(info);
destroy_instance(info);

在示例代码的源文件中查找这些注释,以快速找到与所讨论的主题相关的代码。

Create a Vulkan Instance

这一节对应的代码文件为 01-init_instance.cpp

示例的第一步是创建一个Vulkan实例。在LunarG Vulkan示例目录的API-Samples文件夹中找到01-init_instance.cpp程序,并准备好在阅读本节有关Vulkan instances的内容时查看它。

Vulkan Instances

Vulkan API使用vkInstance对象存储所有每个应用程序的状态。在应用程序中,必须在执行任何其他Vulkan操作之前创建Vulkan实例。

基本的Vulkan架构看起来如下图所示:
这里写图片描述

上图显示了一个Vulkan应用程序链接到一个通常被称为loader(加载器)的Vulkan库。首先创建一个实例用于初始化loader库。然后loader还会加载并初始化通常由GPU硬件供应商提供的底层图形驱动程序。

注意,该图中所描述的layers(插件库)也是由loader加载。这些layers通常用于校验,即由驱动程序正常执行的错误检查。在Vulkan中的驱动程序比其他API(如OpenGL)的要小得多,部分原因是它们将这种校验功能委托给验证layers。Layers是可选的,并且在应用程序每次创建实例时可以有选择地加载。

Vulkan的layers超出了本教程和示例发展过程的讨论范围。因此本教程不会进一步讲解layers。关于layers的更多信息可以在LunarG LunarXchange网站上找到。

vkCreateInstance

查看01-init_instance.cpp文件的源代码,并找到调用vkCreateInstance函数的代码,该函数原型如下所示:

VkResult vkCreateInstance(
    const VkInstanceCreateInfo*                 pCreateInfo,
    const VkAllocationCallbacks*                pAllocator,
    VkInstance*                                 pInstance);

一点点的分开讨论:

VkResult - 函数的返回状态。你可能希望打开vulkan.h头文件,并在我们遇到这些函数时查看其中的一些定义。您可以在Vulkan SDK安装目录中的include目录中找到该文件。

VkInstanceCreateInfo - 该结构体包含了创建实例所需要的任何其他信息。由于这是一个重要的类型,稍后将会仔细讨论。

VkAllocationCallbacks - 通过配置一个参数指定分配主机内存的方式,允许应用程序执行自己的主机内存管理。否则,Vulkan的实现将使用默认的系统内存管理功能。但是有的应用程序可能想要自己管理主机内存,以便记录内存分配。

在示例程序中不使用此功能,因此你将会看到在所有的示例中,该函数和其他函数中传递一个NULL参数。

VkInstance - 如果实例创建成功,该参数就是函数返回的句柄。这是一个不透明的句柄,所以不要尝试取消引用。许多Vulkan函数都是以这种方式返回所创建的对象句柄。

A Closer Look at the VkInstanceCreateInfo Structure

使用Vulkan函数创建某个实例对象时通常都有一个Vk*Object*CreateInfo参数。在示例中查找初始化该结构体的代码:

typedef struct VkInstanceCreateInfo {
    VkStructureType             sType;
    const void*                 pNext;
    VkInstanceCreateFlags       flags;
    const VkApplicationInfo*    pApplicationInfo;
    uint32_t                    enabledLayerCount;
    const char* const*          ppEnabledLayerNames;
    uint32_t                    enabledExtensionCount;
    const char* const*          ppEnabledExtensionNames;
} VkInstanceCreateInfo;

通常在大多Vulkan CreateInfo结构体中都会用到前两个成员变量。

sType - sType字段指出了结构体的类型。在这个示例中,把该成员设置为 VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,因为它是一个 VkInstanceCreateInfo 结构体。这种做法看起来可能是多余的,因为只有该类型的结构体可以作为 vkCreateInstance() 函数的第一个参数。但实际上是有存在的意义,原因如下:

  • 在驱动程序,校验layer或其他使用该结构体的程序中,可以使用 sType 执行简单的有效性检查,如果sType不符合预期,可能会使请求失败。

  • 在接口没有完全定义接口参数的输入类型时可以通过把该结构体以无类型(void)指针的方式进行传递。例如,如果驱动程序支持对实例创建的扩展,它可以查看通过无类型 pNext 指针(接下来要讨论的)传递的这种结构。在这种情况下,sType 成员将被设置为扩展程序能够识别的值。

由于该变量始终是结构体中的第一个成员,因此使用者可以简单地确定结构体的类型并决定如何处理。

pNext - 经常设置 pNext 变量值为 NULL。此 void 指针有时会用于在指定类型的结构体中传递特定的扩展信息,其中 sType 成员被设置为扩展定义的值。如上所述,扩展程序可以沿着 pNext 指针链分析传递过来的任何结构体,以找到它们能够识别的结构体类型。

flags - 当前还没有定义标记值,因此将其设置为零。

pApplicationInfo - 这是一个指向另一个结构体的指针,该结构体也需要自行赋值。稍后再回过头讨论。

enabledLayerCountppEnabledLayerNames - 本教程中的示例不使用layers,因此把这两个成员变量设为空。

enabledExtensionCountppEnabledExtensionNames - 在当前时刻,本教程中的示例还没有使用扩展。稍后在另一个示例中将演示扩展的使用。

The VkApplicationInfo Structure

这个结构体为Vulkan实现提供了关于应用程序的一些基本信息:

typedef struct VkApplicationInfo {
    VkStructureType    sType;
    const void*        pNext;
    const char*        pApplicationName;
    uint32_t           applicationVersion;
    const char*        pEngineName;
    uint32_t           engineVersion;
    uint32_t           apiVersion;
} VkApplicationInfo;

sTypepNext - 这两个成员与vkInstanceCreateInfo结构中的含义相同。

pApplicationName,applicationVersion,pEngineName,engineVersion - 如果需要,这些可以是由应用程序提供的自由格式的字段。在执行调试或收集报告信息等操作时,工具,loader,layers 或驱动程序的一些实现可以使用这些字段来提供信息。甚至有可能根据正在运行的应用程序来改变驱动程序的执行方式。

apiVersion - 此字段传达用于编译应用程序的Vulkan API头文件的一级,二级和补丁版本。如果使用Vulkan 1.0版本,major 应为1,minor 应为0。使用 vulkan.h 中的 VK_API_VERSION_1_0 宏就可以完成此操作,对应的补丁版本为0。补丁版本的差异,不应影响只是补丁版本有差异的不同版本之间的完全兼容性。一般来说,您应该将此字段设置为VK_API_VERSION_1_0,除非您有充分理由设置为其他的值。

Back to the Code

完成结构体的赋值操作后,示例程序将会创建实例:

VkInstance inst;
VkResult res;

res = vkCreateInstance(&inst_info, NULL, &inst);
if (res == VK_ERROR_INCOMPATIBLE_DRIVER) {
    std::cout << "cannot find a compatible Vulkan ICD\n";
    exit(-1);
} else if (res) {
    std::cout << "unknown error\n";
    exit(-1);
}

vkDestroyInstance(inst, NULL);

在上面的代码中,应用程序创建实例后立即检查最可能出现的错误,并根据结果报告该错误或一些其他错误。注意,返回成功的(VK_ERROR_SUCCESS)值为零,因此许多应用程序将非零结果作为错误值,在这里也使用了这种简单的解释方式。

最后,在应用程序退出之前销毁实例。

现在你已经创建了一个Vulkan实例,现在是时候检查你的Vulkan实例可以使用哪些图形设备。

Enumerate Physical Devices

这一节对应的代码文件为 02-enumerate_devices.cpp

示例的下一步是确定系统上存在的物理设备。

完成实例的创建之后,loader程序已经知道了有多少Vulkan物理设备可用,但是应用程序还不知道。应用程序通过调用 Vulkan API 得到物理设备列表就可以知道有多少设备可用。

Vulkan Tutorial_第2张图片
与实例相关物理设备列表,如图所示。

Getting Lists of Objects from Vulkan

获取对象列表是Vulkan中相当常用的操作,并且API具有一致的模式。返回对象列表的API函数具有 count 和 pointer 参数。其中 count 参数是一个指向整数的指针,因为API可以设置该参数的值。实现步骤如下:

  1. 使用一个指向整型变量的有效指针作为 count 参数,以及一个 NULL 作为 pointer 参数调用函数。
  2. API把列表中对象的数量赋值给 count 参数。
  3. 在应用程序分配足够的空间用于存储列表。
  4. 应用程序再次调用函数,这一次 pointers 参数指向刚分配的空间。

在Vulkan API中你将会经常看到这种调用方式。

vkEnumeratePhysicalDevices Function

vkEnumeratePhysicalDevices函数只是返回系统中所有物理设备句柄的列表。每个物理设备可以是一个插入到台式计算机中的显卡,也可以是在SoC上的某种GPU内核等。如果有多个设备可用,应用程序必须决定具体使用哪一个设备。

枚举物理设备的示例代码如下所示:

// Get the number of devices (GPUs) available.
VkResult res = vkEnumeratePhysicalDevices(info.inst, &gpu_count, NULL);
// Allocate space and get the list of devices.
info.gpus.resize(gpu_count);
res = vkEnumeratePhysicalDevices(info.inst, &gpu_count, info.gpus.data());

注意 info.gpus 是一个 VkPhysicalDevice 类型的 vector(c++ STL) 变量,每个 VkPhysicalDevice 对应一个设备句柄。

在 enumerate 示例程序中的所有操作都是获取物理设备句柄的列表。在下一个示例程序 device 程序中,将会查看此列表并决定使用哪个设备。

The Samples info Structure

此外,你将会注意到在上面的代码中使用了一个 info 变量。在每一个示例程序中都会使用该全局的 info 结构体变量来跟踪 Vulkan 信息和应用程序状态。这种方式有助于使用更紧凑的函数调用来执行本教程中已经介绍的步骤。例如,查看 enumerate 示例程序中的下面一行代码:

`init_instance(info, "vulkansamples_enumerate");`

这行代码会执行本教程的 instance 页面上所讨论的步骤。init_instance() 函数创建实例并将实例句柄存储在 info 中。然后在调用vkEnumeratePhysicalDevices() 函数时使用 info.inst 作为 vkEnumeratePhysicalDevices() 函数的第一个参数。

现在你已经得到了设备列表(GPUs),接下来选择一个 GPU 并创建一个 Vulkan 逻辑设备对象,然后你就可以开始使用该GPU了。

你可能感兴趣的:(Vulkan)