Vulkan Tutorial

1-3 - Device Info

Each Vulkan device has its own properties, feature set, limits, set of supported extensions and so on. We can query maximum texture resolution, find out if geometry shader is supported, if we can use double or half precision computations, or whether particular image format is supported on the given physical device. We can also get supported Vulkan version and list of extensions.

If you run 1-3-DeviceInfo example, you can see the output similar to the following one:

Vulkan instance:
   Version: 1.3.275
Vulkan devices:
   Quadro RTX 3000
      Vulkan version:  1.3.277
      Device type:     DiscreteGpu
      VendorID:        0x10de
      DeviceID:        0x1f36
      MaxTextureSize:  32768
      Geometry shader:   supported
      Double precision:  supported
      Memory heaps:
         0: 5955MiB  (device local)
         1: 16200MiB
         2: 214MiB  (device local)
      Queue families:
         0: gct  (count: 16)
         1: t  (count: 2)
         2: ct  (count: 8)
         3: t  (count: 3)
         4: t  (count: 1)
      Format support for images with optimal tiling:
         R8G8B8A8Srgb:        yes
         R8G8B8Srgb:          no
         R16G16B16A16Sfloat:  yes
         R16G16B16Sfloat:     no

We can see various information about the Vulkan instance and device. How to get all this information?

Vulkan provides number of query functions that return much of information in number of structures. Quick overview can be found in the following text:

vk::enumerateInstanceVersion() returns instance version as uint32_t
Vulkan instance:
   Version: 1.3.275
vk::getPhysicalDeviceProperties() returns vk::PhysicalDeviceProperties structure
Vulkan devices:
   Quadro RTX 3000
      Vulkan version: 1.3.277
      Device type: DiscreteGpu
      VendorID: 0x10de
      DeviceID: 0x1f36
      MaxTextureSize: 32768
VkPhysicalDeviceProperties::deviceName
VkPhysicalDeviceProperties::apiVersion
VkPhysicalDeviceProperties::deviceType
VkPhysicalDeviceProperties::vendorID
VkPhysicalDeviceProperties::deviceID
VkPhysicalDeviceProperties::limits::maxImageDimension2D
vk::getPhysicalDeviceFeatures() returns vk::PhysicalDeviceFeatures structure
      Geometry shader: supported
      Double precision: supported
VkPhysicalDeviceFeatures::geometryShader
VkPhysicalDeviceFeatures::shaderFloat64
vk::getPhysicalDeviceMemoryProperties() returns vk::PhysicalDeviceMemoryProperties structure
   Memory heaps:
      0: 5955MiB (device local)
      1: 16200MiB
      2: 214MiB (device local)
vk::PhysicalDeviceMemoryProperties::memoryHeapCount
vk::PhysicalDeviceMemoryProperties::memoryHeaps
vk::getPhysicalDeviceQueueFamilyProperties() returns vk::QueueFamilyProperties structure
   Queue families:
      0: gct (count: 16)
      1: t (count: 2)
      2: ct (count: 8)
      3: t (count: 3)
      4: t (count: 1)
vk::QueueFamilyProperties::queueFlags
vk::QueueFamilyProperties::queueCount
vk::getPhysicalDeviceFormatProperties() returns vk::FormatProperties structure
      Format support for [...]:
         R8G8B8A8Srgb:        yes
         R8G8B8Srgb:          no
         R16G16B16A16Sfloat:  yes
         R16G16B16Sfloat:     no
vk::FormatProperties::optimalTilingFeatures

Instance version

To get Vulkan instance version, vk::enumerateInstanceVersion() function is used:

// instance version
uint32_t instanceVersion = vk::enumerateInstanceVersion();
cout << "Vulkan instance:\n"
     << "   Version:  " << vk::apiVersionMajor(instanceVersion) << "."
     << vk::apiVersionMinor(instanceVersion) << "." << vk::apiVersionPatch(instanceVersion) << endl;

As we can see, the version is returned as uint32_t. Then, major, minor and patch versions are extracted from particular bits of uint32_t value by vk::apiVersion[Major|Minor|Patch]() functions.

