实现 Vulkan

Vulkan 是用于高性能 3D 图形处理的低开销、跨平台 API。与 OpenGL ES 一样,Vulkan 提供多种用于在应用中创建高质量的实时图形的工具。Vulkan 的优势包括降低 CPU 开销和支持 SPIR-V 二进制中间语言。

注意:本部分介绍的是 Vulkan 实现;有关 Vulkan 架构、优势、API 和其他资源的详细信息,请参阅 Vulkan 架构

要实现 Vulkan,设备:

  • 必须在构建环境中包含 Vulkan 加载程序(由 Android 提供)。
  • 必须包含实现 Vulkan API 的 Vulkan 驱动程序(由 SoC 提供,如 GPU IHV)。为了支持 Vulkan 功能,Android 设备需要功能满足需求的 GPU 硬件和相关驱动程序。请咨询您的 SoC 供应商,以请求获取驱动程序支持。

如果设备上有可用的 Vulkan 驱动程序,则该设备需要声明 FEATURE_VULKAN_HARDWARE_LEVELFEATURE_VULKAN_HARDWARE_VERSION 系统功能,并且相关版本能够准确反映设备的功能。

Vulkan 加载程序

Vulkan 应用和设备的 Vulkan 驱动程序之间的主要接口是 Vulkan 加载程序,它是 Android 开放源代码项目 (AOSP) (platform/frameworks/native/vulkan) 的一部分,并安装在 /system/lib[64]/libvulkan.so。加载程序会提供核心 Vulkan API 入口点,以及 Android 上必需且始终存在的一些扩展程序的入口点。尤其是,窗口系统集成 (WSI) 扩展程序由加载程序导出,并主要在加载程序(而非驱动程序)中实现。此外,加载程序还支持枚举和加载可显示其他扩展程序且/或在核心 API 调用到达驱动程序的途中对其进行拦截的层。

NDK 包含一个存根 libvulkan.so 库,该库可导出与加载程序相同的符号(用于进行关联)。在设备上运行时,应用会调用从 libvulkan.so(真正的库,而非存根)导出的 Vulkan 函数,以进入加载程序中的 trampoline 函数(然后根据其第一个参数分派到相应的层或驱动程序)。vkGetDeviceProcAddr 调用返回 trampoline 将分派到的函数指针(即它直接调用核心 API 代码),由于通过这些函数指针(而非导出的符号)进行调用跳过了 trampoline 和分派,因此其效率更高一些。不过,vkGetInstanceProcAddr 必须仍调用 trampoline 代码。

驱动程序枚举和加载

Android 要求在构建系统映像时系统可用的 GPU 是已知状态。加载程序使用现有 HAL 机制(请参阅 hardware.h)来发现和加载驱动程序。32 位和 64 位 Vulkan 驱动程序的首选路径分别为:

/vendor/lib/hw/vulkan.<ro.product.platform>.so
/vendor/lib64/hw/vulkan.<ro.product.platform>.so

其中,<ro.product.platform> 需替换为具有该名称的系统属性的值。有关详细信息和受支持的备选位置,请参阅 libhardware/hardware.c

在 Android 7.0 中,Vulkan hw_module_t 衍生微不足道;仅支持一个驱动程序,并将常量字符串 HWVULKAN_DEVICE_0 传递给 open 函数。如果在 Android 后续版本中添加对多个驱动程序的支持,则 HAL 模块将导出可以传递给 module open 调用的字符串列表。

Vulkan hw_device_t 衍生对应单个驱动程序,尽管该驱动程序可以支持多个物理设备。可以扩展 hw_device_t 结构,以导出 vkGetGlobalExtensionPropertiesvkCreateInstancevkGetInstanceProcAddr 函数。加载程序可以通过调用 vkGetInstanceProcAddr 找到所有其他 VkInstanceVkPhysicalDevicevkGetDeviceProcAddr 函数。

发现和加载层

Vulkan 加载程序支持枚举和加载可显示其他扩展程序且/或在核心 API 调用到达驱动程序的途中对其进行拦截的层。Android 7.0 在系统映像上不包含层;但是,应用可以在其 APK 中包含层。

