Initial Vulkan Setup#
Vulkan itself as an API requires preliminary configuration so it can function in an application. This section covers these basics, including validation layers.
There are three critical objects for any Vulkan application, regardless of purpose. These are:
The instance.
The physical device.
The logical device and associated command queues.
Together, they link the application, the Vulkan library, and the GPU. In particular, the instance is what allows the application to use Vulkan functions at all.
Setting up the Validation Layers#
Part of what makes Vulkan so performance-geared is the fact that it does not do any validation at run-time, thus keeping driver overhead to a minimum. The downside to this may be that debugging the application may prove different to what is normally expected. To help with this, Vulkan is able to use validation layers that can be enabled during the development process.
Layers are a concept that Vulkan implements to handle various different systems that need to access the API or slide themselves in between the application in order to handle tasks such as video capture or debugging. Usually, to achieve these functionalities, they need to be accounted for by the platform-holder, or utilise platform-specific methods to intercept the data. Vulkan provides a system for handling different add-ins into a single interface that the application can communicate with.
Validation layers are very useful for development, as they can ensure the API is being used correctly. Vulkan assumes the application is faultless and is using the API correctly, so the validation layers are not enabled by default. It is worth noting that validation layers are intended to be enabled during development to assist with writing correct code, but should then removed for production versions as they cause performance penalty.
The LunarG Vulkan SDK provides a standard suite of validation layers for standard error checking. Which layers are enabled are up to the developer. Usually the most useful layer is VK_LAYER_KHRONOS_validation
; it offers features such as printing API calls, tracking objects, and validating the use of various Vulkan functions and checks that only supported features and formats are used on a device.
In the example code, there is a method called initLayers()
that performs this task. View in context.
std::vector<std::string> VulkanHelloAPI::initLayers()
{
// This vector will store the supported instance layers that will be returned.
std::vector<std::string> layerNames;
// This ensures validation layers will only be enabled during
// debugging, reducing the overhead of the final release version.
#ifdef PVR_DEBUG
// Create a vector to hold the layer properties.
std::vector<VkLayerProperties> outLayers;
uint32_t numItems = 0;
// Enumerate all the layer properties to find the total number of items to add to the vector created above.
debugAssertFunctionResult(vk::EnumerateInstanceLayerProperties(&numItems, nullptr), "Fetching Layer count");
// Resize the vector to hold the result from vk::EnumerateInstanceLayerProperties.
outLayers.resize(numItems);
// Enumerate once more, this time pass the vector and fetch the layer properties themselves to store them in the vector.
debugAssertFunctionResult(vk::EnumerateInstanceLayerProperties(&numItems, outLayers.data()), "Fetching Layer Data");
// Log the supported layers on this system.
Log(false, "---------- Supported Layers ----------");
for (auto&& layer : outLayers) { Log(false, ">> %s", layer.layerName); }
Log(false, "--------------------------------------");
layerNames = filterLayers(outLayers, InstanceLayers, NumInstanceLayers);
bool requestedStdValidation = false;
bool supportsStdValidation = false;
bool supportsKhronosValidation = false;
uint32_t stdValidationRequiredIndex = -1;
for (const auto& InstanceLayer : InstanceLayers)
{
if (!strcmp(InstanceLayer.c_str(), "VK_LAYER_LUNARG_standard_validation"))
{
requestedStdValidation = true;
break;
}
}
// This code is to cover cases where VK_LAYER_LUNARG_standard_validation is requested but is not supported. This is where on some platforms the
// component layers enabled via VK_LAYER_LUNARG_standard_validation may still be supported, even though VK_LAYER_LUNARG_standard_validation itself is not.
if (requestedStdValidation)
for (const auto& SupportedInstanceLayer : layerNames)
{
if (!strcmp(SupportedInstanceLayer.c_str(), "VK_LAYER_LUNARG_standard_validation")) { supportsStdValidation = true; }
if (!strcmp(SupportedInstanceLayer.c_str(), "VK_LAYER_KHRONOS_validation")) { supportsKhronosValidation = true; }
}
// This code is to cover cases where VK_LAYER_LUNARG_standard_validation is requested but is not supported, where on some platforms the
// component layers enabled via VK_LAYER_LUNARG_standard_validation may still be supported even though VK_LAYER_LUNARG_standard_validation is not.
// Only perform the expansion if VK_LAYER_LUNARG_standard_validation is requested and not supported and the newer equivalent layer VK_LAYER_KHRONOS_validation is also not supported
if (requestedStdValidation && !supportsStdValidation && !supportsKhronosValidation)
{
for (auto it = outLayers.begin(); !supportsStdValidation && it != outLayers.end(); ++it)
{ supportsStdValidation = !strcmp(it->layerName, "VK_LAYER_LUNARG_standard_validation"); }
if (!supportsStdValidation)
{
for (uint32_t i = 0; stdValidationRequiredIndex == static_cast<uint32_t>(-1) && i < outLayers.size(); ++i)
{
if (!strcmp(InstanceLayers[i].c_str(), "VK_LAYER_LUNARG_standard_validation")) { stdValidationRequiredIndex = i; }
}
for (uint32_t j = 0; j < NumInstanceLayers; ++j)
{
if (stdValidationRequiredIndex == j && !supportsStdValidation)
{
const char* stdValComponents[] = { "VK_LAYER_GOOGLE_threading", "VK_LAYER_LUNARG_parameter_validation", "VK_LAYER_LUNARG_object_tracker",
"VK_LAYER_LUNARG_core_validation", "VK_LAYER_GOOGLE_unique_objects" };
for (auto& stdValComponent : stdValComponents)
{
for (auto& outLayer : outLayers)
{
if (!strcmp(stdValComponent, outLayer.layerName))
{
layerNames.emplace_back(stdValComponent);
break;
}
}
}
}
}
// Filter the layers again. This time checking for support for the component layers enabled via VK_LAYER_LUNARG_standard_validation.
layerNames = filterLayers(outLayers, layerNames.data(), static_cast<uint32_t>(layerNames.size()));
}
}
Log(false, "---------- Supported Layers to be enabled ----------");
for (auto&& layer : layerNames) { Log(false, ">> %s", layer.c_str()); }
Log(false, "--------------------------------------");
#endif
return layerNames;
}
Selecting the Extensions#
Extensions provide additional functionality to the Vulkan API beyond the core specification. They are divided into two categories:
- Device-level extensions
Functionality from these extensions affects device-level objects such as logical devices, queues, and command buffers.
- Instance-level extensions
Functionality from these extensions affects instance-level objects such as physical devices.
Vulkan does not make any assumptions about the type of application under development or its purposes, and so some key functionality of graphics applications can be found in extensions. In particular, the two vital extensions for any Vulkan graphics application are:
VK_KHR_surface
This is an instance-level extension that provides functionality related to surface objects, where images are presented
VK_KHR_swapchain
This is a device-level extension that provides functionality related to the swapchain, which manages the application’s set of images and controls how they are presented to the surface.
In the example code, there are two methods that can handle these two extensions respectively: initInstanceExtensions()
and initDeviceExtensions()
. This application requires the two core extensions mentioned above, as well as a third, platform-specific extension that will be discussed. Both methods follow the same structure, so only the code for initInstanceExtensions()
will be shown. View in context.
std::vector<std::string> VulkanHelloAPI::initInstanceExtensions()
{
// Concept: Extensions
// Extensions extend the API's functionality; they may add additional features or commands. They can be used for a variety of purposes,
// such as providing compatibility for specific hardware. Instance-level extensions are extensions with global-functionality; they affect
// both the instance-level and device-level commands. Device-level extensions specifically affect the device they are bound to.
// Surface and swapchain functionality are both found in extensions as Vulkan does not make assumptions about the type of application, as not all applications are graphical;
// for example - compute applications. For this reason they are both considered extensions that add functionality to the core API. The surface extension is an instance-level
// extension and is added to the instanceExtensionNames vector, while the swapchain is a device-level one and is added to deviceExtensionNames.
// This function selects the two instance-level extensions which are required by this application.
// This vector will store a list of supported instance extensions that will be returned. The general surface extension is added to this vector first.
std::vector<std::string> extensionNames;
extensionNames.emplace_back(VK_KHR_SURFACE_EXTENSION_NAME);
// An additional surface extension needs to be loaded. This extension is platform-specific so needs to be selected based on the
// platform the example is going to be deployed to.
// Preprocessor directives are used here to select the correct platform.
#ifdef VK_USE_PLATFORM_WIN32_KHR
extensionNames.emplace_back(VK_KHR_WIN32_SURFACE_EXTENSION_NAME);
#endif
#ifdef VK_USE_PLATFORM_XLIB_KHR
extensionNames.emplace_back(VK_KHR_XLIB_SURFACE_EXTENSION_NAME);
#endif
#ifdef VK_USE_PLATFORM_XCB_KHR
extensionNames.emplace_back(VK_KHR_XCB_SURFACE_EXTENSION_NAME);
#endif
#ifdef VK_USE_PLATFORM_ANDROID_KHR
extensionNames.emplace_back(VK_KHR_ANDROID_SURFACE_EXTENSION_NAME);
#endif
#ifdef VK_USE_PLATFORM_WAYLAND_KHR
extensionNames.emplace_back(VK_KHR_WAYLAND_SURFACE_EXTENSION_NAME);
#endif
#ifdef VK_USE_PLATFORM_MACOS_MVK
extensionNames.emplace_back(VK_MVK_MACOS_SURFACE_EXTENSION_NAME);
#endif
#ifdef USE_PLATFORM_NULLWS
extensionNames.emplace_back(VK_KHR_DISPLAY_EXTENSION_NAME);
#endif
return extensionNames;
}
initDeviceExtensions() is exactly the same, except we enable VK_KHR_SWAPCHAIN_EXTENSION_NAME
only before we return.
Creating an Instance#
An instance provides the link between the application and the Vulkan API. By loading in the function pointers to Vulkan commands, this allows the application be able to use the Vulkan library functions. The instance also stores all of the application-specific state. All other application-time objects are created from the instance.
Creating the instance requires information about the layers and extensions, which is why this was done first. Here the use of CreateInfo structs can be seen in action. In the example code, this is contained in initApplicationInstance()
. View in context.
void VulkanHelloAPI::initApplicationAndInstance(std::vector<std::string>& extensionNames, std::vector<std::string>& layerNames)
{
// Declare and populate the application info.
// When creating objects in Vulkan using "vkCreate..." functions, a creation struct must be defined. This struct contains information describing the properties of the
// object which is going to be created. In this case, applicationInfo contains properties such as the chosen name of the application and the version of Vulkan used.
VkApplicationInfo applicationInfo = {};
applicationInfo.pNext = nullptr;
applicationInfo.pApplicationName = "Vulkan Hello API Sample";
applicationInfo.applicationVersion = 1;
applicationInfo.engineVersion = 1;
applicationInfo.pEngineName = "Vulkan Hello API Sample";
applicationInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
applicationInfo.apiVersion = VK_API_VERSION_1_0;
// Declare an instance creation info struct.
// instanceInfo specifies the parameters of a newly created Vulkan instance. The
// application info struct populated above is referenced here along with the instance layers and extensions.
VkInstanceCreateInfo instanceInfo = {};
instanceInfo.pNext = nullptr;
instanceInfo.flags = 0;
instanceInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
instanceInfo.pApplicationInfo = &applicationInfo;
// Assign the number and names of the instance layers to be enabled.
appManager.instanceLayerNames.resize(layerNames.size());
for (uint32_t i = 0; i < layerNames.size(); ++i) { appManager.instanceLayerNames[i] = layerNames[i].c_str(); }
instanceInfo.enabledLayerCount = static_cast<uint32_t>(appManager.instanceLayerNames.size());
instanceInfo.ppEnabledLayerNames = appManager.instanceLayerNames.data();
// Assign the number and names of the instance extensions to be enabled.
appManager.instanceExtensionNames.resize(extensionNames.size());
for (uint32_t i = 0; i < extensionNames.size(); ++i) { appManager.instanceExtensionNames[i] = extensionNames[i].c_str(); }
instanceInfo.enabledExtensionCount = static_cast<uint32_t>(appManager.instanceExtensionNames.size());
instanceInfo.ppEnabledExtensionNames = appManager.instanceExtensionNames.data();
// Create a Vulkan application instance using the instanceInfo struct defined above.
// The handle to this new instance is stored in appManager.instance for access elsewhere.
debugAssertFunctionResult(vk::CreateInstance(&instanceInfo, nullptr, &appManager.instance), "Create Instance");
// The pointers to the functions which depend on the Vulkan instance need to be initialised. GetInstanceProcAddr is used to find the correct function
// pointer associated with this instance. This is not necessary but it is a best practice. It provides a way to bypass the Vulkan loader and grants a
// small performance boost.
if (!vk::initVulkanInstance(appManager.instance)) { Log(true, "Could not initialise the instance function pointers."); }
}