Instance version tells us about the functionality that is provided by Vulkan instance. For example, version 1.0 might be too low for many applications, while 1.1 is quite ok in many cases.

One note: vkEnumerateInstanceVersion() was introduced by Vulkan 1.1. So, if the function is absent, the instance version is 1.0. For the sake of convenience, vkg hides this technical detail and vk::enumerateInstanceVersion() always returns valid instance version, even in the case of version 1.0.

Device properties

Vulkan 1.0 stores device properties in vk::PhysicalDeviceProperties structure. We can find device name there, device type (integrated/discrete/...), Vulkan version of the device, various device limits in nested structure vk::PhysicalDeviceLimits, and so on.

To get the properties, vk::getPhysicalDeviceProperties() function can be used:

// device properties
vk::PhysicalDeviceProperties properties = vk::getPhysicalDeviceProperties(pd);
cout << "   " << properties.deviceName << endl;

// device Vulkan version
cout << "      Vulkan version:  " << vk::apiVersionMajor(properties.apiVersion) << "."
     << vk::apiVersionMinor(properties.apiVersion) << "." << vk::apiVersionPatch(properties.apiVersion) << endl;

// device type
const char* s;
switch(properties.deviceType) {
case vk::PhysicalDeviceType::eIntegratedGpu: s = "IntegratedGpu"; break;
case vk::PhysicalDeviceType::eDiscreteGpu:   s = "DiscreteGpu"; break;
case vk::PhysicalDeviceType::eVirtualGpu:    s = "VirtualGpu"; break;
case vk::PhysicalDeviceType::eCpu:           s = "Cpu"; break;
default: s = "Other";
}
cout << "      Device type:     " << s << endl;

// VendorID and DeviceID
cout << "      VendorID:        0x" << hex << properties.vendorID << endl;
cout << "      DeviceID:        0x" << properties.deviceID << dec << endl;

// device limits
cout << "      MaxTextureSize:  " << properties.limits.maxImageDimension2D << endl;

More info can be found in the Vulkan specification.

Device features

Device features are functionalities not necessarily supported on all devices. Some functionalities might be supported but others do not. Moreover, we must explicitly enable each feature before use.

// device features
vk::PhysicalDeviceFeatures features = vk::getPhysicalDeviceFeatures(pd);
cout << "      Geometry shader:   ";
if(features.geometryShader)
	cout << "supported" << endl;
else
	cout << "not supported" << endl;
cout << "      Double precision:  ";
if(features.shaderFloat64)
	cout << "supported" << endl;
else
	cout << "not supported" << endl;

More queryable features of Vulkan 1.0 are described in vk::PhysicalDeviceFeatures structure documentation.

Device memory properties

Vulkan distinguishes between device memory and host memory. The device memory is usually the one on graphics card and host memory is usually plugged into the motherboard close to the processor. No wonder that device can usually access its graphic memory very efficiently while access to the host memory on the motherboard might be more complicated and slower. The same for the processor - its access to graphics card memory might be more complicated and slower than to the memory on the motherboard. Interesting situation appears on integrated graphics solutions where device memory and host memory might be the same physical memory.

To get information about the memory configuration on given system, vk::getPhysicalDeviceMemoryProperties() can be used:

cout << "      Memory heaps:" << endl;
vk::PhysicalDeviceMemoryProperties memoryProperties = vk::getPhysicalDeviceMemoryProperties(pd);
for(uint32_t i=0, c=memoryProperties.memoryHeapCount; i<c; i++) {
	vk::MemoryHeap& h = memoryProperties.memoryHeaps[i];
	cout << "         " << i << ": " << h.size/1024/1024 << "MiB";
	if(h.flags & vk::MemoryHeapFlagBits::eDeviceLocal)
		cout << "  (device local)";
	cout << endl;
}

Device queue family properties

Device queues execute commands submitted for execution on particular device and by particular queue. They are organized into families. Each family has the same properties and the same functionality. We can list queue families as follows:

cout << "      Queue families:" << endl;
vk::Vector queueFamilyList = vk::getPhysicalDeviceQueueFamilyProperties(pd);
for(uint32_t i=0, c=uint32_t(queueFamilyList.size()); i<c; i++) {
	cout << "         " << i << ": ";
	vk::QueueFamilyProperties& queueFamilyProperties = queueFamilyList[i];
	if(queueFamilyProperties.queueFlags & vk::QueueFlagBits::eGraphics)
		cout << "g";
	if(queueFamilyProperties.queueFlags & vk::QueueFlagBits::eCompute)
		cout << "c";
	if(queueFamilyProperties.queueFlags & vk::QueueFlagBits::eTransfer)
		cout << "t";
	cout << "  (count: " << queueFamilyProperties.queueCount << ")" << endl;
}

We output family index followed by letters g,c and t. These letters indicate graphics, compute and transfer capability. So, we might have queues just for graphics, just for compute or for both. Some of the families might support transfer capability as well. Finally, we output number of the queues in the given family.

Device format properties

Buffers and images use various formats for their data. To find our if particular format is supported on the device, we can use vk::getPhysicalDeviceFormatProperties().

For example, to find out if we can render into R8G8B8A8Srgb format, we can use following code:

cout << "      Format support for rendering into images with optimal tiling:" << endl;
vk::FormatProperties formatProperties = vk::getPhysicalDeviceFormatProperties(pd, vk::Format::eR8G8B8A8Srgb);
if(formatProperties.optimalTilingFeatures & vk::FormatFeatureFlagBits::eColorAttachment)
	cout << "         R8G8B8A8Srgb:        yes" << endl;
else
	cout << "         R8G8B8A8Srgb:        no" << endl;

As the result, we get vk::FormatProperties structure that contains few variables full of bit flags that indicate support for various use cases. In our case, we are interested in eColorAttachment flag inside optimal tiling member. If the flag is set, we can render into image with R8G8B8A8Srgb format and optimal tiling.

For other formats, we use the same code:

formatProperties = vk::getPhysicalDeviceFormatProperties(pd, vk::Format::eR8G8B8Srgb);
if(formatProperties.optimalTilingFeatures & vk::FormatFeatureFlagBits::eColorAttachment)
	cout << "         R8G8B8Srgb:          yes" << endl;
else
	cout << "         R8G8B8Srgb:          no" << endl;
formatProperties = vk::getPhysicalDeviceFormatProperties(pd, vk::Format::eR16G16B16A16Sfloat);
if(formatProperties.optimalTilingFeatures & vk::FormatFeatureFlagBits::eColorAttachment)
	cout << "         R16G16B16A16Sfloat:  yes" << endl;
else
	cout << "         R16G16B16A16Sfloat:  no" << endl;
formatProperties = vk::getPhysicalDeviceFormatProperties(pd, vk::Format::eR16G16B16Sfloat);
if(formatProperties.optimalTilingFeatures & vk::FormatFeatureFlagBits::eColorAttachment)
	cout << "         R16G16B16Sfloat:     yes" << endl;
else
	cout << "         R16G16B16Sfloat:     no" << endl;

R8G8B8A8Srgb is a kind of standard rendering output for many applications. Its support is mandatory by Vulkan specification in Formats chapter. So, our test is actually superfluous. R8G8B8Srgb would be more memory efficient alternative while less memory bandwidth might sometimes improve the performance despite that fact that it is not 4-byte aligned. R8G8B8Srgb format support is optional and we have to test for it.

R16G16B16A16Sfloat is half float based format. It can be used to do HDR rendering. Its support is mandatory, at least for color attachment (e.g. for rendering output), so our test is again superfluous. R16G16B16Srgb is optional. So, we would need to test for its support before use.

Other device query functions

Vulkan 1.0 supports few more query functions:

In the case of interest, they are described in Vulkan specification. Concerning vkEnumerateDeviceExtensionProperties(), we will deal with the topic of device extensions in the next article.