使用层时请注意,Android 的安全模型和政策与其他平台截然不同。尤其是,Android 不允许将外部代码加载到正式版(未取得 root 权限)设备上的不可调试进程中,也不允许外部代码检查或控制进程的内存、状态等。这包括禁止将核心转储、API 跟踪等保存到磁盘以供日后进行检查。只有作为应用一部分提交的层会在正式版设备上启用,而且驱动程序不得提供违反这些政策的功能。

层的使用情形包括:

  • 开发期间的层。这些层(验证层,用于跟踪/分析/调试工具的 Shim 层等)不得安装在正式版设备的系统映像上(因为它们会浪费用户的空间),并且应当可在无需系统更新的情况下进行更新。想要在开发过程中使用这些层之一的开发者可以修改应用包(例如,向其原生库目录中添加一个文件)。对于想要在即将推出的不可修改应用中诊断故障的 IHV 和原始设备制造商 (OEM) 工程师,假定其能够访问系统映像的非正式(已取得 root 权限)版本。
  • 实用工具层。这些层几乎总是显示扩展程序,例如为设备内存实现内存管理器的层。开发者可选择要在其应用中使用的层(以及这些层的版本);使用相同层的不同应用仍可使用不同的版本。开发者可选择要在其应用包中包含哪些层。
  • 注入(隐含)层。在应用不知情或未经应用同意的情况下,包含用户或一些其他应用提供的层,如帧速率、社交网络或游戏启动器叠加层。这些层违反了 Android 的安全政策,因此不受支持。

在正常状态下,加载程序仅在应用的原生库目录中搜索层,并尝试加载任何名称符合特定格式(例如 libVKLayer_foo.so)的库。它不需要单独的清单文件,因为开发者有意包含这些层,而避免在启用库之前加载它们的原因不适用。

Android 允许在 Android 与其他平台之间移植层(包括编译环境更改)。有关层与加载程序之间接口的详细信息,请参阅 Vulkan 加载程序规范和架构概览。已经过验证可在 Android 上构建和运行的 LunarG 验证层的版本托管在 GitHub 上 KhronosGroup/Vulkan-LoaderAndValidationLayers 项目的 android_layers 分支下。

窗口系统集成 (WSI)

窗口系统集成 (WSI) 扩展程序 VK_KHR_surfaceVK_KHR_android_surfaceVK_KHR_swapchain 由 Android 平台实现并存在于 libvulkan.so 中。VkSurfaceKHRVkSwapchainKHR 对象以及与 ANativeWindow 的所有互动都由 Android 平台处理,不会提供给驱动程序。WSI 实现依赖于必须受驱动程序支持的 VK_ANDROID_native_buffer 扩展程序(如下所述);此扩展程序仅由 WSI 实现使用,不会提供给应用。

Gralloc 用途标记

实现可能需要使用由实现定义的私密 gralloc 用途标记来分配交换链缓冲区。创建交换链时,Android 平台要求驱动程序将请求的格式和图像用途标记转换为 gralloc 用途标记,具体方法是调用:

VkResult VKAPI vkGetSwapchainGrallocUsageANDROID(
    VkDevice            device,
    VkFormat            format,
    VkImageUsageFlags   imageUsage,
    int*                grallocUsage
);

formatimageUsage 参数来自 VkSwapchainCreateInfoKHR 结构。驱动程序应使用相关格式和用途所需的 gralloc 用途标记来填充 *grallocUsage(分配缓冲区时,与交换链消费者请求的用途标记配合使用)。

由 Gralloc 支持的图像

VkNativeBufferANDROID 是一个 vkCreateImage 扩展程序结构,用于创建由 gralloc 缓冲区支持的图像。该结构提供给 VkImageCreateInfo 结构链中的 vkCreateImage。在第一次调用 vkGetSwapChainInfoWSI(.. VK_SWAP_CHAIN_INFO_TYPE_IMAGES_WSI ..) 期间,会调用具有此结构的 vkCreateImage。WSI 实现为交换链分配请求的原生缓冲区数,然后为每个缓冲区创建一个 VkImage

