Vulkan on Android 3 — Render Nothing with Lots of Hidden Details Part 2

Jin-Long Wu
4 min readOct 3, 2018

--

Photo by Jonas Svidras on Unsplash

In Vulkan, we need to explicitly choose a device to cooperate whether we are intent on rendering something or not. That is a tedious and elaborate process so it deserves a single composition.

Device

There are two kinds of devices, physical and logical. A physical device usually represents a single complete implementation of Vulkan(excluding instance-level functionality) available to the host, while a logical device represents an instance of that implementation with its own state and resources independent of other logical devices.

Let’s see what members are there in Device. We usually use VkDevice in most cases but have to select a physical device(VkPhysicalDevice) to create it beforehand. The spec takes it into account that we might have multiple physical devices on hardware. Additionally, it allows deriving multiple logical devices from one physical device. Though in most cases there is the only one physical device and one its corresponding logical device.

Before creating a logical device, we check the properties( VkPhysicalDeviceProperties) of the physical device. They include the information about the physical device we used like names, API version, an integrated or independent GPU, the “limits” of the devices like various max and min parameters, etc.

Like instance creation, we specify optional layers and extensions to use. Nowadays the device layers are deprecated and should be the same as the instance layers. If they differ we are probably to receive a message indicating the device’s layers will be overridden by instance’s layers if we have validation layers enabled.

In addition to layers and extensions, we specify device-specific features(VkPhysicalDeviceFeatures) to use like geometry shader, drawing wide lines, extended data types, etc. About 60+ features the spec enumerates so we are not going to list them all.

We can also query memory types(VkPhysicalDeviceMemoryProperties) supported by a physical device. When we need to allocate memory from a device we should consider what type of memory best fits our needs to obtain better performance.

Queue

A queue(VkQueue) is an object that executes various operations on a device and they are created as part of device creation. Queues are grouped into queue families(VkQueueFamilyProperties) with each supporting one or more different kinds of operations. What types of queue families a logical device can be created with are determined by the capabilities of the physical device. Four types of operation a queue supports are:

  • Graphics operation.
  • Compute operation.
  • Transfer operation.
  • Sparse memory management operation.

Besides, each VkQueueFamilyProperties has a boolean value indicates if the queue family supports image presentation or not. A queue family support one or more types of operations. However, if a queue family supports graphics and compute operation, we store it in QueueGroup::graphics and find another queue family which supports compute operation to be stored in QueueGroup::compute for clarity.

Here’s the example we might have containing what we have discussed so far.

Before we actually kick off to create a VkDevice the constructor needs a VkPhysicalDevice as the argument. SelectPhysicalDevice enumerates physical devices with VkInstance and pass them to IsPhysicalDeviceSuitable to check if the physical device meets our requirements(queuesRequested / deviceExtensionNamesRequested / featuresRequested / needPresent).

In Device constructor we query device properties, features and memory properties with vkGetPhysicalDeviceProperties, vkGetPhysicalDevice-Features, and vkGetPhysicalDeviceQueueFamilyProperties based on injected VkPhysicalDevice. Then query queue families with vkGetPhysicalDeviceQueueFamilyProperties. We have seen the calling pattern when we enumerate instance layers and extensions and query the capabilities of a surface and will be more familiar with this considering we will confront many situations with the pattern.

EnumerateExtensions take instance layers’ names as arguments to enumerate extensions layer by layer and the control flow of the function is pretty much the same to LayerAndExtension enumerating instance extensions. Like enabling instance extensions, we take the all-or-nothing approach.

GetQueueFamilyIndex finds a particular queue family index based on operation specified with queueFlagBit and try to get dedicated one first since dedicated one is rarer than those supporting multiple types of operations and usually appear on high-end GPU.

CreateDevice use vkCreateDevice with features and extensions we want and queue family indices just queried to create a VkDevice. We bypass duplicated queue family indices or validation layers will complain about it and layers enabled on instance affect derived devices so we ignore enabledlayerCount and ppEnabledLayerNames.

--

--