本文翻译自Vulkan-Loader的LoaderInterfaceArchitecture.md
目录
概要
Vulkan是一个分层的架构,由以下部分组成:
- Vulkan应用程序(Vulkan Application)
- Vulkan加载器(Vulkan Loader)
- Vulkan层(Vulkan Layers)
- 驱动(Drivers)
- 配置(VkConfig)
它们之间的关系如下图所示:
加载器 Loader
Vulkan应用程序(application)位于最上层,直接与加载器(loader)交互,而Vulkan驱动(driver)位于底层。驱动能够控制一个或多个能够使用Vulkan进行渲染的物理设备(通常理解为显卡GPU,不过CPU也是可以实现软渲染的),实现从Vulkan转换为本机图形API(例如MoltenVK)。
在应用程序与驱动之间,加载器可以注入任意数量的可选的层,以提供特殊功能。加载器对于将Vulkan函数正确分配到合适的一组层和驱动程序的管理至关重要。Vulkan对象模型允许加载器在调用链中插入层,这样一来这些层就能够在驱动被调用之前处理Vulkan函数了。
加载器的作用:
- 在一个用户系统上,支持一个或多个Vulkan驱动同时存在而不相互影响;
- 支持Vulkan层,这些层是一些可选的功能模块(由应用程序、开发人员或标准系统设置来使能);
- 降低开销。
层 Layers
层(layers)是一些可选的模块,可以用来增强Vulkan的开发环境。层可以在Vulkan函数从应用程序传递分配到驱动这一路的中间对其进行拦截、评估、以及修改。层是以动态库/静态库的形式实现,在CreateInstance的过程中被加载。
每一层都可以选择挂钩(hook)或拦截(intercept)Vulkan函数,对应的Vulkan功能可以被忽略,检查或增强。只要是没有被层hook的函数就会被该层略过,控制流会继续向前执行到下一个支持的层或者驱动,这样一来,层就可以选择是拦截所有已知的Vulkan函数还是仅拦截它感兴趣的那一部分。
部分层的作用:
- 验证API的使用/用法;
- 跟踪API的调用;
- 辅助调试;
- 分析。
由于层是可选且动态加载的,开发人员可以随时对其使能或禁用。例如:当开发调试一个应用程序时,使能特定的层能够帮助确认该应用程序是否正确地使用了Vulkan API;而release应用程序时,这些层就没必要使能了,禁用掉可以加快应用程序的执行速度。
驱动 Drivers
驱动是一组实现了Vulkan功能的库,这组库能够直接支持物理硬件设备、将Vulkan命令转换成本地图形命令、以及通过软件模拟Vulkan。最常见的驱动类型是可安装客户端驱动(Installable Client Driver, ICD)。加载器负责发现系统上可用的Vulkan驱动。只要给定可用驱动程序的列表,加载器就可以枚举所有可用的物理设备,并将这些信息提供给应用程序。
Vulkan允许系统上有多个可安装客户端驱动(Installable Client Driver, ICD),每一个ICD都支持一个或多个硬件设备。每一个设备由Vulkan中的VkPhysicalDevice
对象来表示。加载器能够通过搜索系统上的标准驱动来发现可用的Vulkan ICD。
配置 VkConfig
VkConfig是由LunarG开发的一套工具,用于辅助开发人员修改本地系统上的Vulkan开发环境。它可以发现层、使能层、修改层的设置等。只要安装Vulkan SDK或者编译LunarG VulkanTools Github Repo的源码就可以拥有VkConfig了~
VkConfig会产生3个输出,其中2个与Vulkan加载器和层相关,这3个输出分别是:
- Vulkan Override Layer:是VkConfig的重要部分,当加载器找到这个层时,这个层会强制加载在VkConfig中启用的所需层,并禁用那些在设置中禁用的层(包括隐式层);
- Vulkan层设置文件:可用于指定每个启用层期望执行的某些行为和动作。这些设置也可以由VkConfig控制,或者可以手动启用;
- VkConfig配置。
这些文件在系统中的存放位置如下表所示:
系统平台 | 输出 | 存放位置 |
---|---|---|
Linux | Vulkan Override Layer | $USER/.local/share/vulkan/implicit_layer.d/VkLayer_override.json |
Vulkan层设置文件 | $USER/.local/share/vulkan/settings.d/vk_layer_settings.txt | |
VkConfig配置 | $USER/.local/share/vulkan/settings.d/vk_layer_settings.txt | |
Windows | Vulkan Override Layer | %HOME%\AppData\Local\LunarG\vkconfig\override\VkLayerOverride.json |
Vulkan层设置文件 | (registry) HKEY_CURRENT_USER\Software\Khronos\Vulkan\Settings | |
VkConfig配置 | (registry) HKEY_CURRENT_USER\Software\LunarG\vkconfig |
Vulkan的重要概念
Instance vs. Device
Vulkan中的对象(objects)、函数(functions)、扩展(extensions)和其它行为能够划分为两个部分:
- 基于实例(Instance)的;
- 基于设备(Device)的。
基于实例(Instance)的
“Vulkan instance”(VkInstance
)是一个高级结构,用于提供Vulkan系统级信息和功能。
Instance对象
与Instance直接关联的Vulkan对象如下:
VkInstance
;VkPhysicalDevice
;VkPhysicalDeviceGroup
。
Instance函数
Instance函数是指以Instance对象作为第一个参数或者没有对象的任意Vulkan函数,部分Instance函数如下:
vkEnumerateInstanceExtensionProperties
;vkEnumeratePhysicalDevices
;vkCreateInstance
;vkDestroyInstance
;
应用程序能够通过Vulkan加载器的头文件直接连接到所有核心Instance函数。应用程序也可以通过vkGetInstanceProcAddr
来查询函数指针,该函数可以查询任意Instance或Device的入口点。
如果vkGetInstanceProcAddr
函数是通过VkInstance
调用的,那么该函数返回的任何函数指针都是基于这个VkInstance
的。
Instance扩展
Vulkan有哪些扩展是基于Vulkan提供了哪些类型函数。扩展可以分为Instance扩展和Device扩展,Instance扩展里面大部分是Instance相关类型的函数,Device扩展里面大部分是Device相关类型的函数。
基于设备(Device)的
Vulkan设备(VkDevice
)是一个逻辑标识符,这个逻辑标识符通过操作系统中特定的驱动与Vulkan物理设备(VkPhysicalDevice
)关联。
Deivce对象
与Device直接关联的Vulkan对象如下:
VkDevice
;VkQueue
;VkCommandBuffer
。
Device函数
Device函数是指任意以Device对象或Device的子对象为第一个参数的Vulkan函数,大多数Vulkan函数都是Device函数。部分Deivce函数如下:
vkQueueSubmit
;vkBeginCommandBuffer
;vkCreateEvent
。
Device函数可以使用vkGetInstanceProcAddr
或者vkGetDeviceProcAddr
来查询:①如果在应用程序中选择使用vkGetInstanceProcAddr
,那么在每次调用该函数时会额外调用内置在调用链(call chain)中的函数,这样会稍微降低性能。②如果应用程序选择使用vkGetDeviceProcAddr
,那么调用链会基于特定设备进行优化,但返回的就是仅用于该特定设备的函数指针了。③与vkGetInstanceProcAddr
既能查询Instance函数又能查询Device函数不同,vkGetDeviceProcAddr
只能用于查询Device函数。
总而言之,最佳方案就是使用vkGetInstanceProcAddr
查询Instance函数,使用vkGetDeviceProcAddr
查询Device函数。
Device扩展
和Instance扩展一样,Device扩展是一组Vulkan设备相关的函数。
Dispatch Tables和Call Chains
Vulkan使用对象模型(object model)来控制行为或操作的作用范围。被作用的对象总是Vulkan函数的第一个参数,而且这个对象是可分派的(参考Vulkan Specification Section 3.3 Object Model)。可分派对象的句柄是一个指向结构的指针,该结构包含了指向加载器维护的分派表(一组函数指针)的指针,分派表包含指向相应对象的Vulkan函数的指针。
加载器维护了两类分派表:
- Instance分派表:加载器在调用
vkCreateInstance
期间创建的; - Device分派表:加载器在调用
vkCreateDevice
期间创建的。
应用程序和系统可以各自指定要包含的可选层。然后,由加载器初始化指定的层,为每个Vulkan函数创建一个调用链,分派表的每个条目(入口)将指向该链的第一个元素。这样一来,加载器就为其创建的每个VkInstance
建立了一个Instance调用链,为其创建的每个VkDevice
建立了一个Device调用链。
当应用程序调用Vulkan函数时,在加载器中首先会命中trampoline
函数,这些trampoline
函数非常短小简单,主要用于跳转到指定对象的合适的分派表入口。此外,加载器在调用链中还有一类函数叫做terminator
,terminator
函数会在加载器使能了指定层之后被调用,主要用于将信息传递给驱动。
Instance调用链示例
下图描述了应用程序调用vkCreateInstance
函数之后调用链中发生的事情。在初始化调用链之后,加载器会调用第一层的vkCreateInstance
,接着调用下一层的vkCreateInstance
,一直到最后一个层,最后再调用每个驱动程序的vkCreateInstance
。这样一来,调用链中的每个启用了的层都能够根据应用程序中的VkInstanceCreateInfo
结构设置所需的内容。
这样同样也突出了当使用Instance调用链时加载器必须解决的某些复杂问题。如上图所示,加载器的terminator
需要从多个驱动中收集信息,这意味着加载器必须知道作用于VkInstance
上的任意实例级的扩展,以正确地收集它们的信息。
Device调用链示例
Device调用链创建于vkCreateDevice
函数中,但通常要比Instance调用链简单,因为它只作用于一个Device,使得暴露Device的特定驱动始终是调用链的terninator
。
权限提升下的注意事项
提升权限:在计算机系统中,提升权限是指用户或程序获得比其正常权限更高的访问权限,以执行特定任务或访问特定资源。
为了在开发过程中保证系统的安全,需要对运行在权限提升条件下的Vulkan应用程序的某些操作进行限制,例如:从不安全的位置读取环境变量和在用户控制的路径中搜索文件。这样一来,就可以保证以提升权限运行的Vulkan应用程序就不会使用未安装在适当位置的组件。
Vulkan Loader会使用特定平台的机制(例如secure_getenv
以及类似方式)来查询敏感的环境变量以避免使用不受信任的结果。
在特权条件下会忽略一下环境变量:
VK_DRIVER_FILES
/VK_ICD_FILENAMES
VK_ADD_DRIVER_FILES
VK_LAYER_PATH
VK_ADD_LAYER_PATH
XDG_CONFIG_HOME
(Linux/Mac-specific)XDG_DATA_HOME
(Linux/Mac-specific)