typedef struct {
    VkStructureType             sType; // must be VK_STRUCTURE_TYPE_NATIVE_BUFFER_ANDROID
    const void*                 pNext;

    // Buffer handle and stride returned from gralloc alloc()
    buffer_handle_t             handle;
    int                         stride;

    // Gralloc format and usage requested when the buffer was allocated.
    int                         format;
    int                         usage;
} VkNativeBufferANDROID;

创建由 gralloc 支持的图像时,VkImageCreateInfo 具有以下数据:

 .imageType           = VK_IMAGE_TYPE_2D
  .format              = a VkFormat matching the format requested for the gralloc buffer
  .extent              = the 2D dimensions requested for the gralloc buffer
  .mipLevels           = 1
  .arraySize           = 1
  .samples             = 1
  .tiling              = VK_IMAGE_TILING_OPTIMAL
  .usage               = VkSwapChainCreateInfoWSI::imageUsageFlags
  .flags               = 0
  .sharingMode         = VkSwapChainCreateInfoWSI::sharingMode
  .queueFamilyCount    = VkSwapChainCreateInfoWSI::queueFamilyCount
  .pQueueFamilyIndices = VkSwapChainCreateInfoWSI::pQueueFamilyIndices

获取图像

vkAcquireImageANDROID 会获取交换链图像的所有权,并将已获得外部信号量的原生栅栏同时导入到现有的 VkSemaphore 对象和现有的 VkFence 对象:

VkResult VKAPI vkAcquireImageANDROID(
    VkDevice            device,
    VkImage             image,
    int                 nativeFenceFd,
    VkSemaphore         semaphore,
    VkFence             fence
);

此函数在 vkAcquireNextImageWSI 期间被调用,以将原生栅栏导入到应用提供的 VkSemaphoreVkFence 对象中(不过,在此调用中,信号量和栅栏对象均是可选的)。驱动程序可能也会借此机会识别和处理 gralloc 缓冲区状态的所有外部更改;许多驱动程序在此无需进行任何操作。此调用会将 VkSemaphoreVkFence 分别置于与 vkQueueSignalSemaphorevkQueueSubmit 相同的待处理状态,因此队列可以等待信号量,应用可以等待栅栏。

当底层的原生栅栏发出信号时,两个对象都处于有信号量的状态;如果原生栅栏已经处于有信号量状态,则当该函数返回时,信号量也处于有信号量的状态。驱动程序拥有栅栏 fd 的所有权,负责在其不再需要时将其关闭。即使没有提供信号量或栅栏对象,或者即使 vkAcquireImageANDROID 失败并返回错误,它也必须这样做。如果 fenceFd 为 -1,就如同原生栅栏已处于有信号量的状态。

释放图像

vkQueueSignalReleaseImageANDROID 会准备交换链图像以供外部使用,创建一个原生栅栏,并安排其在队列上的前期工作完成后收到信号:

VkResult VKAPI vkQueueSignalReleaseImageANDROID(
    VkQueue             queue,
    VkImage             image,
    int*                pNativeFenceFd
);

此 API 在提供的队列上的 vkQueuePresentWSI 期间被调用。具体效果与 vkQueueSignalSemaphore 类似,但使用的是原生栅栏,而非信号量。不过,与 vkQueueSignalSemaphore 不同的是,此调用会创建并返回将收到信号(而非作为输入进行提供)的同步对象。如果调用此函数时队列已经闲置,则允许其(但并非必需)将 *pNativeFenceFd 设置为 -1。调用方拥有 *pNativeFenceFd 中返回的文件描述符并会将其关闭。

更新驱动程序

很多驱动程序可以忽略图像参数,但有些驱动程序可能需要准备与 gralloc 缓冲区相关联的 CPU 端数据结构,以供外部图像消费者使用。作为将图像转换为 VK_IMAGE_LAYOUT_PRESENT_SRC_KHR 的一个环节,准备外部消费者使用的缓冲区内容应该异步完成。

验证

OEM 可以使用 CTS 测试其 Vulkan 实现,其中包括使用 Vulkan 运行时的 drawElements 质量计划 (dEQP) 测试。