Vulkan加载器接口架构(2)

Application Interface to the Loader

在本节,我们将讨论应用程序如何和加载器交互协作,包括:

Interfacing with Vulkan Functions

你可以通过加载器用多种方式和Vulkan函数交互。

Vulkan Direct Exports

在Windows,Linux和Android上的加载器将导出所有的核心Vulkan和所有合适的 Window System Interface (WSI) 拓展。完成了这步会让Vulkan开发变得简单。当应用程序通过这种方式直接链接到加载器library,Vulkan调用只是简单的跳转函数, 跳转到它们所在的对象在转发表中合适的入口。

Directly Linking to the Loader
Dynamic Linking

加载器通常是以动态库的方式发布的(在Windows上是.dll,在Linux上是.so),安装在系统的动态库路径中。 链接到加载器通常推荐的方式是链接到动态库,这样做允许加载器可以更新版本来修复bug或提升性能。 还有,动态库通常被安装在Windows 系统中,作为驱动安装的一部分,在Linux系统上通常通过包管理系统提供。 这意味着应用程序一般可以获取到当前系统中加载器的一份copy。如果应用程序想要百分百保证加载器是存在的,程序可以自带一份加载器安装包。

Static Linking

加载器也可以被静态链接(这通常随着Windows SDK一起发布,名为VKstatic.1.lib)。链接静态加载器意味着用户不需要系统已经安装过Vulkan运行时环境,这也保证了你的应用程序将使用一个特定版本的加载器。 然而,这种方式有几个缺陷:

  • 若不重新连接程序则无法更新静态库
  • 这可能导致包含的两个库可能包含不同版本的加载器
    • 这可能导致不同版本加载器潜在的的冲突

因此,推荐用户使用链接到.dll或.so 的方式。

Indirectly Linking to the Loader

应用程序并不需要直接链接到加载器库,它们可以对加载器使用合适的平台特定的动态符号查找方式来初始化应用程序自己的转发表。 这允许应用程序在没有找到加载器时可以从容的退出。这也给应用程序提供了调用Vulkan函数最快的机制。 应用程序只要向加载器库查询 vkGetInstanceProcAddr 的地址(通过dlsym()这个系统调用)。应用程序可以使用 vkGetInstanceProcAddr 来找到所有函数的地址和可用的拓展的地址, 如 vkCreateInstance,vkEnumerateInstanceExtensionProperties 和vkEnumerateInstanceLayerProperties

Best Application Performance Setup

如果你希望得到最佳性能,你应该建立起自己的转发表,这样你所有的实例函数都可以通过 vkGetInstanceProcAddr 获取到,所有的设备函数可以使用vkGetDeviceProcAddr 获取到。

Why should you do this?

这个答案取决于实例函数调用链和设备函数调用链是如何被实现的。记住,一个Vulkan 实例是一个高级的数据结构,用来提供Vulkan系统级别的信息。 因此,实例函数需要传播到系统上所有可用的ICD。下图展示了启用三个layers是实例调用链的视图:

Instance Call Chain

若你使用vkGetInstanceProcAddr查询它的话,这也是Vulkan设备函数调用链看起来的样子。 在另外一方面,一个设备函数并不需要担心传播,因为它已经知道自己关联的ICD和和函数所应该最后被调用的物理设备了。 由此,加载器不需要和任何启用的layers与ICD相干。故,如果你使用一个加载器暴露出来的设备函数,如上描述情形的调用链将如下所示:

Loader Device Call Chain

一个更好的解决方案是在应用程序中使用vkGetDeviceProcAddr 来调用所有的设备函数。这将通过移出大多数情形下加载器的中无关紧要的部分来进行更多优化。

Application Device Call Chain

还有,注意如果没有启用任何layer,你的应用程序函数指针将直接指向ICD。如果调用次数足够多,将带来一些性能提升。

注意: 有一些设备函数要求加载器用一个 trampoline and terminator来拦截它们。虽然个数比较少,但是,它们是典型的需要加载器包装他们数据的函数。 在这些情形下,即使是设备函数调用链看起来也像是实例调用链。一个例子是vkCreateSwapchainKHR要求一个 terminator 。对于这个函数,在把函数剩下的信息传递给ICD之前, 加载器需要把 KHR_surface 对象转换为一个ICD特定的 KHR_surface。

记住:

  • vkGetInstanceProcAddr 可以用来查询设备或者实例入口函数(包括所有核心的入口函数)。
  • vkGetDeviceProcAddr 只可以用来查询设备拓展或者核心的设备入口函数。
