实现 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 函数(根据其第一个参数分派到相应的层或驱动程序)。vkGet*ProcAddr 调用会返回 trampoline 将分派到的函数指针(即它会直接调用核心 API 代码),由于通过这些函数指针(而非导出的符号)进行调用跳过了 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 函数。加载程序可以通过调用 hw_device_tvkGetInstanceProcAddr 找到所有其他的 VkInstanceVkPhysicalDevicevkGetDeviceProcAddr 函数。

发现和加载层

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

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

层的使用情形包括:

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

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

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

Vulkan API 版本和功能

本部分将介绍受支持的 Vulkan API 版本。

Vulkan API 版本 1.0

Android 7.0 版增加了对 Vulkan API 版本 1.0 的支持。

Vulkan API 版本 1.1

Android 9 版本引入了对 Vulkan 1.1 图形 API 的支持。要详细了解 Vulkan 1.1 API,请参阅 Vulkan 1.1 API 规格

Vulkan 1.1 支持概览

Vulkan 1.1 支持包括对 Vulkan 1.1 和 memory/synchronization 交互操作的支持。这样一来,OEM 即可在设备上支持 Vulkan 1.1,而且开发者可以确定某个设备是否支持 Vulkan 1.1,并在支持时有效地使用该版本。Vulkan 1.1 没有 Vulkan 1.0 之外的新硬件要求,但相应实现大都位于特定于 SOS 的图形驱动程序(而不是框架)中。

对于 Android,最重要的 Vulkan 1.1 功能包括:

  • 支持从 Vulkan 外部导入和导出内存缓冲区和同步对象(以便与摄像头、编解码器和 GLES 进行交互操作)
  • 支持 YCbCr 格式

Vulkan 1.1 还包含几项较小的功能和 API 易用性增强功能。

实现 Vulkan 1.1

Vulkan 1.1 在 Android 9 版本中是可选的。要使用该版本,设备必须具有符合 Vulkan 1.1 最低功能要求的 GPU。

要实现 Vulkan 1.1,请执行以下操作:

  1. 添加支持 Vulkan 1.1 且符合其他 Android 1.1 要求的 Vulkan 驱动程序(请务必查看 9 API 级别文档),或更新现有 Vulkan 1.0 驱动程序。
  2. 您可能需要更新内核 GPU 驱动程序,具体取决于您的实现。
  3. 确保 PackageManager#hasSystemFeature(PackageManager.FEATURE_VULKAN_HARDWARE_VERSION, 0x401000) 返回 true,方法是将如下所示的规则添加到相关的 device.mk 文件:

    PRODUCT_COPY_FILES += frameworks/native/data/etc/android.hardware.vulkan.version-1_1.xml: $(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.vulkan.version.xml

自定义 Vulkan 1.1

依赖于 Vulkan 1.1 的应用与不提供 Vulkan 1.1 支持的设备不兼容。有些应用可能不需要 Vulkan 1.1,但会在具有 Vulkan 1.1 时提供额外的功能或性能。

窗口系统集成 (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 8.0 会要求驱动程序将请求的格式和图像用途标记转换为 gralloc 用途标记,具体方法是调用以下内容:

typedef enum VkSwapchainImageUsageFlagBitsANDROID {
    VK_SWAPCHAIN_IMAGE_USAGE_SHARED_BIT_ANDROID = 0x00000001,
    VK_SWAPCHAIN_IMAGE_USAGE_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF
} VkSwapchainImageUsageFlagBitsANDROID;
typedef VkFlags VkSwapchainImageUsageFlagsANDROID;

VkResult VKAPI vkGetSwapchainGrallocUsage2ANDROID(
    VkDevice                          device,
    VkFormat                          format,
    VkImageUsageFlags                 imageUsage,
    VkSwapchainImageUsageFlagsANDROID swapchainUsage,
    uint64_t*                         grallocConsumerUsage,
    uint64_t*                         grallocProducerUsage
);

formatimageUsage 参数来自 VkSwapchainCreateInfoKHR 结构。驱动程序应使用相关格式和用途所需的 gralloc 用途标记来填充 *grallocConsumerUsage*grallocProducerUsage。驱动程序返回的用途将在系统分配缓冲区时与交换链消费者请求的用途标记配合使用。

此函数的早期版本由 Android 7.x 调用。在 Android 8.0 中,此函数的早期版本已被弃用,但如果驱动程序未提供 vkGetSwapchainGrallocUsage2ANDROID,系统仍会调用它的早期版本:

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

此早期版本不支持交换链使用标记或扩展 gralloc 使用标记。

由 Gralloc 支持的图像

VkNativeBufferANDROID 是一个 vkCreateImage 扩展程序结构,用于创建由 gralloc 缓冲区支持的图像。该结构提供给 VkImageCreateInfo 结构链中的 vkCreateImage。对具有此结构的 vkCreateImage 的调用发生在第一次调用 vkGetSwapChainInfoWSI(.. VK_SWAP_CHAIN_INFO_TYPE_IMAGES_WSI ..) 期间。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;
    // Beginning in Android 8.0, the usage field above is deprecated and the
    // usage2 struct below was added. The usage field is still filled in for
    // compatibility with Android 7.0 drivers. Drivers for Android 8.0
    // should prefer the usage2 struct, especially if the
    // android.hardware.graphics.allocator HAL uses the extended usage bits.
    struct {
        uint64_t                consumer;
        uint64_t                producer;
    } usage2;
} 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
在 Android 8.0 及更高版本中,如果交换链需要任何交换链图像使用标记,则平台将在为 vkCreateImage 提供的 VkImageCreateInfo 链中提供 VkSwapchainImageCreateInfo 扩展结构。该扩展结构包含交换链图像使用标记:
typedef struct {
    VkStructureType                        sType; // must be VK_STRUCTURE_TYPE_SWAPCHAIN_IMAGE_CREATE_INFO_ANDROID
    const void*                            pNext;

    VkSwapchainImageUsageFlagsANDROID      usage;
} VkSwapchainImageCreateInfoANDROID;

获取图像

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,
    uint32_t            waitSemaphoreCount,
    const VkSemaphore*  pWaitSemaphores,
    VkImage             image,
    int*                pNativeFenceFd
);

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

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

如果图像是使用 VK_SWAPCHAIN_IMAGE_USAGE_SHARED_BIT_ANDROID 创建的,则驱动程序必须允许反复调用 vkQueueSignalReleaseImageANDROID,而不会干预对 vkAcquireImageANDROID 的调用。

验证

OEM 可以使用 CTS 测试其 Vulkan 实现,其中包括:

  • CtsDeqpTestCases 模块中的 drawElements 质量计划 (dEQP) 测试,其中包括针对 Vulkan 1.0 和 1.1 的功能 API 测试。
  • CtsGraphicsTestCases 模块,用于测试设备是否针对所支持的 Vulkan 功能进行了正确的配置。