1. Introduction
Why use Vulkan in my game?
Vulkan is the primary low-level graphics API on Android. Vulkan enables reaching higher performance for games that implement their own game engine and renderer.
Vulkan is available on Android from Android 7.0 (API level 24). Vulkan 1.1 support is a requirement for new 64-bit Android devices beginning with Android 10.0. The 2022 Android Baseline Profile also sets a minimum Vulkan API version of 1.1.
Games that have lots of draw calls and that use OpenGL ES can see significant driver overhead due to the high cost of making draw calls within OpenGL ES. These games can become CPU bound from spending large portions of their frame time in the graphics driver. These games can also see significant reductions in CPU and power use by switching from OpenGL ES to Vulkan. This is especially applicable if the game has complex scenes that can't effectively use instancing to reduce draw calls.
What you'll build
In this codelab, you're going to take a basic C++ Android App and add code to set up the Vulkan rendering pipeline. You will then implement code that uses Vulkan to render a textured, rotating triangle on the screen.
What you'll need
- Android Studio Iguana or later.
- An Android device running Android 10.0 or later, connected to your computer, that has Developer options and USB debugging enabled.
2. Getting set up
Setting up your development environment
If you have not previously worked with native projects in Android Studio, you may need to install Android NDK and CMake. If you already have them installed, proceed to Setting up the project.
Checking that the SDK, NDK and CMake is installed
Launch Android Studio. When the Welcome to Android Studio window is displayed, open the Configure dropdown menu and select the SDK Manager option.

If you already have an existing project opened, you can instead open the SDK Manager via the Tools menu. Click on Tools menu and select SDK Manager, the SDK Manager window will open.
In the sidebar, select in order: Appearance & Behavior > System Settings > Android SDK. Select the SDK Platforms tab in the Android SDK pane to display a list of installed tool options. Ensure Android SDK 12.0 or later is installed.

Next, select the SDK Tools tab and ensure NDK and CMake are installed.
Note: The exact version shouldn't matter as long as they're reasonably new, but we're currently on NDK 26.1.10909125 and CMake 3.22.1. The version of the NDK being installed by default will change over time with subsequent NDK releases. If you need to install a specific version of the NDK, follow the instructions in the Android Studio reference for installing the NDK under the section "Install a specific version of the NDK".

