Implementing Vulkan

Vulkan is a low-overhead, cross-platform API for high-performance 3D graphics. Like OpenGL ES (GLES), Vulkan provides tools for creating high-quality, real-time graphics in apps. Advantages of using Vulkan include reductions in CPU overhead and support for the SPIR-V Binary Intermediate language.

To implement Vulkan successfully, a device must include:

  • The Vulkan loader, provided by Android.
  • A Vulkan driver, provided by SoCs such as GPU IHVs, that implements the Vulkan API. To support Vulkan functionality, the Android device needs Vulkan-capable GPU hardware and the associated driver. The GPU must also support GLES 3.1 and higher. Consult your SoC vendor to request driver support.

If a device includes a Vulkan driver, the device needs to declare FEATURE_VULKAN_HARDWARE_LEVEL and FEATURE_VULKAN_HARDWARE_VERSION system features, with versions that accurately reflect the capabilities of the device. This helps ensure that the device is in compliance with the Compatibility Definition Document (CDD).

Vulkan loader

The Vulkan loader platform/frameworks/native/vulkan is the primary interface between Vulkan apps and a device's Vulkan driver. The Vulkan loader is installed at /system/lib[64]/libvulkan.so. The loader provides the core Vulkan API entry points, as well as the entry points of extensions required by the Android CDD. Window System Integration (WSI) extensions are exported by the loader and primarily implemented in the loader rather than in the driver. The loader also supports enumerating and loading layers that can expose additional extensions and intercept core API calls on their way to the driver.

The NDK includes a stub libvulkan.so library for linking. The library exports the same symbols as the loader. Apps call the functions exported from the real libvulkan.so library to enter trampoline functions in the loader, which dispatch to the appropriate layer or driver based on their first argument. The vkGet*ProcAddr() call returns the function pointers to which the trampolines dispatch (that is, it calls directly into the core API code). Calling through the function pointers, rather than the exported symbols, is more efficient as it skips the trampoline and dispatch.

Driver enumeration and loading

When the system image is built, Android expects the system to know which GPUs are available. The loader uses the existing HAL mechanism in hardware.h to discover and load the driver. Preferred paths for 32-bit and 64-bit Vulkan drivers are:

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

In Android 7.0 and higher, the Vulkan hw_module_t derivative wraps a single hw_module_t struct; only one driver is supported and the constant string HWVULKAN_DEVICE_0 is passed to open().

The Vulkan hw_device_t derivative corresponds to a single driver that can support multiple physical devices. The hw_device_t structure can extend to export vkGetGlobalExtensionProperties(), vkCreateInstance(), and vkGetInstanceProcAddr() functions. The loader can find all other VkInstance(), VkPhysicalDevice(), and vkGetDeviceProcAddr() functions by calling the hw_device_t structure's vkGetInstanceProcAddr().

Layer discovery and loading

The Vulkan loader supports enumerating and loading layers that can expose additional extensions and intercept core API calls on their way to the driver. Android doesn't include layers on the system image; however, apps may include layers in their APK.

When using layers, keep in mind that Android's security model and policies differ significantly from other platforms. In particular, Android doesn't allow loading external code into a nondebuggable process on production (nonrooted) devices, nor does it allow external code to inspect or control the process's memory, state, and so on. This includes a prohibition on saving core dumps, API traces, and so on to disk for later inspection. Only layers delivered as part of nondebuggable apps are enabled on production devices, and drivers must not provide functionality that violates these policies.

Use cases for layers include:

  • Development-time layers — Validation layers and shims for tracing/profiling/debugging tools shouldn't be installed on the system image of production devices. Validation layers and shims for tracing/profiling/debugging tools should be updatable without a system image. Developers who want to use one of these layers during development can modify the app package, for example, by adding a file to their native libraries directory. IHV and OEM engineers who want to diagnose failures in shipping unmodifiable apps are assumed to have access to nonproduction (rooted) builds of the system image, unless those apps are debuggable. For more information see Vulkan validation layers on Android.
  • Utility layers — These layers expose extensions, such as a layer that implements a memory manager for device memory. Developers choose layers, and versions of those layers, to use in their app; different apps using the same layer may still use different versions. Developers choose which of these layers to ship in their app package.
  • Injected (implicit) layers — Includes layers such as frame rate, social network, and game launcher overlays provided by the user or some other app without the app's knowledge or consent. These violate Android's security policies and aren't supported.

For nondebuggable apps, the loader searches for layers only in the app's native library directory and attempts to load any library with a name matching a particular pattern (for example, libVKLayer_foo.so).

For debuggable apps, the loader searches for layers in /data/local/debug/vulkan and attempts to load any library matching a particular pattern.

