Vulkan on Android 3 — Render Nothing with Lots of Hidden Details Part 2
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
.