Once all the required tools are checked, click the Apply button at the bottom of the window to install them. You may then close the Android SDK window by clicking the OK button.
Setting up the project
A starting project derived from the C++ template has been set up for you in a git repository. The starting project implements app initialization and event handling, but does not yet do any graphics setup or rendering.
Cloning the repo
From the command line, change to the directory you wish to contain the root project directory and clone it from GitHub:
git clone -b codelab/start https://github.com/android/getting-started-with-vulkan-on-android-codelab.git --recurse-submodules
Make sure that you're starting from the initial commit of the repo titled [codelab] start: empty app.
Open the project with Android Studio, build the project, then run it on an attached device. The project will launch to an empty black screen, you will be adding graphics rendering in the following sections.
3. Create a Vulkan instance and device
The first step in initializing the Vulkan API for use is to create a Vulkan instance object (VkInstance).
The VkInstance object represents an application's instance of the Vulkan runtime. It is the root object of the Vulkan API and is used to retrieve information about and instantiate Vulkan device objects and any layers it wants to activate.
When an application creates a VkInstance, it must provide information about itself, such as its name, version, and the Vulkan instance extensions it needs.
The Vulkan API design includes a layer system that provides a mechanism for intercepting and processing API calls before they reach the GPU driver. The application can designate layers to activate when creating a VkInstance. The most commonly used layer is the Vulkan validation layer, which provides runtime analysis of API usage for errors or suboptimal performance practices.
Once a VkInstance has been created, the application can use it to query for the available physical devices on the system, create logical devices, and create surfaces to render to.
A VkInstance is typically created once at the start of the application and destroyed at the end. However, it is possible to create multiple VkInstances within the same application, for example, if the application needs to use multiple GPUs or create multiple windows.
// CODELAB: hellovk.h
void HelloVK::createInstance() {
VkApplicationInfo appInfo{};
appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
appInfo.pApplicationName = "Hello Triangle";
appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.pEngineName = "No Engine";
appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.apiVersion = VK_API_VERSION_1_0;
VkInstanceCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
createInfo.pApplicationInfo = &appInfo;
createInfo.enabledExtensionCount = (uint32_t)requiredExtensions.size();
createInfo.ppEnabledExtensionNames = requiredExtensions.data();
createInfo.pApplicationInfo = &appInfo;
createInfo.enabledLayerCount = 0;
createInfo.pNext = nullptr;
VK_CHECK(vkCreateInstance(&createInfo, nullptr, &instance));
}
}
A VkPhysicalDevice is a Vulkan object that represents a physical Vulkan device on the system. Most Android devices will only have return a single VkPhysicalDevice representing the GPU. However a PC or Android device could enumerate multiple physical devices. For example, a computer than contains both discrete GPU and an integrated GPU.
VkPhysicalDevices can be queried for their properties, such as their name, vendor, driver version, and supported features. This information can be used to choose the best physical device for a particular application.
Once a VkPhysicalDevice has been chosen, the application can create a logical device from it. A logical device is a representation of the physical device that is specific to the application. It has its own state and resources, and it is independent of other logical devices that may be created from the same physical device.
There are different types of queues that originate from different Queue Families and each family of queues allows only a subset of commands. For example, there could be a queue family that only allows processing of compute commands or one that only allows memory transfer related commands.
A VkPhysicalDevice can enumerate all available types of Queue Families. We are only interested in the graphics queue here, but there may also be additional queues that support only COMPUTE or TRANSFER. A Queue Family does not have its own type. Instead, it is represented by numeric index type uint32_t inside their parent object (VkPhysicalDevice).
A VkPhysicalDevice can have multiple logical devices created from it. This is useful for applications that need to use multiple GPUs or create multiple windows.
A VkDevice is a Vulkan object that represents a logical Vulkan device. It is a thin abstraction over the physical device, and provides all of the functionality needed to create and manage Vulkan resources, such as buffers, images, and shaders.
A VkDevice is created from a VkPhysicalDevice, and it is specific to the application that created it. It has its own state and resources, and it is independent of other logical devices that may be created from the same physical device.
A VkSurfaceKHR object represents a surface that can be the target of rendering operations. To display graphics on the device screen, you will create a surface using a reference to the application window object. Once a VkSurfaceKHR object has been created, the application can use it to create a VkSwapchainKHR object.
VkSwapchainKHR object represents an infrastructure that owns the buffers we will render to before we can visualize them on the screen. It is essentially a queue of images that are waiting to be presented to the screen. We will acquire such an image to draw to it, and then return it to the queue. How exactly the queue works and the conditions for presenting an image from the queue depends on how the swap chain is set up, but the general purpose of the swap chain is to synchronize the presentation of images with the refresh rate of the screen.
// CODELAB: hellovk.h - Data Types
struct QueueFamilyIndices {
std::optional<uint32_t> graphicsFamily;
std::optional<uint32_t> presentFamily;
bool isComplete() {
return graphicsFamily.has_value() && presentFamily.has_value();
}
};
struct SwapChainSupportDetails {
VkSurfaceCapabilitiesKHR capabilities;
std::vector<VkSurfaceFormatKHR> formats;
std::vector<VkPresentModeKHR> presentModes;
};
struct ANativeWindowDeleter {
void operator()(ANativeWindow *window) { ANativeWindow_release(window); }
};
You can set up validation layer support if you need to debug your application. You can also check specific extensions your game may need.
// CODELAB: hellovk.h
bool HelloVK::checkValidationLayerSupport() {
uint32_t layerCount;
vkEnumerateInstanceLayerProperties(&layerCount, nullptr);
std::vector<VkLayerProperties> availableLayers(layerCount);
vkEnumerateInstanceLayerProperties(&layerCount, availableLayers.data());
for (const char *layerName : validationLayers) {
bool layerFound = false;
for (const auto &layerProperties : availableLayers) {
if (strcmp(layerName, layerProperties.layerName) == 0) {
layerFound = true;
break;
}
}
if (!layerFound) {
return false;
}
}
return true;
}
std::vector<const char *> HelloVK::getRequiredExtensions(
bool enableValidationLayers) {
std::vector<const char *> extensions;
extensions.push_back("VK_KHR_surface");
extensions.push_back("VK_KHR_android_surface");
if (enableValidationLayers) {
extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
}
return extensions;
}
Once you've found the appropriate setup and create the VkInstance, create the VkSurface which represents the window to render to.
// CODELAB: hellovk.h
void HelloVK::createSurface() {
assert(window != nullptr); // window not initialized
const VkAndroidSurfaceCreateInfoKHR create_info{
.sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR,
.pNext = nullptr,
.flags = 0,
.window = window.get()};
VK_CHECK(vkCreateAndroidSurfaceKHR(instance, &create_info,
nullptr /* pAllocator */, &surface));
}
Enumerate the physical device (GPUs) available and pick the first suitable device available.
// CODELAB: hellovk.h
void HelloVK::pickPhysicalDevice() {
uint32_t deviceCount = 0;
vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr);
assert(deviceCount > 0); // failed to find GPUs with Vulkan support!
std::vector<VkPhysicalDevice> devices(deviceCount);
vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data());
for (const auto &device : devices) {
if (isDeviceSuitable(device)) {
physicalDevice = device;
break;
}
}
assert(physicalDevice != VK_NULL_HANDLE); // failed to find a suitable GPU!
}
To check whether the device is suitable, we need to find one that supports the GRAPHICS queue.
// CODELAB: hellovk.h
bool HelloVK::isDeviceSuitable(VkPhysicalDevice device) {
QueueFamilyIndices indices = findQueueFamilies(device);
bool extensionsSupported = checkDeviceExtensionSupport(device);
bool swapChainAdequate = false;
if (extensionsSupported) {
SwapChainSupportDetails swapChainSupport = querySwapChainSupport(device);
swapChainAdequate = !swapChainSupport.formats.empty() &&
!swapChainSupport.presentModes.empty();
}
return indices.isComplete() && extensionsSupported && swapChainAdequate;
}
// CODELAB: hellovk.h
bool HelloVK::checkDeviceExtensionSupport(VkPhysicalDevice device)