Android enables layers to be ported with build-environment changes between Android and other platforms. For details on the interface between layers and the loader, see Architecture of the Vulkan Loader Interfaces. The Khronos-maintained validation layers are hosted in Vulkan Validation Layers.

Vulkan API versions and capabilities

Android 9 and higher support the Vulkan API version 1.1. Android 7 to Android 9 support the Vulkan API version 1.0. For more information about the Vulkan 1.1 API, see the Vulkan 1.1 API spec.

Vulkan 1.1 support overview

Vulkan 1.1 includes support for memory/synchronization interop, which enables OEMs to support Vulkan 1.1 on devices. Additionally, memory/synchronization interop enables developers to determine whether Vulkan 1.1 is supported on a device, and use it effectively when it is. Vulkan 1.1 has the same hardware requirements as Vulkan 1.0, but most of the implementation is in the SOC-specific graphics driver, not in the framework.

The most important Vulkan 1.1 features for Android are:

  • Support for importing and exporting memory buffers and synchronization objects from outside Vulkan (for interop with camera, codecs, and GLES)
  • Support for YCbCr formats

Vulkan 1.1 also includes several smaller features and API usability enhancements.

Implementing Vulkan 1.1

Android devices should support Vulkan 1.1 if they:

  • Launch with Android 10.
  • Support a 64-bit ABI.
  • Aren't low memory.

Other devices can optionally support Vulkan 1.1.

To implement Vulkan 1.1:

  1. Add a Vulkan driver that supports Vulkan 1.1 plus the additional Android 1.1 CDD requirements, or update the existing Vulkan 1.0 driver.
  2. Ensure that PackageManager#hasSystemFeature(PackageManager.FEATURE_VULKAN_HARDWARE_VERSION, 0x401000) returns true by adding a rule such as the following to an appropriate device.mk file:
    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
    

Window System Integration (WSI)

In libvulkan.so, the driver implements the following Window System Integration (WSI) extensions:

  • VK_KHR_surface
  • VK_KHR_android_surface
  • VK_KHR_swapchain
  • VK_KHR_driver_properties, implemented for Vulkan 1.1 in Android 10 only
  • VK_GOOGLE_display_timing, implemented for any Vulkan version in Android 10

The VkSurfaceKHR and VkSwapchainKHR objects and all interactions with ANativeWindow are handled by the platform and aren't exposed to drivers. The WSI implementation relies on the VK_ANDROID_native_buffer extension, which must be supported by the driver; this extension is used only by the WSI implementation and isn't exposed to apps.

Gralloc usage flags

Vulkan implementations may need swapchain buffers to be allocated with implementation-defined private Gralloc usage flags. When creating a swapchain, Android asks the driver to translate the requested format and image usage flags into Gralloc usage flags by calling:

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
);

The format and imageUsage parameters are taken from the VkSwapchainCreateInfoKHR structure. The driver should fill *grallocConsumerUsage and *grallocProducerUsage with the Gralloc usage flags required for the format and usage. The usage flags returned by the driver are combined with the usage flags requested by the swapchain consumer when allocating buffers.

Android 7.x calls an earlier version of VkSwapchainImageUsageFlagsANDROID(), named vkGetSwapchainGrallocUsageANDROID(). Android 8.0 and higher deprecates vkGetSwapchainGrallocUsageANDROID() but still calls vkGetSwapchainGrallocUsageANDROID() if vkGetSwapchainGrallocUsage2ANDROID() isn't provided by the driver:

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

vkGetSwapchainGrallocUsageANDROID() doesn't support swapchain usage flags or extended Gralloc usage flags.

Gralloc-backed images

VkNativeBufferANDROID is a vkCreateImage extension structure for creating an image backed by a Gralloc buffer. VkNativeBufferANDROID is provided to vkCreateImage() in the VkImageCreateInfo structure chain. Calls to vkCreateImage() with VkNativeBufferANDROID happen during the first call to vkGetSwapChainInfoWSI(.. VK_SWAP_CHAIN_INFO_TYPE_IMAGES_WSI ..). The WSI implementation allocates the number of native buffers requested for the swapchain, then creates a VkImage for each one:

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;

When creating a Gralloc-backed image, VkImageCreateInfo has the following data:

  .sType               = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO
  .pNext               = the above VkNativeBufferANDROID structure
  .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

In Android 8.0 and higher, the platform provides a VkSwapchainImageCreateInfo extension structure in the VkImageCreateInfo chain provided to vkCreateImage when any swapchain image usage flags are required for the swapchain. The extension structure contains the swapchain image usage flags:

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

    VkSwapchainImageUsageFlagsANDROID      usage;
} VkSwapchainImageCreateInfoANDROID;

