Vulkan是一个跨平台的2D和3D绘图应用程序接口(API),最早由科纳斯组织(KhronosGroup)在2015年游戏开发者大会(GDC)上发表 。
科纳斯最先把VulkanAPI称为“下一代OpenGL行动”(nextgenerationOpenGLinitiative)或“glNext”,但在正式宣布Vulkan之后这些名字就没有再使用了 。就像OpenGL,Vulkan针对实时3D程序(如电子游戏)设计,Vulkan并计划提供高性能和低CPU管理负担(overhead),这也是Direct3D12和AMD的Mantle的目标 。Vulkan兼容Mantle的一个分支,并使用了Mantle的一些组件 。
Vulkan旨在提供更低的CPU开销与更直接的GPU控制,其理念大致与Direct3D12和Mantle类似 。
文章插图
介绍并解释Vulkan是什么 。我们会介绍API背后的基本概念,包括初始化、对象生命周期、Vulkan实例以及逻辑和物理设备 。在本章的最后,我们会完成一个简单的Vulkan应用程序,这个程序可以初始化Vulkan系统,查找可用的Vulkan设备并显示其属性和功能,最后彻底地关闭程序 。
1.1 引言Vulkan是一个用于图形和计算设备的编程接口 。Vulkan设备通常由一个处理器和一定数量的固定功能硬件模块组成,用于加速图形和计算操作 。通常,设备中的处理器是高度线程化的,所以在极大程度上Vulkan里的计算模型是基于并行计算的 。Vulkan还可以访问运行应用程序的主处理器上的共享或非共享内存 。Vulkan也会给开发人员提供这个内存 。
Vulkan是个显式的API,也就是说,几乎所有的事情你都需要亲自负责 。驱动程序是一个软件,用于接收API调用传递过来的指令和数据,并将它们进行转换,使得硬件可以理解 。在老的API(例如OpenGL)里,驱动程序会跟踪大量对象的状态,自动管理内存和同步,以及在程序运行时检查错误 。这对开发人员非常友好,但是在应用程序经过调试并且正确运行时,会消耗宝贵的CPU性能 。Vulkan解决这个问题的方式是,将状态跟踪、同步和内存管理交给了应用程序开发人员,同时将正确性检查交给各个层进行代理,而要想使用这些层必须手动启用 。这些层在正常情况下不会在应用程序里执行 。
由于这些原因,Vulkan难以使用,并且在一定程度上很不稳定 。你需要做大量的工作来保证Vulkan运行正常,并且API的错误使用经常会导致图形错乱甚至程序崩溃,而在传统的图形API里你通常会提前收到用于帮助解决问题的错误消息 。以此为代价,Vulkan提供了对设备的更多控制、清晰的线程模型以及比传统API高得多的性能 。
另外,Vulkan不仅仅被设计成图形API,它还用作异构设备,例如图形处理单元(Graphics Processing Unit,GPU)、数字信号处理器(Digital Signal Processor,DSP)和固定功能硬件 。功能可以粗略地划分为几类 。Vulkan的当前版本定义了传输类别——用于复制数据;计算类别——用于运行着色器进行计算工作;图形类别——包括光栅化、图元装配、混合、深度和模板测试,以及图形程序员所熟悉的其他功能 。
Vulkan设备对每个分类的支持都是可选的,甚至可以根本不支持图形 。因此,将图像显示到适配器设备上的API(这个过程叫作展示)不但是可选择的功能,而且是扩展功能,而不是核心API 。
1.2 实例、设备和队列Vulkan包含了一个层级化的功能结构,从顶层开始是实例,实例聚集了所有支持Vulkan的设备 。每个设备提供了一个或者多个队列,这些队列执行应用程序请求的工作 。
Vulkan实例是一个软件概念,在逻辑上将应用程序的状态与其他应用程序或者运行在应用程序环境里的库分开 。系统里的物理设备表示为实例的成员变量,每个都有一定的功能,包括一组可用的队列 。
物理设备通常表示一个单独的硬件或者互相连接的一组硬件 。在任何系统里,都有一些数量固定的物理设备,除非这个系统支持重新配置,例如热插拔 。由实例创建的逻辑设备是一个与物理设备相关的软件概念,表示与某个特定物理设备相关的预定资源,其中包括了物理设备上可用队列的一个子集 。可以通过创建多个逻辑设备来表示一个物理设备,应用程序花大部分时间与逻辑设备交互 。
图1.1展示了这个层级关系 。图1.1中,应用程序创建了两个Vulkan实例 。系统里的3个物理设备能够被这两个实例使用 。经过枚举,应用程序在第一个物理设备上创建了一个逻辑设备,在第二个物理设备创建了两个逻辑设备,在第三个物理设备上创建了一个逻辑设备 。每个逻辑设备启用了对应物理设备队列的不同子集 。在实际开发中,大多数Vulkan应用程序不会这么复杂,而会针对系统里的某个物理设备只创建一个逻辑设备,并且使用一个实例 。图1.1仅仅用来展示Vulkan的复杂性 。
文章插图
图1.1 Vulkan里关于实例、设备和队列的层级关系
后面的小节将讨论如何创建Vulkan实例,如何查询系统里的物理设备,并将一个逻辑设备关联到某个物理设备上,最后获取设备提供的队列句柄 。
1.2.1 Vulkan实例
Vulkan可以被看作应用程序的子系统 。一旦应用程序连接了Vulkan库并初始化,Vulkan就会追踪一些状态 。因为Vulkan并不向应用程序引入任何全局状态,所以所有追踪的状态必须存储在你提供的一个对象里 。这就是实例对象,由VkInstance对象来表示 。为了构建这个对象,我们会调用第一个Vulkan函数vkCreateInstance(),其原型如下 。
VkResult vkCreateInstance ( const VkInstanceCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkInstance* pInstance);该声明是个典型的Vulkan函数:把多个参数传入Vulkan,函数通常接收结构体的指针 。这里,pCreateInfo是指向结构体VkInstanceCreateInfo的实例的指针 。这个结构体包含了用来描述新的Vulkan实例的参数,其定义如下 。
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;几乎每一个用于向API传递参数的Vulkan结构体的第一个成员都是字段sType,该字段告诉Vulkan这个结构体的类型是什么 。核心API以及任何扩展里的每个结构体都有一个指定的结构体标签 。通过检查这个标签,Vulkan工具、层和驱动可以确定结构体的类型,用于验证以及在扩展里使用 。另外,字段pNext允许将一个相连的结构体链表传入函数 。这样在一个扩展中,允许对参数集进行扩展,而不用将整个核心结构体替换掉 。因为这里使用了核心的实例创建结构体,将字段sType设置为
VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,并且将pNext设置为nullptr 。
字段flags留待将来使用,应该设置为0 。下一个字段pApplicationInfo是个可选的指针,指向另一个描述应用程序的结构体 。可以将它设置为nullptr,但是推荐填充为有用的信息 。pApplicationInfo指向结构体VkApplicationInfo的一个实例,其定义如下 。
typedef struct VkApplicationInfo { VkStructureType sType; const void* pNext; const char* pApplicationName; uint32_t applicationVersion; const char* pEngineName; uint32_t engineVersion; uint32_t apiVersion; } VkApplicationInfo;我们再一次看到了字段sType和pNext 。SType 应该设置为
VK_STRUCTURE_TYPE_APPLICATION_INFO,并且可以将pNext设置为nullptr 。pApplicationName是个指针,指向以nul为结尾的字符串[1],这个字符串用于包含应用程序的名字 。applicationVersion是应用程序的版本号 。这样就允许工具和驱动决定如何对待应用程序,而不用猜测[2]哪个应用程序正在运行 。同样,pEngineName与engineVersion也分别包含了引擎或者中间件(应用程序基于此构建)的名字和版本号 。
【vulkan是什么意思】最后,apiVersion包含了应用程序期望运行的Vulkan API的版本号 。这个应该设置为你期望应用程序运行所需的Vulkan的绝对最小版本号——并不是你安装的头文件中的版本号 。这样允许更多设备和平台运行应用程序,即使并不能更新它们的Vulkan实现 。
回到结构体VkInstanceCreateInfo,接下来是字段enabledLayerCount和ppEnabledLayerNames 。这两个分别是你想激活的实例层的个数以及名字 。层用于拦截Vulkan的API调用,提供日志、性能分析、调试或者其他特性 。如果不需要层,只需要将enabledLayerCount设置为0,将ppEnabledLayerNames设置为nullptr 。同样,enabledExtensionCount是你想激活的扩展的个数[3],ppEnabledExtensionNames是名字列表 。如果我们不想使用任何的扩展,同样可以将这些字段分别设置为0和nullptr 。
最后,回到函数vkCreateInstance(),参数pAllocator是个指向主机内存分配器的指针,该分配器由应用程序提供,用于管理Vulkan系统使用的主机内存 。将这个参数设置为nullptr会导致Vulkan系统使用它内置的分配器 。在这里先这样设置 。应用程序托管的主机内存将会在第2章中讲解 。
如果函数vkCreateInstance()成功,会返回VK_SUCCESS,并且会将新实例的句柄放置在变量pInstance里 。句柄是用于引用对象的值 。Vulkan句柄总是64位宽,与主机系统的位数无关 。一旦有了Vulkan实例的句柄,就可以用它调用实例函数了 。
1.2.2 Vulkan物理设备
一旦有了实例,就可以查找系统里安装的与Vulkan兼容的设备 。Vulkan有两种设备:物理设备和逻辑设备 。物理设备通常是系统的一部分——显卡、加速器、数字信号处理器或者其他的组件 。系统里有固定数量的物理设备,每个物理设备都有自己的一组固定的功能 。
逻辑设备是物理设备的软件抽象,以应用程序指定的方式配置 。逻辑设备是应用程序花费大部分时间处理的对象 。但是在创建逻辑设备之前,必须查找连接的物理设备 。需要调用函数
vkEnumeratePhysicalDevices(),其原型如下 。
VkResult vkEnumeratePhysicalDevices ( VkInstance instance, uint32_t* pPhysicalDeviceCount, VkPhysicalDevice* pPhysicalDevices);函数
vkEnumeratePhysicalDevices()的第一个参数instance是之前创建的实例 。下一个参数pPhysicalDeviceCount是一个指向无符号整型变量的指针,同时作为输入和输出 。作为输出,Vulkan将系统里的物理设备数量写入该指针变量 。作为输入,它会初始化为应用程序能够处理的设备的最大数量 。参数pPhysicalDevices是个指向VkPhysicalDevice句柄数组的指针 。
如果你只想知道系统里有多少个设备,将pPhysicalDevices设置为nullptr,这样Vulkan将忽视pPhysicalDeviceCount的初始值,将它重写为支持的设备的数量 。可以调用vkEnumerate PhysicalDevices()两次,动态调整VkPhysicalDevice数组的大小:第一次仅将pPhysicalDevices设置为nullptr(尽管pPhysicalDeviceCount仍然必须是个有效的指针),第二次将pPhysicalDevices设置为一个数组(数组的大小已经调整为第一次调用返回的物理设备数量) 。
推荐阅读
- 试述什么是灰饼和标筋及其作用 灰饼和标筋及其作用是什么
- 参天大树下一句是什么
- 女性最适合怀孕的年龄是什么时候
- 馒头蒸出来硬邦邦的是什么原因 馒头蒸出来硬邦邦的怎么回事
- 3r平衡型是什么意思
- 维持原判是什么意思
- 等闲识得东风面下一句是什么
- 什么是经济学中的蝴蝶效应 经济学中的蝴蝶效应是什么
- 紫色玉石代表什么意思
- 万豪集团董事长刘轩豪是什么电视