a-simple-triangle / Part 17 - Vulkan setup Android

In this article we will get Vulkan running on Android. When I was researching how to do this I got stuck a number of times and went down a few rabbit holes trying to understand how to get the damn thing working the way I wanted.

One of the key differences with Android compared to MacOS and iOS is that we will use the dynamic Vulkan loader on Android - meaning that there will be no library that we link against at compile time to statically resolve the Vulkan APIs.

The benefit of using the dynamic loader approach is that we can keep our Android app backward compatible with older versions of Android which don’t have Vulkan support - basically Marshmallow (Android 6, API 23) and earlier. On older devices our engine will gracefully fall back to our OpenGL implementation which is great!

The trade off comes in the form of having to do what feels to me like a bit of hackiness to get the Vulkan SDK that is bundled in Android (at least in NDK version 20 which was the latest at the time of authoring this article) to work via the C++ vulkan.hpp header. You will see the hackiness soon, but I hope that a future NDK version resolves the need for it.

The vulkan.hpp header works splendidly for statically compiled Vulkan apps, such as the MacOS and iOS apps we have already, but if we don’t statically compile against an ICD that can resolve all the symbols that the Vulkan C++ header is looking for then we can’t compile properly.

The reason we pinned to a specific version of the Android NDK in part 5 of this series was actually in preparation for this article. If we didn’t pin ourselves to a specific version then eventually our code base would experience a breaking Vulkan source code change via a non deterministic Android NDK.


Vulkan in the Android NDK

I’ll describe where to find the Vulkan source files we need in the Android NDK - we will need to include these files in our Android project to build against but I’ll get to that in a bit.

Browse the Android NDK in our third party folder on Mac at:

/Users/<your user name>/Library/Android/sdk/ndk/20.0.5594570/sources/third_party/vulkan/src

and in Windows:

C:\Users\<your user name>\AppData\Local\Android\Sdk\ndk\20.0.5594570\sources\third_party\vulkan\src

The most interesting folders and files to us under that folder are:

+ common
  vulkan_wrapper.cpp
  vulkan_wrapper.h
+ include
  + vulkan
    vulkan.h
    vulkan.hpp

include/vulkan/vulkan.h: This is the core C Vulkan header file that forms the foundation of the Vulkan APIs.

include/vulkan/vulkan.hpp: This is the C++ Vulkan header file we want to use to mediate our Vulkan integration with the core APIs. The version of the C++ header file that ships with the NDK includes references to a bunch of Vulkan API functions which will cause us compilation errors without some extra work.

common/vulkan_wrapper.h/cpp: The Vulkan wrapper is a representation of the core Vulkan API functions available to us through the dynamic loader. The implementation file contains a rather cryptic collection of function pointer mappings - forming the connection to the dynamically loaded Vulkan library that we don’t know at compile time. There are inconsistencies with this wrapper and the C++ Vulkan header in the NDK which we will have to patch a fix for.

Dynamic API mapping

Here is an example of how the vkCreateInstance function is mapped - though pretty much all of the Vulkan functions are done like this. First off the vulkan_wrapper.h file declares an extern declaration of type PFN_vkCreateInstance and names it vkCreateInstance within the global scope.

// vulkan_wrapper.h
extern PFN_vkCreateInstance vkCreateInstance;

The PFN_vkCreateInstance type is defined in the include/vulkan/vulkan.h file (others are defined in include/vulkan/vulkan_core.h as well) which the wrapper includes like this:

// vulkan.h
typedef VkResult (VKAPI_PTR *PFN_vkCreateInstance)(const VkInstanceCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkInstance* pInstance);

The implementation of the Vulkan wrapper will attempt to load the libvulkan.so library on the current Android device through the invocation of the InitVulkan function - which we will have the responsibility of calling. This function will return with a fail code of 0 if the library could not load. This represents the dynamic loader behaviour and is what allows older versions of Android to still run - if the Vulkan library can’t load then we will simply bootstrap our OpenGL application instead.

