Vulkan Tutorial

1-5 - Logical Device

Logical device is needed to perform the real work in Vulkan. The real work is, for example, computations or graphics rendering. In the previous articles we covered Vulkan instance, physical devices, and the ways in which we can get their properties, features, list of extensions, and other things out of them. The next step is logical device.

A logical device can be created using the following code:

// create device
vk::initDevice(
	pd,  // physicalDevice
	vk::DeviceCreateInfo{  // pCreateInfo
		.flags = {},
		.queueCreateInfoCount = 1,  // at least one queue is mandatory
		.pQueueCreateInfos = array<vk::DeviceQueueCreateInfo,1>{
			vk::DeviceQueueCreateInfo{
				.flags = {},
				.queueFamilyIndex = 0,
				.queueCount = 1,
				.pQueuePriorities = &(const float&)1.f,
			}
		}.data(),
		.enabledLayerCount = 0,  // no enabled layers
		.ppEnabledLayerNames = nullptr,
		.enabledExtensionCount = 0,  // no enabled extensions
		.ppEnabledExtensionNames = nullptr,
		.pEnabledFeatures = nullptr,  // no enabled features
	}
);

As can be seen, the logical device is created out of the physical device. Additionally, we provide the list of the required queues, the list of enabled layers and the list of extensions, and pointer to the structure with enabled features.

Logical and physical devices

So, why we have logical and physical devices and not just devices? One reason is that we can combine several physical devices to create one logical device with increased performance. Another reason is resource optimizations. We first query for capabilites, and then create logical device exactly as we need it. Thus, logical device is not full blown device, but optimized for our needs. In other words, having logical and physical device separated, the driver and our application might be more efficient.

We can depict Vulkan architecture in the following way:

The applications are doing their Vulkan calls. These are going through the Vulkan loader. The loader forwards them to the appropriate Vulkan drivers. The drivers, then, control physical devices. One driver can controls one physical device, or it might control more of them. Especially, if there are more physical devices from the same vendor, they sometimes use the same driver. There are also drivers that are not directly connected with any physical device. One example is "CPU" drivers that are doing software rendering.

Function pointers

When the call is going through the Vulkan loader, it uses trampoline to direct the call to particular Vulkan driver. The overhead is usually negligible. Anyway, to avoid it or to gain additional flexibility, Vulkan provides vkGetDeviceProcAddr() function to query function pointer for particular logical device. Thus, we can directly call the driver functions as shown on the following image by the yellow arrow:

We can even print the name of library where particular Vulkan function is implemented. It is demonstrated in the code of this article:

// device function pointers
cout << "Device function pointers for " << properties.deviceName << ":" << endl;
cout << "   vkCreateShaderModule() points to: " << getLibraryOfAddr(vk::getDeviceProcAddr("vkCreateShaderModule")) << endl;
cout << "   vkQueueSubmit()        points to: " << getLibraryOfAddr(vk::getDeviceProcAddr("vkQueueSubmit")) << endl;

When run, the output similar to the following one should appear:

Instance function pointers:
   vkCreateInstance()     points to: /usr/lib/x86_64-linux-gnu/libvulkan.so.1
   vkCreateShaderModule() points to: /usr/lib/x86_64-linux-gnu/libvulkan.so.1
   vkQueueSubmit()        points to: /usr/lib/x86_64-linux-gnu/libvulkan.so.1
Device function pointers:
   Quadro K1000M
      vkCreateShaderModule() points to: /usr/lib/x86_64-linux-gnu/libnvidia-glcore.so.460.73.01
      vkQueueSubmit()        points to: /usr/lib/x86_64-linux-gnu/libGLX_nvidia.so.0

This is the result from Linux and Nvidia Quadro K1000M. Windows 7 with three different cards is slightly different:

Instance function pointers:
   vkCreateInstance()     points to: C:\Windows\system32\vulkan-1.dll
   vkCreateShaderModule() points to: C:\Windows\system32\vulkan-1.dll
   vkQueueSubmit()        points to: C:\Windows\system32\vulkan-1.dll
Device function pointers:
   GeForce GTX 1050
      vkCreateShaderModule() points to: C:\Windows\system32\nvoglv64.dll
      vkQueueSubmit()        points to: C:\Windows\system32\nvoglv64.dll
   Radeon(TM) RX 460 Graphics
      vkCreateShaderModule() points to: C:\Windows\System32\amdvlk64.dll
      vkQueueSubmit()        points to: C:\Windows\System32\amdvlk64.dll
   Intel(R) HD Graphics 530
      vkCreateShaderModule() points to: C:\Windows\system32\igvk64.dll
      vkQueueSubmit()        points to: C:\Windows\system32\igvk64.dll

So, the Vulkan loader is implemented by libvulkan.so.1 on Linux and vulkan-1.dll on Windows. The names of the drivers can be seen in the rest of the output.