ABI Versioning

Vulkan加载器库有各种分发方式,包括Vulkan SDK、操作系统包管理分发和Independent Hardware Vendor (IHV) 驱动安装包。 这些东西不在本文的关注点内。然而,Vulkan加载器库的名字和版本需要被明确下来,应用程序才能链接到正确版本的Vulkan ABI 库。 Vulkan 版本机制保证ABI 在同一个主版本号内向后兼容。(比如1.0 和1.1)。在Windows上,加载器库文件名字包含了ABI版本,所以会多个版本的Vulkan加载器库会同时存在。 Vulkan加载器库文件名形如 vulkan-<ABI version>.dll。例如,对于Windows上Vulkan 1.X版本,库文件名为 vulkan-1.dll。 这个文件通常可以在 windows/system32 目录找到(在64位系统上,32位版本的加载器以同样的命在可以在windows/sysWOW64 目录找到)。

对于Linux,共享库的版本号以后缀决定。故,ABI号并不如Windows上那样编码。 在Linux上应用程序如果想要连接最新版本的Vulkan ABI版本,只需要链接libvulkan.so 即可。也可以由应用程序指定链接的Vulkan ABI版本(如libvulkan.so.1)。

Application Layer Usage

应用程序可以使用各种layers或者拓展来获取在核心API 提供的基础上更多的能力。 一个layer并不能引入在Vulkan.h中暴露出来的新的Vulkan核心API 函数。然而,layers可以提供拓展,来引入新的Vulkan命令,可以通过查询拓展接口来获取到。

layers常见的一种使用方式是API验证,它可以在应用程序开发过程中载入layer时被启动,但是在应用程序发布时被关闭。 这减少了验证应用程序使用API的性能损失,在一些其他的图形API中并没有这个特性。

可以使用vkEnumerateInstanceLayerProperties来获知应用程序可以用哪些layers。 它会报告加载器发现的所有layers。加载器在系统的各个路径搜索layers。关于此点,请参考下节的Layer discovery 。

要想启用一个layer,仅需要在调用vkCreateInstance时把你想要启用的layer的名字传递到 VkInstanceCreateInfo的 ppEnabledLayerNames域。 一旦完成了,你想要启用的layers对于所有的使用已创建的VkInstance对象及其子对象的Vulkan函数都已经启用了。

NOTE: Layer 顺序在几个场合下非常重要,因为一些layers会相互之间操作。当此情形时需要格外小心。参考Overall Layer Ordering 一节获取更多信息。

如下代码展示了如何启用VK_LAYER_LUNARG_standard_validation layer。

   char *instance_validation_layers[] = {
        "VK_LAYER_LUNARG_standard_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_validation_layers,
        .enabledExtensionCount = 0,
        .ppEnabledExtensionNames = NULL,
    };
    err = vkCreateInstance(&inst_info, NULL, &demo->inst);

在 vkCreateInstance 和 vkCreateDevice时,加载器构造了调用链,包含应用程序指定启用的layers。 ppEnabledLayerNames数组中元素的顺序非常重要。 0号元素是调用链最顶层的layer,最后一个元素距离驱动最近。参看 Overall Layer Ordering 一节来获取关于layer排序的信息。

NOTE: Device Layers 已经被废弃

vkCreateDevice 最初可以像vkCreateInstance那样选择layers。这导致 "instance layers" 概念 和 "device layers"。Khronos决定废弃"device layer" 功能,只考虑"instance layers"。 因此, vkCreateDevice 将会使用vkCreateInstance指定的layers。 因此,如下条目也被废弃了:

  • VkDeviceCreateInfo fields:
  • ppEnabledLayerNames
  • enabledLayerCount
  • The vkEnumerateDeviceLayerProperties function
Implicit vs Explicit Layers

显式layers是被应用程序启用的layers(例如,通过vkCreateInstance 函数启用),或者通过环境变量(如前面提到的)。

隐式layers是那些默认存在的layers。例如,特定的应用程序环境(如Steam或者娱乐系统)可能有对于所有应用程序都启用的layers。其他的隐式layers可能有系统给所有的程序都自动启用了, 相比之下显式layers需要显式地启用。

隐式层相比于显式层有一些附加的要求,它们需要受环境变量控制来启用、关闭。这是因为它们不受应用程序影响,也不会造成任何问题。 一个好的准则就是记住要定义启动和关闭layers的环境变量,这样用户就可以决定启用哪些功能了。 在桌面平台(Windows和Linux),这些启用/关闭设定都在层的JSON文件中定义了。