// vulkan_wrapper.cpp
int InitVulkan(void) {
  void *libvulkan = dlopen("libvulkan.so", RTLD_NOW | RTLD_LOCAL);
  if (!libvulkan) return 0;

  ...

If the Vulkan library was successfully loaded, the function pointer mapping is then performed like so:

// vulkan_wrapper.cpp
vkCreateInstance = reinterpret_cast<PFN_vkCreateInstance>(dlsym(libvulkan, "vkCreateInstance"));

The "vkCreateInstance" symbol is fetched from the libvulkan target then cast as a PFN_vkCreateInstance type, allowing it to be assigned into the global vkCreateInstance field. By doing this, application code such as in our vulkan.hpp C++ header can invoke the function using notation like this:

// vulkan.hpp
VkResult vkCreateInstance(const VkInstanceCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkInstance* pInstance) const
{
  return ::vkCreateInstance(pCreateInfo, pAllocator, pInstance);
}

The ::vkCreateInstance( ... ) is invoking the vkCreateInstance function pointer in the global namespace which ultimately is resolved through our Vulkan wrapper down into the dynamically loaded Vulkan library.

Clear as mud? It’s not exactly the prettiest code I’ve seen but we have to deal with it to get Vulkan running the way we’d like.

So why am I telling you this? Well I got stuck on this for a long time because the vulkan.hpp header which ships with Android NDK 20 uses a bunch of Vulkan API functions that do not seem to be registered in the vulkan_wrapper.h/cpp even though they are bundled in the same Android NDK. The mismatch means that attempting to compile an Android application using the C++ header will be guaranteed to fail. I’m somewhat baffled why Google choose to release the NDK code in this way … perhaps many people just use the straight C based header instead of the C++ one.

Never fear though, I have a trick that will get us through this problem!


The first fail

Let’s get our Android application up and running all the way to the first batch of compilation errors - exciting stuff isn’t it!?

First we need to add a compilation definition to our CMakeLists.txt file in our Android app folder to allow Vulkan to compile when 32 bit architectures are involved:

# This is required to allow us to compile the Vulkan C++ header successfully.
add_definitions(-DVULKAN_HPP_TYPESAFE_CONVERSION)

Then scroll down to the include_directories section and add two new entries to include the NDK Vulkan files we need:

include_directories(${ANDROID_NDK}/sources/third_party/vulkan/src/include)
include_directories(${ANDROID_NDK}/sources/third_party/vulkan/src/common)

Note: The ANDROID_NDK property is injected automatically into our CMake build script when the Android build tooling runs - we do not need to declare it ourselves.

We also need to add the vulkan_wrapper.cpp from the NDK as a source file to our main target so it is compiled alongside the rest of our code. Update the add_library section to include it:

add_library(
        a-simple-triangle
        SHARED
        ${CPP_HEADERS}
        ${CPP_SOURCES}
        ${ANDROID_NDK}/sources/third_party/vulkan/src/common/vulkan_wrapper.cpp
)

Save and close the CMakeLists.txt file. We will now make a few C++ code changes to add the Vulkan integration to our Android platform.

Graphics wrapper

Previously we added the Vulkan header to our MacOS and iOS platform in core/graphics-wrapper.hpp. We now need to update the Android part of that header file:

#elif __ANDROID__
#include <GLES2/gl2.h>
#define USING_GLES

becomes:

#elif __ANDROID__
#include <vulkan_wrapper.h>
#include <vulkan/vulkan.hpp>
#include <GLES2/gl2.h>
#define USING_GLES

The other spot we have to update is in our bool ast::vulkan::isVulkanAvailable() function in the vulkan-common.cpp file. We need to invoke the InitVulkan() function from vulkan_wrapper.h before any subsequent Vulkan APIs are called. Doing this maps all of our Vulkan function pointers ready to be invoked by application code. Of course if the InitVulkan() function returns 0 then we should consider this to mean there is no Vulkan support on the current device.

Edit the isVulkanAvailable() implementation in vulkan-common.cpp, adding the following at the top of the function:

bool ast::vulkan::isVulkanAvailable()
{
    static const std::string logTag{"ast::vulkan::isVulkanAvailable"};

#ifdef __ANDROID__
    // Try to dynamically load the Vulkan library and seed the function pointer mapping.
    if (!InitVulkan())
    {
        return false;
    }
#endif

    ...

Fantastic - open the project in Android Studio then:

Wait a little while and your build output should start spewing compilation errors at you that look like this:

In file included from .../core/graphics-wrapper.hpp:18:
..../Android/sdk/ndk/20.0.5594570/sources/third_party/vulkan/src/include/vulkan/vulkan.hpp:1129:12: error: no member named 'vkBindAccelerationStructureMemoryNVX' in the global namespace; did you mean simply 'vkBindAccelerationStructureMemoryNVX'?
    return ::vkBindAccelerationStructureMemoryNVX( device, bindInfoCount, pBindInfos);
           ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

You will have heaps of these errors. Because I already experienced this pain I’ll share my learnings on it with you. Notice the error message mentions no member named ... in the global namespace. This should be a hint if you recall how the function pointer mappings are done in the global namespace.

To verify our hunch, open up the vulkan_wrapper.h that ships with the NDK and try to find an extern mapping named vkBindAccelerationStructureMemoryNVX. Go on, I’ll wait. Not there is it? So our C++ Vulkan header is looking for a bunch of Vulkan API signatures that haven’t been declared anywhere in the global namespace of our project.

This is a symptom - and for Android a disadvantage - of using the C++ Vulkan header which tries to map everything it can which forces us to have awareness of SDK APIs that we may not even care about.

Another collection of errors that you can observe in vulkan_wrapper.cpp bundled in the NDK are references to Vulkan symbols that are missing - unless I’m missing something it actually feels like a bug in the NDK code:

To correct these errors we are going to add a new header file which patches in all the missing API functions and stubs out the missing symbols, but doesn’t bother implementing their mappings. Our project will just need to be careful not to use them but I believe most of them aren’t Vulkan APIs applicable to Android anyway. If you did actually want to use them, you’d need to wire them up with an implementation similar to how the rest of the APIs are done.

Create a new cpp source folder in the Android app module and add a header file named vulkan-wrapper-patch.h to it:

: root
  + android
    + app
      + src
        + main
          + cpp
            vulkan-wrapper-patch.h

Now, you are most welcome to work your own way through all those tedious error messages adding extern declarations for each one until they are all resolved, OR you can copy the following code into the vulkan-wrapper-patch.h file since I already did all that tedious work for you - it should be accurate for the NDK version we are compiling against. Note the collection of typedef declarations - these will resolve the missing symbols problems in the vulkan_wrapper.cpp file. All the extern declarations provide the global mapping to the unresolved APIs that are missing from the vulkan_wrapper.h file:

#pragma once

/*
 * Vulkan API definitions to assist with issues in the `vulkan_wrapper.h/vulkan_wrapper.cpp`
 * which ships with the Android NDK 20 when using the `vulkan.hpp` header.
 */

#define VK_NO_PROTOTYPES 1
#include <vulkan/vulkan.h>

typedef void *PFN_vkCreateAccelerationStructureNV;
typedef void *PFN_vkDestroyAccelerationStructureNV;
typedef void *PFN_vkGetAccelerationStructureMemoryRequirementsNV;
typedef void *PFN_vkBindAccelerationStructureMemoryNV;
typedef void *PFN_vkCmdBuildAccelerationStructureNV;
typedef void *PFN_vkCmdCopyAccelerationStructureNV;
typedef void *PFN_vkCmdTraceRaysNV;
typedef void *PFN_vkCreateRayTracingPipelinesNV;
typedef void *PFN_vkGetRayTracingShaderGroupHandlesNV;
typedef void *PFN_vkGetAccelerationStructureHandleNV;
typedef void *PFN_vkCmdWriteAccelerationStructuresPropertiesNV;
typedef void *PFN_vkCompileDeferredNV;

extern PFN_vkBindAccelerationStructureMemoryNVX vkBindAccelerationStructureMemoryNVX;
extern PFN_vkCmdBeginConditionalRenderingEXT vkCmdBeginConditionalRenderingEXT;
extern PFN_vkCmdBeginDebugUtilsLabelEXT vkCmdBeginDebugUtilsLabelEXT;
extern PFN_vkCmdBeginQueryIndexedEXT vkCmdBeginQueryIndexedEXT;
extern PFN_vkCmdBeginTransformFeedbackEXT vkCmdBeginTransformFeedbackEXT;
extern PFN_vkCmdBindShadingRateImageNV vkCmdBindShadingRateImageNV;
extern PFN_vkCmdBindTransformFeedbackBuffersEXT vkCmdBindTransformFeedbackBuffersEXT;
extern PFN_vkCmdBuildAccelerationStructureNVX vkCmdBuildAccelerationStructureNVX;
extern PFN_vkCmdCopyAccelerationStructureNVX vkCmdCopyAccelerationStructureNVX;
extern PFN_vkCmdDebugMarkerBeginEXT vkCmdDebugMarkerBeginEXT;
extern PFN_vkCmdDebugMarkerEndEXT vkCmdDebugMarkerEndEXT;
extern PFN_vkCmdDebugMarkerInsertEXT vkCmdDebugMarkerInsertEXT;
extern PFN_vkCmdDrawIndexedIndirectCountAMD vkCmdDrawIndexedIndirectCountAMD;
extern PFN_vkCmdDrawIndirectByteCountEXT vkCmdDrawIndirectByteCountEXT;
extern PFN_vkCmdDrawIndirectCountAMD vkCmdDrawIndirectCountAMD;
extern PFN_vkCmdDrawMeshTasksIndirectCountNV vkCmdDrawMeshTasksIndirectCountNV;
extern PFN_vkCmdDrawMeshTasksIndirectNV vkCmdDrawMeshTasksIndirectNV;
extern PFN_vkCmdDrawMeshTasksNV vkCmdDrawMeshTasksNV;
extern PFN_vkCmdEndConditionalRenderingEXT vkCmdEndConditionalRenderingEXT;
extern PFN_vkBindAccelerationStructureMemoryNVX vkBindAccelerationStructureMemoryNVX;
extern PFN_vkCmdBeginConditionalRenderingEXT vkCmdBeginConditionalRenderingEXT;
extern PFN_vkCmdBeginDebugUtilsLabelEXT vkCmdBeginDebugUtilsLabelEXT;
extern PFN_vkCmdBeginQueryIndexedEXT vkCmdBeginQueryIndexedEXT;
extern PFN_vkCmdBeginTransformFeedbackEXT vkCmdBeginTransformFeedbackEXT;
extern PFN_vkCmdBindShadingRateImageNV vkCmdBindShadingRateImageNV;
extern PFN_vkCmdBindTransformFeedbackBuffersEXT vkCmdBindTransformFeedbackBuffersEXT;
extern PFN_vkCmdBuildAccelerationStructureNVX vkCmdBuildAccelerationStructureNVX;
extern PFN_vkCmdCopyAccelerationStructureNVX vkCmdCopyAccelerationStructureNVX;
extern PFN_vkCmdDebugMarkerBeginEXT vkCmdDebugMarkerBeginEXT;
extern PFN_vkCmdDebugMarkerEndEXT vkCmdDebugMarkerEndEXT;
extern PFN_vkCmdDebugMarkerInsertEXT vkCmdDebugMarkerInsertEXT;
extern PFN_vkCmdDrawIndexedIndirectCountAMD vkCmdDrawIndexedIndirectCountAMD;
extern PFN_vkCmdDrawIndirectByteCountEXT vkCmdDrawIndirectByteCountEXT;
extern PFN_vkCmdDrawIndirectCountAMD vkCmdDrawIndirectCountAMD;
extern PFN_vkCmdDrawMeshTasksIndirectCountNV vkCmdDrawMeshTasksIndirectCountNV;
extern PFN_vkCmdDrawMeshTasksIndirectNV vkCmdDrawMeshTasksIndirectNV;
extern PFN_vkCmdDrawMeshTasksNV vkCmdDrawMeshTasksNV;
extern PFN_vkCmdEndConditionalRenderingEXT vkCmdEndConditionalRenderingEXT;
extern PFN_vkBindAccelerationStructureMemoryNVX vkBindAccelerationStructureMemoryNVX;
extern PFN_vkCmdBeginConditionalRenderingEXT vkCmdBeginConditionalRenderingEXT;
extern PFN_vkCmdBeginDebugUtilsLabelEXT vkCmdBeginDebugUtilsLabelEXT;
extern PFN_vkCmdBeginQueryIndexedEXT vkCmdBeginQueryIndexedEXT;
extern PFN_vkCmdEndDebugUtilsLabelEXT vkCmdEndDebugUtilsLabelEXT;
extern PFN_vkCmdEndQueryIndexedEXT vkCmdEndQueryIndexedEXT;
extern PFN_vkCmdEndTransformFeedbackEXT vkCmdEndTransformFeedbackEXT;
extern PFN_vkCmdInsertDebugUtilsLabelEXT vkCmdInsertDebugUtilsLabelEXT;
extern PFN_vkCmdProcessCommandsNVX vkCmdProcessCommandsNVX;
extern PFN_vkCmdReserveSpaceForCommandsNVX vkCmdReserveSpaceForCommandsNVX;
extern PFN_vkCmdSetCheckpointNV vkCmdSetCheckpointNV;
extern PFN_vkCmdSetCoarseSampleOrderNV vkCmdSetCoarseSampleOrderNV;
extern PFN_vkCmdSetDiscardRectangleEXT vkCmdSetDiscardRectangleEXT;
extern PFN_vkCmdSetExclusiveScissorNV vkCmdSetExclusiveScissorNV;
extern PFN_vkCmdSetSampleLocationsEXT vkCmdSetSampleLocationsEXT;
extern PFN_vkCmdSetViewportShadingRatePaletteNV vkCmdSetViewportShadingRatePaletteNV;
extern PFN_vkCmdSetViewportWScalingNV vkCmdSetViewportWScalingNV;
extern PFN_vkCmdTraceRaysNVX vkCmdTraceRaysNVX;
extern PFN_vkCmdWriteAccelerationStructurePropertiesNVX vkCmdWriteAccelerationStructurePropertiesNVX;
extern PFN_vkCmdWriteBufferMarkerAMD vkCmdWriteBufferMarkerAMD;
extern PFN_vkCompileDeferredNVX vkCompileDeferredNVX;
extern PFN_vkCreateAccelerationStructureNVX vkCreateAccelerationStructureNVX;
extern PFN_vkCreateDebugReportCallbackEXT vkCreateDebugReportCallbackEXT;
extern PFN_vkCreateDebugUtilsMessengerEXT vkCreateDebugUtilsMessengerEXT;
extern PFN_vkCreateIndirectCommandsLayoutNVX vkCreateIndirectCommandsLayoutNVX;
extern PFN_vkCreateObjectTableNVX vkCreateObjectTableNVX;
extern PFN_vkCreateRaytracingPipelinesNVX vkCreateRaytracingPipelinesNVX;
extern PFN_vkCreateValidationCacheEXT vkCreateValidationCacheEXT;
extern PFN_vkDebugMarkerSetObjectNameEXT vkDebugMarkerSetObjectNameEXT;
extern PFN_vkDebugMarkerSetObjectTagEXT vkDebugMarkerSetObjectTagEXT;
extern PFN_vkDebugReportMessageEXT vkDebugReportMessageEXT;
extern PFN_vkDestroyAccelerationStructureNVX vkDestroyAccelerationStructureNVX;
extern PFN_vkDestroyDebugReportCallbackEXT vkDestroyDebugReportCallbackEXT;
extern PFN_vkDestroyDebugUtilsMessengerEXT vkDestroyDebugUtilsMessengerEXT;
extern PFN_vkDestroyIndirectCommandsLayoutNVX vkDestroyIndirectCommandsLayoutNVX;
extern PFN_vkDestroyObjectTableNVX vkDestroyObjectTableNVX;
extern PFN_vkDestroyValidationCacheEXT vkDestroyValidationCacheEXT;
extern PFN_vkDisplayPowerControlEXT vkDisplayPowerControlEXT;
extern PFN_vkGetAccelerationStructureHandleNVX vkGetAccelerationStructureHandleNVX;
extern PFN_vkGetAccelerationStructureMemoryRequirementsNVX vkGetAccelerationStructureMemoryRequirementsNVX;
extern PFN_vkGetAccelerationStructureScratchMemoryRequirementsNVX vkGetAccelerationStructureScratchMemoryRequirementsNVX;
extern PFN_vkGetCalibratedTimestampsEXT vkGetCalibratedTimestampsEXT;
extern PFN_vkGetImageDrmFormatModifierPropertiesEXT vkGetImageDrmFormatModifierPropertiesEXT;
extern PFN_vkGetMemoryHostPointerPropertiesEXT vkGetMemoryHostPointerPropertiesEXT;
extern PFN_vkGetPastPresentationTimingGOOGLE vkGetPastPresentationTimingGOOGLE;
extern PFN_vkGetPhysicalDeviceCalibrateableTimeDomainsEXT vkGetPhysicalDeviceCalibrateableTimeDomainsEXT;
extern PFN_vkGetPhysicalDeviceExternalImageFormatPropertiesNV vkGetPhysicalDeviceExternalImageFormatPropertiesNV;
extern PFN_vkGetPhysicalDeviceGeneratedCommandsPropertiesNVX vkGetPhysicalDeviceGeneratedCommandsPropertiesNVX;
extern PFN_vkGetPhysicalDeviceMultisamplePropertiesEXT vkGetPhysicalDeviceMultisamplePropertiesEXT;
extern PFN_vkGetPhysicalDeviceSurfaceCapabilities2EXT vkGetPhysicalDeviceSurfaceCapabilities2EXT;
extern PFN_vkGetQueueCheckpointDataNV vkGetQueueCheckpointDataNV;
extern PFN_vkGetRaytracingShaderHandlesNVX vkGetRaytracingShaderHandlesNVX;
extern PFN_vkGetRefreshCycleDurationGOOGLE vkGetRefreshCycleDurationGOOGLE;
extern PFN_vkGetShaderInfoAMD vkGetShaderInfoAMD;
extern PFN_vkGetSwapchainCounterEXT vkGetSwapchainCounterEXT;
extern PFN_vkGetValidationCacheDataEXT vkGetValidationCacheDataEXT;
extern PFN_vkMergeValidationCachesEXT vkMergeValidationCachesEXT;
extern PFN_vkQueueBeginDebugUtilsLabelEXT vkQueueBeginDebugUtilsLabelEXT;
extern PFN_vkQueueEndDebugUtilsLabelEXT vkQueueEndDebugUtilsLabelEXT;
extern PFN_vkQueueInsertDebugUtilsLabelEXT vkQueueInsertDebugUtilsLabelEXT;
extern PFN_vkRegisterDeviceEventEXT vkRegisterDeviceEventEXT;
extern PFN_vkRegisterDisplayEventEXT vkRegisterDisplayEventEXT;
extern PFN_vkRegisterObjectsNVX vkRegisterObjectsNVX;
extern PFN_vkReleaseDisplayEXT vkReleaseDisplayEXT;
extern PFN_vkSetDebugUtilsObjectNameEXT vkSetDebugUtilsObjectNameEXT;
extern PFN_vkSetDebugUtilsObjectTagEXT vkSetDebugUtilsObjectTagEXT;
extern PFN_vkSetHdrMetadataEXT vkSetHdrMetadataEXT;
extern PFN_vkSubmitDebugUtilsMessageEXT vkSubmitDebugUtilsMessageEXT;
extern PFN_vkUnregisterObjectsNVX vkUnregisterObjectsNVX;
extern PFN_vkCreateAccelerationStructureNV vkCreateAccelerationStructureNV;

Yeah can you guess how fun it was figuring this out and making that file?

Hackiness FTW

To include our new header file we need to tweak our CMakeLists.txt file again, adding another include_directories entry to pick up files in our main/cpp folder:

include_directories(${CMAKE_SOURCE_DIR}/src/main/cpp)

Using our new header file is actually a bit tricky because it needs to be added as an #include header into the NDK vulkan_wrapper.h itself so it can patch away the unresolved symbols in the vulkan_wrapper.cpp file. Althought we really don’t want to have to edit the NDK source files, I couldn’t think of a cleaner way to do it. What we will need to do is change the code starting line 24 in the vulkan_wrapper.h file inside the NDK from this:

#define VK_NO_PROTOTYPES 1
#include <vulkan/vulkan.h>

to this:

#define VK_NO_PROTOTYPES 1
#include <vulkan/vulkan.h>
#include <vulkan-wrapper-patch.h>

Now, we could manually make this change, but it would mean it wouldn’t automatically apply if we didn’t yet have the Android NDK and ran android/setup.sh to download a fresh copy of it. So, we will automate making the required code change to the NDK source file via our android/setup.sh instead - that way even a fresh copy of the NDK will be tweaked automatically.

Open up android/setup.sh and at the end add the following monstrosity. Note the use of the $ANDROID_NDK path which we formulated at the start of our setup script a long time ago:

# We need to add our own `vulkan-wrapper-patch.h` header into the `vulkan_wrapper.h`
# that ships with NDK version 20. The patch fixes compilation problems when using the
# `vulkan_wrapper.h` header in combination with the `vulkan.hpp` - both of which ship
# with NDK version 20, but appear to be misaligned - the `vulkan_wrapper.cpp` has
# unknown symbols in it which we will need to patch in.
pushd "$ANDROID_NDK/sources/third_party/vulkan/src/common"
    PATCH_HEADER=`grep -Fx "#include <vulkan-wrapper-patch.h>" vulkan_wrapper.h`

    if [ "" == "$PATCH_HEADER" ]; then
        echo "Patching NDK vulkan_wrapper.h to include <vulkan-wrapper-patch.h> ..."
        # Note: The following 'sed' command is deliberately left aligned so it
        # correctly adds the line break before the header include line for our patch.
sed -i '' -e 's/\#include <vulkan\/vulkan\.h>/\#include <vulkan\/vulkan\.h>\
#include <vulkan-wrapper-patch\.h>/g' vulkan_wrapper.h
    else
        echo "NDK vulkan_wrapper.h already patched to use vulkan-wrapper-patch.h ..."
    fi
popd

What we are doing here is this:

#include <vulkan/vulkan.h>

and replacing it with:

#include <vulkan/vulkan.h>
#include <vulkan-wrapper-patch.h>

Note: As the shell script comment mentions, the sed command is deliberately left aligned so it can properly insert a line break without any spaces before it.

Save setup.sh then run it again, you will see output like this:

$ ./setup.sh

Patching NDK vulkan_wrapper.h to include <vulkan-wrapper-patch.h> ...

Feel free to run it again to see that once it has been patched it won’t do it again:

$ ./setup.sh

NDK vulkan_wrapper.h already patched to use vulkan-wrapper-patch.h ...

Android on Windows setup script

For our Windows based setup script add the following to the bottom of the android\setup.ps1 script to achieve a similar outcome to the MacOS setup script regarding patching the NDK header file:

# We need to add our own `vulkan-wrapper-patch.h` header into the `vulkan_wrapper.h`
# that ships with NDK version 20. The patch fixes compilation problems when using the
# `vulkan_wrapper.h` header in combination with the `vulkan.hpp` - both of which ship
# with NDK version 20, but appear to be misaligned - the `vulkan_wrapper.cpp` has
# unknown symbols in it which we will need to patch in.
Push-Location "$env:ANDROID_NDK\sources\third_party\vulkan\src\common"
    $PATCH_HEADER = Select-String -Path vulkan_wrapper.h -Pattern "#include <vulkan-wrapper-patch.h>"

    if ($null -eq $PATCH_HEADER) {
        Write-Host "Patching <android-ndk>\sources\third_party\vulkan\src\common\vulkan_wrapper.h to include patch wrapper ..."
        ((Get-Content -Path vulkan_wrapper.h -Raw) -replace('\#include <vulkan\/vulkan\.h>', "#include <vulkan/vulkan.h>`r`n#include <vulkan-wrapper-patch.h>")) | Set-Content -Path vulkan_wrapper.h
    }
Pop-Location

Write-Host "All done - import the project in this folder into Android Studio to run it!"

Sweet - don’t you just love Android development … now go back to Android Studio and notice that the symbol errors in vulkan_wrapper.cpp are now gone and the vulkan_wrapper.h has our <vulkan-wrapper-patch.h> include directive which will clear up all those no member named '...' in the global namespace compilation errors.

Run the Android application again and if the stars align you should finally be up and running with Vulkan!


Summary

I think it was pretty neat that we are able to have Vulkan support in our Android app but keep the backward compatible OpenGLES support too in the same application.

It was unfortunate about the amount of effort and hackiness required to use the Vulkan C++ header but we got there in the end. If you coded directly against the Vulkan C header you probably wouldn’t have some of these issues but in my opinion it is definately worth the effort - when you see some of the Vulkan code we have to write later on I am glad to be able to use C++ for it.

In the next article, we’ll deal with the Emscripten platform target, which actually can’t use Vulkan.

The code for this article can be found here.

Continue to Part 18: Vulkan setup Emscripten.

End of part 17