目录
一、Loader
The Loader
二、Layer
调度链Dispatch Chains
JSON
一、Loader
Vulkan是一个层架构,由Vulkan Application+Loader+Layer+ICDs(Installable Client Drivers)组成。
Vulkan 是一个显式 API,可以直接控制 GPU 的实际工作方式。因此,Vulkan 支持具有多个 GPU 的系统,每个 GPU 运行不同的驱动程序或 ICD(可安装客户端驱动程序)。 Vulkan 还支持多个全局上下文(Vulkan 术语中的实例)。 ICD 加载器是一个放置在 Vulkan 应用程序和任意数量的 Vulkan 驱动程序之间的库,以支持多个驱动程序以及跨这些驱动程序工作的实例级功能。此外,加载程序还管理在应用程序和驱动程序之间插入 Vulkan 层库,例如验证层。
The Loader
application在一端,直接和loader打交道。loader另一端是ICDs,在application和ICDs之间,loader可以插入一系列可选的layers。loader负责和各个layer交互并且支持多GPUs和其驱动。任何一个vulkan api函数可以经过loader,layers和ICDs,loader负责将api传递dispatch给合适的layers和ICDs,vulkan对象模型允许loader插入layers层,并且组成调用链上一环,并最终传递给vulkan api给ICD。
loader的职责简单总结如下:
- 支持1个ICD或者多个ICDs,且保证ICD间不相互干扰,ICD是就是不同厂商对图形API所做的驱动文件。
- 支持Vulkan Layer,Layer是可选的
- 已最可能低的方式影响vulkan应用性能
恰巧vulkan的一个思想就是,去除掉验证层,那么验证层能放在何处?也就是放在这个SDK里,类似各种Graphic Debugger的实现,Vulkan有很多Validation Layer,可以根据需求载入,在真正的函数调用之前,截取一些信息,获得一些信息,最后再调用真正的接口。也就是说整个SDK不仅仅是一个函数指针获取器,还是一个Graphic Debugger。
二、Layer
layers是可选组件,可以增强vulkan系统,可以拦截,修改vulkan api,layers是作为lib库实现,可以通过不同方式使能并且在CreateInstance中被加载。每个layer可以选择任何vulkan api进行拦截,一个layer不需要拦截所有vulkan api function,layer可以选择取拦截所有已知vulkan api,也可以拦截一条vulkan api。
layer的一些示例如下:
- 校验api使用
- 增加debug和trace等调试信息
- 覆盖额外内容
因为layer是可选的,我们可以在调试阶段使能,在release时关闭。
vulkan中很多扩展和函数(api)被分成两个主要组,一个是实例instance相关对象,另一个是device相关对象。
vulkan instance是一个high-level系统级信息或者函数,vulkan对象如VkInstance和VkPhysicalDevice,vulkan函数如vkEnumerateInstanceExtensionProperties、vkEnumeratePhysicalDevices、vkCreateInstance、vkDestroyInstance等。可以使用vkGetInstanceProcAddr查询vulkan instance function,vkGetXXXProcAddr可以查询device或者instance入口点,返回的函数指针对实例或者基于实例创建的对象有效,包括vkDevice对象,同理,instance扩展是一系列vulkan实例函数。
vulkan device是一个逻辑标识,别用于在特定physical device关联的api,device object有vkDevice,vkQueue,vkCommandBuffer,任何是前面三个object孩子的dispatchable object。device function一般是将device object作为第一个参数的api,比如vkQueueSubmit、vkBeginComandBuffer、vkCreateEvent等。
可以使用vkGetInstanceProcAddr和vkGetDeviceProcAddr获取device function。
调度链Dispatch Chains
此时,我们需要讨论单个函数调用(如vkCreateInstance
)如何传播到加载器、ICD 或 ICD 以及许多不同的层。
当应用程序调用加载器静态导出的任何函数时,它会调用trampoline function。然后,这个trampoline function调用调度链Dispatch Chains。调度链的思想来自函数指针链。它从加载器的trampoline function入口点开始,然后trampoline function调用第一层,然后第一层调用第二层。该链一直持续到最终到达 ICD 中的端点以真正完成工作。
更明确地说,vkCreateInstance
就是这些特殊功能之一。在调度链开头的trampoline function中,它首先验证请求的层和扩展是否有效。一旦满足,它就会分配 Vulkan 实例和调度链,然后调用vkCreateInstance
第一层的函数。第一层将初始化自身和任何内部结构,然后将执行传递到vkCreateInstance
下一层,依此类推。
现在,从应用程序的角度来看,Vulkan 实例主要是一个加载器概念。它们代表了 Vulkan 使用的所有内容,但它们也将所有可用的 ICD 整合到一起。这意味着当我们到达vkCreateInstance
的调度链末端时,我们不会以 ICD 结束。可能有多个同时使用,并且 ICD 不知道彼此是否链接在一起。
加载器通过将自己的终止符函数放在调度链的末尾terminator function on the end of the dispatch chain,供最后一层调用来解决这个问题。然后,该terminator function 依次调用vkCreateInstance
每个可用的 ICD 并存储所有这些 ICD 以供以后使用:
当我们进行时,各层正在初始化自己并为它们在调度链中的位置做好准备。特别是,每一层都用vkGetInstanceProcAddr
查找它希望能够在下一层中调用的所有入口点。每层调用下一层的vkGetInstanceProcAddr
并将它们存储在调度表中。这只是一个充满函数指针的结构。当加载程序调用链中第一层的入口点时,它已经知道要传递到下一层的位置。这还实现了一个有用的功能 - 层不必挂钩每个Vulkan 函数,只需挂钩它们感兴趣的函数即可。
当加载器和每一层调用vkGetInstanceProcAddr
以查找调度链中的下一个函数时,就会发生这种情况。如果某个层不想拦截函数调用,则它不必返回自己的little stub function。相反,t can just forward the call to the next layer and return the result.。只要每个点的调度表都知道下一个要调用的函数,它们是否跳过一两层并不重要。
JSON
加载器将使用它来寻找入口点vkGetInstanceProcAddr/
vkGetDeviceProcAddr,
用于构建调度链。这些是模块需要导出的唯一入口点。由于必须导出与 API 函数名称完全相同的函数可能会很尴尬或不方便,因此您可以改为导出
SampleLayer_GetInstanceProcAddr
SampleLayer_GetDeviceProcAddr
。
{
"file_format_version" : "1.0.0",
"layer" : {
"name": "VK_LAYER_SAMPLE_SampleLayer",
"type": "GLOBAL",
"library_path": ".\\sample_layer.dll",
"api_version": "1.0.0",
"implementation_version": "1",
"description": "Sample layer - https://renderdoc.org/vulkan-layer-guide.html",
"functions": {
"vkGetInstanceProcAddr": "SampleLayer_GetInstanceProcAddr",
"vkGetDeviceProcAddr": "SampleLayer_GetDeviceProcAddr"
},
}
}
针对导出的每个函数按顺序strcmp
参数pName
,然后返回入口点的地址。
应用程序调用其中之一时,它们会调用loader而不是直接调用 Vulkan 驱动程序。
vkGet**ProcAddr:该命令可获取所有Vulkan命令的函数指针
Hook这一层的函数:在vkGet**ProcAddr中返回本地函数指针
调用下一层实体:每层调用下一层的vkGet**ProcAddr,并将其存储在调度表中
loader_platform_open_library(const char * libPath)
loader_scanned_icd_add(...)
loader_icd_scan(...)
vkCreateInstance(...)
main()
由于应用程序与任一调度链都无关,因此真正的区别在于,在可能的情况下,vkGetDeviceProcAddr
直接返回指向调度链中第一个条目的函数指针 - 如果不存在任何层,则通常将函数指针直接指向 ICD。vkGetInstanceProcAddr
可能会返回与加载程序导出的相同的函数,该函数从可分派句柄中获取分派表并跳转到它。