关于系统安装的显式和隐式layers在稍后的 Layer Discovery Section 讲解。 我们暂时只需要知道显式、隐式的区别取决于操作系统即可,如下表所示:

Operating System Implicit Layer Identification
Windows 隐式层和显式层在不同的Windows注册表位置
Linux 隐式层和显式层在不同的目录中
Android Android ** 不支持显式层 **
Forcing Layer Source Folders

开发者也许需要使用特殊的,预先产生的layers,而无需修改系统安装的layers。 你可以通过定义"VK_LAYER_PATH" 环境变量来引导加载器到一个特定的文件夹搜寻layers。 这将覆盖查找系统安装的layers的机制。因为重点的layers可能在系统的不同文件夹中,这个环境变量可以包含几个不同的路径,以系统特定路径分隔符来分割。 在Windows上,每一个文件夹路径在列表中都应该用一个分号来分割。在Linux上每个文件夹路径都应该用一个冒号来分割。

如果 "VK_LAYER_PATH" 存在, 只有 这个文件夹会被扫描。列表中每一个目录都应该是包含layer明细文件的全路径。

Forcing Layers to be Enabled on Windows and Linux

开发者可能想要那些在使用的给定应用程序并没有被启用的层开始启用。 在Linux和Windows上,环境变量"VK_INSTANCE_LAYERS" 可以用来启用那些并没有被应用程序的vkCreateInstance`指定的层。 "VK_INSTANCE_LAYERS" 是一个以冒号(Linux)、分号(Windows)间隔的层名字的列表。 前后顺序的关系是:在列表中第一个是最顶层的层(靠近应用程序),列表最后一个是最底层的layer(靠近驱动)。 参考 Overall Layer Ordering 一节以获取更多细节。

应用程序指定的layers和用户指定的layers(通过环境变量)会被综合,重复的会被加载器删除。通过环境变量指定的layers是最顶层(靠近应用程序),而应用程序指定的layers是最底层的。

使用用环境变量的一个例子是在Windows或Linux上启用的验证layer VK_LAYER_LUNARG_parameter_validation :

> $ export VK_INSTANCE_LAYERS=VK_LAYER_LUNARG_parameter_validation
Overall Layer Ordering

The overall ordering of all layers by the loader based on the above looks as follows:

Loader Layer Ordering

排序对于多个显式layer也很重要。一些layers可能依赖于加载器在加载之前或者之后的其他的行为。 例如, VK_LAYER_LUNARG_core_validation 要求VK_LAYER_LUNARG_parameter_validation 先被调用。 这是因为VK_LAYER_LUNARG_parameter_validation 将会在VK_LAYER_LUNARG_core_validation进行检查之前 过滤任何无效的 NULL 指针。如果没有这么干,你可能会看到 VK_LAYER_LUNARG_core_validation层崩溃,这本是可以避免的。

Application Usage of Extensions

拓展是由layer,加载器或者ICD提供的可选的功能。拓展可以修改Vulkan API的行为,需要在Khronos进行指定和注册。 这些拓展可以由Independent Hardware Vendor (IHV) 创建,来暴露出硬件的功能,或者由layer程序制造者来暴露内部特性,或者由加载器来提升性能。 提取信息用途的拓展可以在Vulkan Spec,vulkah.h头文件中找到。

Instance and Device Extensions

如在 Instance Versus Device 一节中提到的,有两种类型的拓展:

  • Instance Extensions
  • Device Extensions

实例拓展是一种可修改实例级别对象(如VkInstanceVkPhysicalDevice)上已有行为或者实现新的行为的拓展。 一个设备拓展也是相似的定义,只是针对于任何 VkDevice 对象,或者以VkDevice(VkQueue 和 VkCommandBuffer 等)为父对象的可转发对象。

当你使用vkCreateInstance启用实例拓展,用vkCreateDevice启用设备拓展时,知道我们要启用那种类别的拓展 非常 重要。

加载器从layers(显式的和隐式的)和ICD中收集并综合所有的拓展,发生在加载器在把这些信息通过vkEnumerateXXXExtensionProperties XXX 是 "Instance" 或 "Device") 返回给应用程序之前。

  • Instance extensions are discovered via vkEnumerateInstanceExtensionProperties.
  • Device extensions are be discovered via vkEnumerateDeviceExtensionProperties.

查看vulkan.h, 你将发现它们很像。比如,vkEnumerateInstanceExtensionProperties 原型如下:

   VkResult
   vkEnumerateInstanceExtensionProperties(const char *pLayerName,
                                          uint32_t *pPropertyCount,
                                          VkExtensionProperties *pProperties);

The "pLayerName" parameter in these functions is used to select either a single layer or the Vulkan platform implementation. If "pLayerName" is NULL, extensions from Vulkan implementation components (including loader, implicit layers, and ICDs) are enumerated. If "pLayerName" is equal to a discovered layer module name then only extensions from that layer (which may be implicit or explicit) are enumerated. Duplicate extensions (e.g. an implicit layer and ICD might report support for the same extension) are eliminated by the loader. For duplicates, the ICD version is reported and the layer version is culled.

Also, Extensions must be enabled (in vkCreateInstance or vkCreateDevice) before the functions associated with the extensions can be used. If you get an Extension function using either vkGetInstanceProcAddr or vkGetDeviceProcAddr, but fail to enable it, you could experience undefined behavior. This should actually be flagged if you run with Validation layers enabled.

WSI Extensions

Khronos已经审批的WSI拓展已经可用了,并提供了多种执行环境的Windows System集成。 我们必须要明白一些WSI拓展对所有平台都有效,但有一些值针对某些执行环境或者加载器才有效。桌面端加载器(目前只有Windows和Linux支持)只 启用并直接对当前环境暴露出了这些WSI拓展。WSI拓展的选择,在加载器编译期间就通过标志位指定了。所有版本的桌面端加载器当前都至少保留了如下的WSI拓展:

  • VK_KHR_surface
  • VK_KHR_swapchain
  • VK_KHR_display

此外,如下的目标操作系统的加载器都支持其特定的拓展:

Windowing System Extensions available
Windows VK_KHR_win32_surface
Linux (Default) VK_KHR_xcb_surface and VK_KHR_xlib_surface
Linux (Wayland) VK_KHR_wayland_surface
Linux (Mir) VK_KHR_mir_surface

NOTE: Wayland 和 Mir 平台在当前并没有完全被支持。 Wayland支持只是出现了,但应当被视为beta版本。Mir支持则完全没有实现。

要明确虽然加载支持拓展的多重入口,但是需要认证才能使用它们:

  • 至少有一个物理设备必须支持这些拓展
  • 应用程序必须选择这样的一个物理设备
  • 应用程序必须要求在床将实例或者逻辑设备时,这些拓展被启用了(这依赖于给定的拓展和实例或设备之间是否共同工作)。
  • 实例或者逻辑设备的创建必须要成功

这些条件都符合之后你才能在Vulkan程序中使用WSI拓展。

Unknown Extensions

拓展Vulkan的能力是如此的简单,所以创建拓展时加载器也无需知道拓展相关的信息。如果是设备拓展,加载器将把未知的入口函数传递设备调用链,以ICD入口点函数为作为结束。 对于实例拓展而言,也是同样的过程,拓展接受一个物理设备参数为第一个component。然而,对于所有其他的实例拓展,加载器将加载失败。

But why doesn't the loader support unknown instance extensions? 
Let's look again at the Instance call chain:

Instance call chain

注意,对于一个普通实例函数调用,加载器必须处理好把函数调用传递给可用的ICD。如果加载器不知道实例调用的参数或者返回值,就不可能正确的把信息传递到ICD。 有很多办法可以做到这样,将在后面提到。然而,目前,加载器不支持把物理设备作为第一个参数的实例拓展。

Because the device call-chain does not normally pass through the loader terminator, this is not a problem for device extensions. Additionally, since a physical device is associated with one ICD, we can use a generic terminator pointing to one ICD. This is because both of these extensions terminate directly in the ICD they are associated with.

Is this a big problem? 
No! 绝大多数拓展功能只影响一个物理设备或者逻辑设备,并不影响实例。故,大多数拓展都应该被加载器所直接支持。

Filtering Out Unknown Instance Extension Names

在一些情形下,一个ICD可能支持实例拓展,但加载器并不支持。 对于以上原因,当应用程序调用vkEnumerateInstanceExtensionProperties时,加载器将过滤掉这些未知的实例拓展的名字。 另外,如果你继续使用这些拓展,此行为将导致加载器在运行vkCreateInstance时抛出一个错误。 这将保护应用程序,避免其使用能致其崩溃的功能。

另外一方面,如果你能够安全的使用拓展,你可以定义环境变量 VK_LOADER_DISABLE_INST_EXT_FILTER 来关闭过滤,并设置这个值为一个非0值。 这将高效禁用加载器的过滤实例拓展的名字。

猜你喜欢

转载自blog.csdn.net/cloudqiu/article/details/78886070
今日推荐