本文翻译自Vulkan-Loader的LoaderApplicationInterface.md。
目录
概要
本文以使用Vulkan Loader的应用程序为中心视角展开,想要了解关于Vulkan Loader的完整概要信息请参阅Vulkan Loader接口的架构这篇文章。
与Vulkan函数的对接
Vulkan函数能够通过以下几种方式与Vulkan Loader对接:
Vulkan直接导出
不管是Windows、Linux、Android,还是macOS,Vulkan Loader库均导出了所有核心Vulkan入口点(entry-points)以及所有所有窗口系统接口(Window System Interface,WSI)入口点。这样一来,Vulkan的开发就会变得简单一些。当应用程序直接链接到Vulkan Loader库时,调用的是简单的trampoline
函数,这个函数会跳转到给定对象对应的分派表项。
直接链接到Vulkan Loader
动态链接
Vulkan Loader以动态库的形式发布(Windows系统中为.dll文件,Linux系统中是.so文件,macOS系统中为.dylib文件),并安装在动态库的系统路径中。在Windows系统中,动态库通常作为驱动程序安装的一部分安装,而在Linux系统中一般通过系统包管理器提供。如果应用程序想要确定当前系统上是否存在Vulkan Loader,可以包含一下这个Vulkan Loader或者runtime installer。
静态链接
早期的Vulkan Loader版本是可以被静态链接的,但现在已经不可以了。不再支持静态链接的原因是:一旦驱动程序有更改,这些使用静态链接的应用程序就无法找到新的驱动程序。
此外,静态链接还有以下几个问题:
- 除非应用程序进行重新链接,否则Vulkan Loader更新了也用不了;
- 如果包含两个不同版本的Vulkan Loader可能会导致版本之间的冲突。
间接链接到Vulkan Loader
应用程序不一定要直接链接到Vulkan Loader,也可以通过动态符号查找Vulkan Loader库来初始化自己的分派表(dispatch table)。但是,一旦找不到Vulkan Loader,应用程序就会报错。
此外,应用程序还有更快的机制来调用Vulkan函数,即(通过系统调用,例如dlsym)查询Vulkan Loader库中vkGetInstanceProcAddr
函数的地址,然后应用程序再使用vkGetInstanceProcAddr
函数来加载所有可用的函数,例如vkCreateInstance
,vkEnumerateInstanceExtensionProperties
和vkEnumerateInstanceLayerProperties
等。
最佳应用程序性能设置
为了使Vulkan应用程序获得最佳性能,应用程序应该为每一个Vulkan API入口点设置自己的分派表。对于分派表中的每一个实例级(instance-level)的Vulkan命令,应该用vkGetInstanceProcAddr
函数来查询和填充函数指针;对于每一个设备级(device-level)的Vulkan命令,应该用vkGetDeviceProcAddr
来查询和填充函数指针。
为什么要这样做呢?主要在于Instance函数的调用链与Device函数的调用链是如何实现的。
Vulkan Instance是用于提供Vulkan系统级信息的高级结构。因此,Instance函数需要“广播”到系统上的每个可用驱动程序。下图展示了带有3个使能层(Layers)的Instance调用链流程:
上图同样也是Vulkan中Device函数调用链的流程图(如果查询方式是使用vkGetInstanceProcAddr
函数的话)。
其实,Device函数不需要考虑“广播”问题,因为它们知道自己应该停在哪一个驱动、哪一个物理设备。这样一来,Vulkan Loader也不需要介入任何使能层和驱动程序之间。使用Vulkan Loader导出的Vulkan Device函数的调用链是这样的:
对应用程序来说,还有一个更优的解决方案,就是使用vkGetDeviceProcAddr
函数查询所有的Device函数,其调用链可以进一步优化成下面这样(彻底将Vulkan Loader从调用链中移除):
还有,如果调用链中没有层被使能,应用程序的函数指针是直接指向驱动的,每减少一层间接的调用就会带来显著的性能提升。
注意:Device函数中仍有一少部分需要使用Vulkan Loader的trampoline
和terminator
去拦截,这些函数的典型特征就是Vulkan Loader用自己的数据将其包装了一遍。这种情况下,Device调用链会与Instance调用链一样。例如vkCreateSwapchainKHR
函数,这个函数就需要Vulkan Loader的terminator
,因为对于vkCreateSwapchainKHR
函数,Vulkan Loader需要在将函数的信息传递给驱动程序之前,把KHR_surface对象转换为特定驱动的KHR_surface对象。
总而言是,请记住:
vkGetInstanceProcAddr
:是用来查询Instacne函数和物理设备函数的,但是也可用来查询所有函数;vkGetDeviceProcAddr
:仅用于查询Device函数。
ABI Versioning
Vulkan Loader库可以以各种形式发布,包括Vulkan SDK,操作系统软件包,以及独立硬件供应商(Independent Hardware Vendor,IHV)驱动程序包。具体的细节已经超过了本文的介绍范围,但是Vulkan Loader库的名字和版本是特定的,这样应用程序才能链接到正确的Vulkan应用程序二进制接口(Application Binary Interface,ABI)库版本,具有相同主版本号的所有版本(例如1.0和1.1)都保证了ABI向后兼容性。
Windows下动态库的用法
在Windows系统上,Vulkan Loader库在库名中加上ABI版本,使得多个不兼容的ABI版本可以在操作系统上共存,命名的形式为vulkan-<ABI version>.dll
。例如Vulkan1.X版本在Windows操作系统上的库文件名就是vulkan-1.dll
。这个库文件位于windows\system32
目录下(64位的库版本),32位的Vulkan Loader库版本以相同的名字存放在windows\sysWOW64
目录下,这里不要弄混了!
Linux下动态库的用法
对于Linux系统,共享库是基于后缀来确定版本的。因此,ABI版本号不是和Windows系统上一样加在了库名中。
在Linux上,依赖Vulkan的应用程序应该链接到的是一个没有版本号的、库名为libvulkan.so
的动态库(但是其实这个库文件是一个链接文件,它最终链接到的是一个后缀中包含版本号的库文件)。例如,通过导入CMake目标Vulkan::Vulkan
或使用pkg-config --cflags --libs vulkan
的输出作为编译器标志,然后编译器和链接器会把它解析为对应正确版本的“库的名称的别名”(SONAME),目前的名字是libvulkan.so.1
。但是,如果是在运行时动态加载Vulkan Loader,Linux应用程序不能只传递“libvulkan.so
”,而是应该确保传递包含版本的名称,例如传递libvulkan.so.1
给dlopen()
,这样才能够确保加载到兼容的版本。
MacOS下动态库的用法
MacOS下的链接与Linux下的链接方式相似,对应的标准动态库的名字为libvulkan.dylib
,带有版本号的ABI库名为libvulkan.1.dylib
。
绑定Vulkan Loader与应用程序
Vulkan Loader库的安装典型地要么是通过基于操作系统平台的方式安装(例如Linux系统的软件包package),要么作为驱动的一部分进行安装(例如Windows系统上使用Vulkan Runtime installer)。应用程序或引擎可能希望将Vulkan Loader本地安装到它们的执行树中,作为它们自己的安装过程的一部分(即打包在一起),这是因为提供了特定的Vulkan Loader:
- 保证某些Vulkan API导出在Vulkan Loader中是可用的;
- 确保某些Vulkan Loader的行为是周知的;
- 在用户安装之间提供一致性。
然而,我们非常不建议这样做,因为:
- 打包后的Vulkan Loader可能与将来驱动程序的修订版不兼容 (尤其是在Windows系统上,当操作系统升级更新后驱动的安装位置会变);
- 它不利于应用程序/引擎使用新的Vulkan API版本/扩展的导出;
- 应用程序/引擎可能会错过重要的bug修复;
- 打包后的Vulkan Loader不会包含有用的功能更新(例如:改进了Vulkan Loader的可调试性等)。
当然,即使一个应用程序或引擎在最初发布的时候确实附带了特定版本的Vulkan Loader,它也可以选择在未来的某个时刻升级或移除这个Vulkan Loader,因为Vulkan Loader中所需要的功能会随着时间的推移而显露,不过这取决于最终用户是否正确执行更新程序。
在Windows上有一个更好的选择是:为产品提供所需版本的Vulkan Loader打包Vulkan Runtime installer。 然后,安装过程可以使用Vulkan Runtime installer来确保最终用户的系统是最新的,Runtime installer将检测已安装的版本,并且只在有必要的情况下安装新的runtime。
另一种替代方法是编写应用程序,让它可以回退到更早的版本,但会显示警告,指示哪些功能已禁用,直到用户将其系统更新到特定的runtime或驱动程序。
应用程序对于层的使用
应用程序想要的功能超过了当前系统上Vulkan驱动能够提供的功能怎么办呢?可以选择使用各种各样的层(Layers)来增强Vulkan API。无法添加新Vulkan核心API 的入口点的层是不会在Vulkan.h中暴露的,但是这些层可以提供扩展的实现,这个扩展的实现会引入额外的入口点,这些额外的扩展入口点能够通过Vulkan扩展接口来查询。
层的常用功能是验证API,只需在应用程序开发过程中使能就行,最后在应用程序发布前再禁用。这样就能够简单地控制由于应用程序使能API的验证而产生的开销,不过这在早期的图形API中不一定能实现。
对于一个应用程序来说,哪些层是可用的呢?Vulkan Loader会在系统的各个位置寻找层,并且报告它找到的所有层。
要启用特定的层,只需在调用期间在参数中传递要使能的层的名字就行。一旦使能,这些层对于所有Vulkan函数及其子对象都是活跃状态。
注意:当某些层会与其它层交互时,层的顺序会变得很重要,在应用程序中使能层的时候就需要特别注意,详见下文中的“层总体排序”。
下面的代码展示了怎样使能验证层VK_LAYER_KHRONOS_validation
:
char *instance_layers[] = {
"VK_LAYER_KHRONOS_validation"
};
const VkApplicationInfo app = {
.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,
.pNext = NULL,
.pApplicationName = "TEST_APP",
.applicationVersion = 0,
.pEngineName = "TEST_ENGINE",
.engineVersion = 0,
.apiVersion = VK_API_VERSION_1_0,
};
VkInstanceCreateInfo inst_info = {
.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
.pNext = NULL,
.pApplicationInfo = &app,
.enabledLayerCount = 1,
.ppEnabledLayerNames = (const char *const *)instance_layers,
.enabledExtensionCount = 0,
.ppEnabledExtensionNames = NULL,
};
err = vkCreateInstance(&inst_info, NULL, &demo->inst);
if (VK_ERROR_LAYER_NOT_PRESENT == err) {
// Couldn't find the validation layer
}
然后,Vulkan Loader会构造包含了应用程序指定使能层的调用链。在包含层名字的数组char *instance_layers[]
中顺序是非常重要的,数组的第0个元素在调用链中是最顶层(即最接近应用程序的层),数组的最后一个元素是最接近驱动的层。
注意:设备层(Device Layers)现在已经不可用了。
vkCreateDevice
原本也是可以指定使能哪些层的,从而引出了“instance layers”和“device layers”的概念。但是Khronos决定抛弃“device layers”,仅保留“instance layers”。因此,以下项就已经是过去式了:VkDeviceCreateInfo
中的ppEnabledLayerNames
和enabledLayerCount
,以及函数vkEnumerateDeviceLayerProperties
。
Meta-Layer
Meta-Layer包含了一系列有顺序的层,它可以将层按照指定的顺序组合,以便层与层之间能够正常交互。原来,Meta-layer只是用来将独立的Vulkan验证层以正确的顺序组合起来避免冲突。Meta-layer是非常有必要的,因为验证层不是单一的层,而是被拆分成了多个组合层。这个新的VK_LAYER_KHRONOS_validation
将所有的东西装进了一个层里面,从而可以不需要Meta-layer。尽管Meta-layer不再被验证层需要,但是VkConfig能够按照用户的喜好使用Meta-layer来组合其它层。想要了解更多可以参阅下文“Override Layer”部分。
隐式层 vs. 显式层
显式层:是指由应用程序使能的层(例如:由应用程序调用vkCreateInstance
函数使能的层)。
隐式层:是指自动使能的层(除非必须额外的手动使能步骤),而显式层则必须显式使能。例如,某些应用程序环境(例如Steam)可能有其希望在应用程序启动后始终保持使能状态的层。
相比显式层,隐式层有额外的条件,那就是隐式层必须要能够通过环境变量禁止,这是因为隐式层对应用程序是不可见的,而且还可能会导致一些问题。因此,应该在环境中同时定义enable和disable两个环境变量,以便用户能够决定是否使能这个层。在带桌面的操作系统上(Windows、Linux和macOS),这些enable/disable的设置定义在层的JSON文件中。
对于不同的操作系统,隐式层和显式层位于不同的位置,如下表所示:
操作系统 | 隐式层位置 |
---|---|
Windows | 隐式层位于与显式层不同的Windows注册表位置。 |
Linux | 隐式层位于与显式层不同的目录位置。 |
Android | 安卓系统不支持隐式层 |
macOS | 隐式层位于与显式层不同的目录位置。 |
Override Layer
Override layer是由VkConfig工具创建的特殊的隐式Meta-layer,并且当VkConfig工具运行时该层是默认可用的。一旦VkConfig退出,Override layer就被移除,系统应该恢复到标准的Vulkan行为。只要Override layer出现在层搜索路径中,Vulkan Loader就会将其与标准隐式层以及要加载的层列表中包含的所有层一起拉入层调用堆栈。这样,用户或开发人员就能通过VkConfig轻松地设置任意数量的层。
强制层源文件夹
开发人员可能想要在不修改系统原本安装的层的前提下,使用特殊的pre-production层。可以通过以下两种方式实现:
- 使用VkConfig工具选择特殊层的路径;
- 使用环境变量
VK_LAYER_PATH
来直接告诉Vulkan Loader去找指定的文件或文件夹。
环境变量可以包含多个路径,只要用操作系统指定的路径分隔符隔开就行(在Windows上是使用分号,而Linux和macOS上是使用冒号)。如果存在,文件或文件夹会被扫描成显式层清单文件。隐式层的搜索不受该环境变量的影响。列出的每个目录项为包含层清单文件的文件夹的完整路径名。
在Windows、Linux和macOS上强制使能层
如果开发人员想要使能自己正在使用的应用程序没有使能的层,那么可以通过以下两种方法完成:
- 使用VkConfig工具选择指定的层;
- 通过环境变量
VK_INSTANCE_LAYERS
来告诉Vulkan Loader直接利用层的名字搜索层。
以上两个方法都可以用来使能没有被应用程序调用vkCreateInstance
函数使能的层。
环境变量对应的是一组层名,这些层名由由操作系统指定的路径分隔符隔开(例如:Windows系统上的分隔符是分号,Linux和macOS系统上的分隔符是冒号)。关于层名的顺序中,第一个层名是最顶层(最接近应用程序)和最后一个层名是最底层(最接近驱动程序)。
应用程序指定的层和用户(通过环境变量)指定的层会被聚合,并且当Vulkan Loader使能层时会删除重复的层。通过环境变量指定的层是最顶层的(最靠近应用程序),而应用程序指定的层是最底层的。
在Linux或macOS上,通过环境变量来激活验证层VK_LAYER_KHRONOS_validation
的方法如下:
> $ export VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation
层的总体排序
Vulkan Loader基于上述内容对所有层的总体排序如下所示(离应用程序最近的是隐式层,接着是Override layer,然后是由环境变量使能的显式层,最后离驱动最近的是由应用程序使能的显式层):
在显式层的内部,层与层之间的顺序也可能会很重要,因为某些层会依赖Vulkan Loader调用其它层之前/后的一些行为。例如,一个overlay layer可能想要使用VK_LAYER_KHRONOS_validation
来验证overlay layer的行为是否正确。这需要将overlay layer放在离应用程序更近的地方,验证层才能可以拦截overlay layer所需的任何Vulkan API调用。
应用程序对于扩展的使用
扩展(extension)是由层、Vulkan Loader或者驱动提供的可选功能。扩展可以修改Vulkan API的行为,需要在Khronos中指定和注册。关于各种扩展的信息可以在Vulkan规范和Vulkan .h头文件中找到。
Instance和Deivce扩展
扩展可分为以下两类:
- Instance扩展:能够在实例级对象(例如:
VkInstance
和VkPhysicalDevice
)上修改其现有行为或实现新的行为; - Device扩展:够在设备级对象(例如:
VkDevice
、VkQueue
、VkCommandBuffer
及其子对象)上修改其现有行为或实现新的行为。
了解扩展属于哪一个类型非常重要,因为Instance扩展必须使用vkCreateInstance
来使能,而Device扩展必须使用vkCreateDevice
来使能。
当调用vkEnumerateInstanceExtensionProperties
和vkEnumerateDeviceExtensionProperties
函数时,Vulkan Loader会在将结果报告给应用程序之前,从层(包括显式和隐式)、驱动和Vulkan Loader自身里面搜索并汇总对应类型的所有扩展。
观察vulkan.h
文件,这两个函数是非常相似的,vkEnumerateInstanceExtensionProperties
的定义如下:
VkResult
vkEnumerateInstanceExtensionProperties(
const char *pLayerName,
uint32_t *pPropertyCount,
VkExtensionProperties *pProperties);
而vkEnumerateDeviceExtensionProperties
的定义如下:
VkResult
vkEnumerateDeviceExtensionProperties(
VkPhysicalDevice physicalDevice,
const char *pLayerName,
uint32_t *pPropertyCount,
VkExtensionProperties *pProperties);
其中,参数pLayerName
是用来指定一个层或者Vulkan平台实现。如果pLayerName
参数为NULL,则枚举Vulkan实现组件(包括Vulkan Loader、隐式、以及驱动)中的扩展。如果pLayerName
等于一个层模块名,那么仅枚举这个层(可以是显示,也可以是隐式)的扩展。
注意:尽管Device layer已经没有了(被Khronos决定抛弃),但是使能了的Instance layer依然存在于Device调用链中。
重复的扩展(例如,隐式层和驱动程序支持相同的扩展)会被Vulkan Loader消除:只报告驱动程序的扩展,而剔除层的扩展。
此外,在使用与扩展相关的功能之前,必须启用扩展(使用vkCreateInstance
或vkCreateDevice
函数)。如果使用vkGetInstanceProcAddr
或vkGetDeviceProcAddr
查询扩展函数,但这个时候扩展尚未使能,就可能导致未定义的行为,验证层会捕获这个无效的API使用。
WSI扩展
Kronos认可的WSI扩展能够为各种执行环境提供窗口系统集成(Windows System Integration)支持。某些WSI扩展是对所有目标均有效,但某些扩展仅支持特定的执行环境(和Vulkan Loader)。Khronos Vulkan Loader(目前针对Windows、Linux、macOS、Stadia和Fuchsia)仅使能并导出适合当前操作系统的WSI扩展。在大多数情况下,WSI扩展的选择是在Vulkan Loader编译时使用预处理器标志完成的。所有Khronos Vulkan Loader版本至少会支持以下WSI扩展:
- VK_KHR_surface;
- VK_KHR_swapchain;
- VK_KHR_display。
下表是Vulkan Loader支持的特定平台(操作系统)的扩展:
操作系统 | 可用的扩展 |
---|---|
Windows | VK_KHR_win32_surface |
Linux(Wayland) | VK_KHR_wayland_surface |
Linux(X11) | VK_KHR_xcb_surface 和 VK_KHR_xlib_surface |
macOS(MoltenVK) | VK_MVK_macos_surface |
QNX(Screen) | VK_QNX_screen_surface |
实际上,Vulkan Loader与扩展之间需要满足以下握手机制,才能在Vulkan应用程序正常使用WSI扩展:
- 至少需要有一个物理设备是支持这些扩展的;
- 应用程序必须使用该物理设备创建逻辑设备;
- 在创建Instance或者逻辑设备时,应用程序必须使能这些扩展(当然这也取决于给定的扩展程序是否与实例或设备一起使用)。
未知扩展
扩展(Extensions)具有轻松地扩展Vulkan的能力,而新创建的扩展可能对于Vulkan Loader来说是完全未知的。
如果某个扩展属于Device扩展,那么Vulkan Loader会将未知的入口点下传到device调用链(这个调用链是以对应的驱动程序入口点结尾的)。
如果某个扩展属于Instance扩展,也就是将物理设备参数作为其第一个组成,那么Vulkan Loader的执行流程与上面相似。但是,对于所有其它未知的Instance扩展,Vulkan Loader是无法加载的。
为什么Vulkan Loader不支持未知Instance扩展呢?让我们来回顾一下Instance调用链:
对于一个普通的Instance函数调用,Vulkan Loader需要将该函数调用传递至可用的驱动,如果Vulkan Loader不知道这个Instance函数的参数或返回值,那它就不能正确地把信息向下传递给驱动。因此,目前为止Vulkan Loader不支持Instance扩展。由于device调用链不需要经过Vulkan Loader的terminator
,因此对于Device扩展而言,这不是问题。
此外,由于物理设备是与驱动程序相关联的,所以Vulkan Loader可以使用通用terminator
指向一个驱动,这是因为这些扩展都直接终止在与之关联的驱动。
不过,“Instance不支持未知的扩展”这一现象问题不大,因为大部分的扩展功能仅影响物理设备或逻辑设备,而不会影响Instance。
过滤掉未知Instance扩展名
某些情况下,可能会存在驱动支持某Instance扩展而Vulkan Loader不支持的现象。这时,当应用程序调用vkEnumerateInstanceExtensionProperties
时,Vulkan Loader就会过滤掉这些未知Instance扩展名。此外,如果当应用程序执意使用这些扩展时,Vulkan Loader会在vkCreateInstance
期间发送一个错误,这样做是为了保护应用程序,以免它们无意中使用可能导致崩溃的功能。
另一方面,如果实在需要使能这些扩展,那么可以通过定义VK_LOADER_DISABLE_INST_EXT_FILTER
环境变量强制禁用掉过滤器,这样就可以禁用Vulkan Loader的Instance扩展名的过滤。
物理设备排序
在Vulkan Loader 1.3.204之前,Linux上的物理设备可能会以不一致的顺序返回。为了解决此问题,在将信息返回到任何使能的层之前,Vulkan Loader会对设备进行排序,排序方式如下所示:
- 根据设备类型排序(独显、集显、虚拟显卡、以及其它);
- 根据PCI信息进行排序(域,总线,设备和功能)。
这样就能够在每次运行中都保持物理设备顺序的一致性,除非硬件改变了。
环境变量VK_LOADER_DEVICE_SELECT
能够让用户指定特殊的设备,这个环境变量需要被设置为设备对应的十六进制的Vendor Id和Device Id(这些Id号是由VkPhysicalDeviceProperties
结构体中的vkGetPhysicalDeviceProperties
函数返回)。环境变量的设置如下所示:
set VK_LOADER_DEVICE_SELECT=0x10de:0x1f91
这样就能强制指定Vendor Id为"0x10de",Device Id为"0x1f91"的设备了。如果没有找到该设备,那么这个环境变量就会被忽略。
所有在Vulkan Loader中完成的设备选择工作都能够通过将环境变量VK_LOADER_DISABLE_SELECT
设置为一个非0值来禁用,一般用于调试意图(在调试过程中用于缩小Vulkan Loader设备选择机制相关的问题的范围)。