In Android 10 and higher, the platform supports VK_KHR_swapchain v70, so the Vulkan app is able to create a VkImage backed by swapchain memory. The app first calls vkCreateImage with a VkImageSwapchainCreateInfoKHR structure chained to the VkImageCreateInfo structure. Then the app calls vkBindImageMemory2(KHR) with a VkBindImageMemorySwapchainInfoKHR structure chained to the VkBindImageMemoryInfo structure. The imageIndex specified in the VkBindImageMemorySwapchainInfoKHR structure must be a valid swapchain image index. Meanwhile, the platform provides a VkNativeBufferANDROID extension structure with the corresponding Gralloc buffer information to the VkBindImageMemoryInfo chain, so the driver knows which Gralloc buffer to bind the VkImage with.

Acquiring images

vkAcquireImageANDROID acquires ownership of a swapchain image and imports an externally signaled native fence into both an existing VkSemaphore object and an existing VkFence object:

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

vkAcquireImageANDROID() is called during vkAcquireNextImageKHR to import a native fence into the VkSemaphore and VkFence objects provided by the app (however, both semaphore and fence objects are optional in this call). The driver may also use this opportunity to recognize and handle any external changes to the Gralloc buffer state; many drivers won't need to do anything here. This call puts VkSemaphore and VkFence into the same pending state as vkQueueSignalSemaphore and vkQueueSubmit respectively, so queues can wait on the semaphore and the app can wait on the fence.

Both objects become signaled when the underlying native fence signals; if the native fence has already signaled, then the semaphore is in the signaled state when this function returns. The driver takes ownership of the fence file descriptor and closes the fence file descriptor when no longer needed. The driver must do so even if neither a semaphore or fence object is provided, or even if vkAcquireImageANDROID fails and returns an error. If fenceFd is -1, it's as if the native fence was already signaled.

Releasing images

vkQueueSignalReleaseImageANDROID prepares a swapchain image for external use, creates a native fence, and schedules the native fence to be signaled after the input semaphores have signaled:

VkResult VKAPI vkQueueSignalReleaseImageANDROID(
    VkQueue             queue,
    uint32_t            waitSemaphoreCount,
    const VkSemaphore*  pWaitSemaphores,
    VkImage             image,
    int*                pNativeFenceFd
);

vkQueuePresentKHR() calls vkQueueSignalReleaseImageANDROID() on the provided queue. The driver must produce a native fence that doesn't signal until all waitSemaphoreCount semaphores in pWaitSemaphores signal, and any additional work required to prepare image for presentation completes.

If the wait semaphores (if any) already signaled, and queue is already idle, the driver can set *pNativeFenceFd to -1 instead of an actual native fence file descriptor, indicating that there's nothing to wait for. The caller owns and closes the file descriptor returned in *pNativeFenceFd.

Many drivers can ignore the image parameter, but some may need to prepare CPU-side data structures associated with a Gralloc buffer for use by external image consumers. Preparing buffer contents for use by external consumers should be done asynchronously as part of transitioning the image to VK_IMAGE_LAYOUT_PRESENT_SRC_KHR.

If the image was created with VK_SWAPCHAIN_IMAGE_USAGE_SHARED_BIT_ANDROID, then the driver must allow vkQueueSignalReleaseImageANDROID() to be called repeatedly without intervening calls to vkAcquireImageANDROID().

Shared presentable image support

Some devices can share ownership of a single image between the display pipeline and the Vulkan implementation to minimize latency. In Android 9 and higher, the loader conditionally advertises the VK_KHR_shared_presentable_image extension based on the driver's response to a call to vkGetPhysicalDeviceProperties2.

If the driver doesn't support either Vulkan 1.1 or the VK_KHR_physical_device_properties2 extension, the loader doesn't advertise support for shared presentable images. Otherwise, the loader queries the driver capabilities by calling vkGetPhysicalDeviceProperties2() and including the following structure in the VkPhysicalDeviceProperties2::pNext chain:

typedef struct {
    VkStructureType sType; // must be VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENTATION_PROPERTIES_ANDROID
    const void*     pNext;
    VkBool32        sharedImage;
} VkPhysicalDevicePresentationPropertiesANDROID;

If the driver can share ownership of an image with the display system, it sets the sharedImage member to VK_TRUE.

Validation

OEMs can test their Vulkan implementation using CTS, which includes:

  • Khronos Vulkan Conformance tests in the CtsDeqpTestCases module, which include functional API tests for Vulkan 1.0 and 1.1.
  • The CtsGraphicsTestCases module, which tests that the device is configured correctly for Vulkan capabilities it supports.