COMP5822M – Exercise 1.1 Vulkan Initialization
1 Project overview 1
2 Vulkan Setup (VkInstance) 2
3 Enumerating Vulkan Devices 4
Copyright By PowCoder代写 加微信 powcoder
4 Validation (Layers & Ext.) 9
5 Logical Device (VkDevice) 14
A Brief intro: Premake 17
This first exercise guides you through the initial steps of work- ing with Vulkan. Make sure you have the necessary software installed before continuing with the following. (See the Software Setup document for more information.)
First, you will receive a short overview of the exercise’s project structure. You will then start work with a simple Vulkan pro- gram that loads the Vulkan API, creates a Vulkan instance and examines Vulkan-capable devices on your machine. The exer- cise will then cover the setup necessary to enable standard vali- dation; this is a key step, as good diagnostics will aid Vulkan de- velopment tremendously. Finally, we’ll create a logical Vulkan device (VkDevice) handle from one of the found physical de- vices.
Aside from introducing you to the Vulkan API, this exercise serves as an early check to ensure that you have the neces- sary software and hardware to complete the practical work in COMP5822M.
Listing 1: Project layout. Download and unpack the exercise sources. Take a brief look around the project; you can find the rough
directory structure in Listing 1. The most important parts are:
exercise1 Source code for this exercise. This is where most of your work will take place in Exercise 1.
labutils Code shared across the exercises. Right now this includes a single set of files: to string.cpp.hpp. These define a few functions that help us convert Vulkan constants to strings, such that we can print the values in an easy-to-understand way. Later exercises will add more code to the “labutils”, and you will also end up implementing some of the functions there.
1 Project overview
Makefile & COMP5822M-exercise1.sln
Makefile & labutils.vcxproj*
to_string.{hpp,cpp}
premake5 & premake5.exe
premake5.lua
Makefile & exercise1.vcxproj*
third_party
premake5.lua
LICENSE.md & README.md
volk volk.h
LICENSE.txt & README.txt
… vulkan
(makefiles & VS projects)
third_party.md
COMP5822M – Exercise 1.1 2
third party This contains all the third party code that the exercises use. Right now that includes Volk (see below) and the official Vulkan headers. More third party code will be added as we go forward; you can always find brief summaries of and links to all third party code in the third party.md file.
premake* Premake is a meta build-system, not unlike CMake. Premake does not require any installation, which makes it convenient to distribute. The generated project files do not depend on Premake, so they can also be distributed easily.
*.sln, *.vcxproj* Visual Studio 2022 solution and project files (generated with Premake).
Makefile,*make Makefiles (generated with Premake).
Visual Studio 2022 solutions and Makefiles were generated with Premake and are included for your conve- nience. If you need a different set of project files (e.g., different version of Visual Studio or for one of the other IDEs that Premake supports), there are brief instructions in Appendix A.
Third party software
Exercise 1 relies on Volk to load the Vulkan API (see Lecture 2). Additionally, the exercises include a copy of the Vulkan headers.
is a meta Vulkan loader. The exercises use it to load the Vulkan API without linking against vulkan-1.{lib,dll} (Windows) or libvulkan.so (Linux). Instead, Volk locates the standard Vulkan loader at runtime and loads the Vulkan API from it on request. On one hand, this simplifies project setup (we don’t need to link against the Vulkan loader, and consequently do not need to find where it was installed), and on the other hand, we can detect if the Vulkan loader is missing and give an appropriate error message.
Volk includes the standard Vulkan headers internally (after setting a few options via #define). The Vulkan headers are therefore still required. Warning: You should always include
Vulkan Headers A copy of the Vulkan headers is included in the project to make things a bit easier – by distributing the headers with the project, their location is known. This makes writing the project definitions quite a bit easier (for example, the include paths are set statically in third party/premake5.lua via the includedirs() directive).
2 Vulkan Setup (VkInstance)
In order to work with Vulkan, we first need to create a Vulkan instance. Take a look at the main() function in exercise1/main.cpp. You can compile and run the program out-of-the-box. Do so. It will print something along the lines of
Vulkan loader version: 1.2.198 (variant 0)
and exit with an error condition.
The exact version depends on the version of the Vulkan loader that you have installed. (Devices can report a separate version; we’ll discuss what the various versions imply later.)
Starting at the top of the main() functions, the existing code initializes Volk with volkInitialize(). Volk follows the Vulkan API conventions by returning an return code of type VkResult. In volkInitialize(), Volk will try to locate the Vulkan loader DLL/SO and load a subset of the Vulkan API from it. This is the subset that we require to create a Vulkan instance.
One of loaded functions is vkEnumerateInstanceVersion. This function reports the version of the Vulkan loader. Vulkan version numbers are packed into a single 32-bit unsigned integer as documented in section
Section 40.2.1 for the Vulkan specifcation. To print it, we need to decode it with a set of macros.
Why require installation of the Vulkan SDK if the project includes a copy of the standard Vulkan headers and uses Volk to load the Vulkan API at runtime? We still want access to a few other components installed by the SDK. The validation layer that is used in this exercise is one example.
COMP5822M – Exercise 1.1 3
Originally, Vulkan did not have the variant field (VK_API_VERSION_VARIANT) in the version number; this was added in version 1.2.175. At that point, a new set of macros (VK_API_VERSION_*) were introduced to replace the original ones (VK_VERSION_*). You will likely still stumble across the latter in some code.
Next, the code attempts to create a Vulkan instance with create_instance(). Right now the implementation of create_instance() just returns VK_NULL_HANDLE. VK_NULL_HANDLE is similar to C++’s nullptr in that it indicates an invalid object. However, when working with Vulkan handles, you should use VK_NULL_HANDLE as using nullptr is not guaranteed to work/compile.
We will now implement create_instance(). The function is declared at the top of the main.cpp file and you can find its current definition just after the body of main().
Vulkan instances are created with the vkCreateInstance function. Look at the documentation. It takes three arguments:
• VkInstanceCreateInfo const* • VkAllocationCallbacks const* • VkInstance*
and returns a VkResult to indicate success/failure.
The last argument (VkInstance*) is a pointer to a VkInstance handle. On success, vkCreateInstance will
store the resulting instance in this handle.
The second argument ( VkAllocationCallbacks) allows us to assume control over how Vulkan allocates host memory. For most purposes, the system’s default allocator is more than sufficient, so we just pass nullptr to indicate that Vulkan should use the default allocator. Many of the Vulkan object construction functions have this VkAllocationCallbacks argument. The exercises will always set it to nullptr.
The first argument, VkInstanceCreateInfo, holds all the parameters that we can pass to the instance cre- ation. The structure is defined as follows (see documentation):
typedef struct VkInstanceCreateInfo {
Vulkan handles are implemented either as an opaque pointer type or a std::uint64_t value. The latter prevents the use of nullptr with Vulkan handles. (On 64-bit systems, all Vulkan handles are opaque pointer types, so using nullptr will work, but this is bad practice nevertheless.)
VkStructureType
const void*
VkInstanceCreateFlags
const VkApplicationInfo*
const char* const*
const char* const*
} VkInstanceCreateInfo;
pApplicationInfo;
enabledLayerCount;
ppEnabledLayerNames;
enabledExtensionCount;
ppEnabledExtensionNames;
The first two members, sType and pNext, are found in many of the Vulkan structures. When these two mem- bers are present, we need to initialize sType to the correct VkStructureType value. For now, we can set pNext to nullptr.
The flags field is currently unused, and should be set to zero.
pApplicationInfo, a pointer to VkApplicationInfo, lets the application provide information about itself to Vulkan.
As we will see a bit later, the pNext pointer is used in conjunction with extensions. The pNext can then be pointed to one or more additional Vulkan structures that provide further parameters to the function in question. (The additional Vulkan structure will also have the pNext member, which means that another structure can follow there. This creates a chain of additional strcutres, in the form of a singly linked list.)
The pNext pointer has type void const*. In order to know what type of structure pNext points to, Vulkan can inspect the sType field which is always the first member of such structures. Similarly, pNext is always the second member, meaning that unknown structures could even be skipped without breaking the chain.
COMP5822M – Exercise 1.1 4
The last four fields (enabledLayerCount, …) are used to load Vulkan layers and to enable instance extensions. We will return to them a bit later in this exercise (Section 4). For now, we do not need them and can just set them to zero and nullptr, respectively.
We will start with defining the VkApplicationInfo. Take a look at the documentation. Declare a new VkApplicationInfo structure at the top of create_instance() and fill in the parameters, for example, as follows:
VkInstance create_instance() 1 {2 v// Most of the VkApplicationInfo fields we can choose freely. The only 3 v// ”important” field is the .apiVersion, which specifies the highest 4 v// version of Vulkan that the application is designed to use. 5 v6 vVkApplicationInfo appInfo{}; 7
vappInfo.sType
vappInfo.pApplicationName
vappInfo.applicationVersion
vappInfo.apiVersion
= VK_STRUCTURE_TYPE_APPLICATION_INFO; 8 = “COMP5822-exercise1”; 9 = 2021; // academic year of 2021/22 10 = VK_MAKE_API_VERSION( 0, 1, 3, 0 ); // Version 1.3 11
The appInfo structure is zero-initialized thanks to the pair or braces {}, which sets all fields in the struc- ture to zero/nullptr. We then just specify values for a subset of the fields, leaving e.g. pEngineName and engineVersion zeroed out.
Next, directly following, we define the VkInstanceCreateInfo structure:
We set the mandatory sType and the pointer to the VkApplicationInfo that we just created. The other fields are not required at the moment and we can leave them zeroed out.
Now we are ready to call vkCreateInstance:
Do not forget to change the final return statement of the create_instance() function to return the newly created instance instead of VK_NULL_HANDLE!
Many Vulkan functions return a VkResult return value. It is good practice to check it for errors. In this case, if the value is not VK_SUCCESS, we print an error message, and return VK_NULL_HANDLE (which then causes the program to exit with an error condition).
Back in main(), the program will call volkLoadInstance() with our new Vulkan instance. At this point, Volk loads the remainder of the Vulkan API (it uses a Vulkan function, vkGetInstanceProcAddr, to do so – this function requires a valid instance handle).
We now have access to the full Vulkan API.
3 Enumerating Vulkan Devices
Vulkan is designed with systems that have multiple devices (GPUs) in mind. We can list devices that Vulkan knows of (i.e., that have a properly installed Vulkan driver), inspect these, and then select one or more. While
vVkInstanceCreateInfo instanceInfo{}; 1 vinstanceInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; 2 vinstanceInfo.pApplicationInfo = &appInfo; 3
vVkInstance instance = VK_NULL_HANDLE; 1 2 vif( auto const res = vkCreateInstance( &instanceInfo, nullptr, &instance ); ▽ 3
◃ VK_SUCCESS != res )
v{ 4 v vstd::fprintf( stderr, “Error: unable to create Vulkan instance\n” ); 5 v vstd::fprintf( stderr, “vkCreateInstance(): %s\n”, lut::to_string(res).c_str() ); 6 v vreturn VK_NULL_HANDLE; 7 v} 8
Like many Vulkan objects, we are required to destroy the Vulkan instance when we no longer need it. The code already does this – look for vkDestroyInstance in main.cpp. However, unlike C++, where it is legal to delete a nullptr-pointer, Vulkan does not allow passing of VK_NULL_HANDLE to its destructors.
COMP5822M – Exercise 1.1 5
Vulkan uses the term device, this really refers to a Vulkan implementation. A software implementation would therefore also show up as a device.
Vulkan distinguishes between physical devices ( VkPhysicalDevice) and logical devices ( VkDevice). Physical devices represent a single Vulkan implementation. When we enumerate available devices, these will be iden- tified by a VkPhysicalDevice handle. Through the handle, we can query Vulkan for information about the underlying device/implementation. We then select one (or more) devices to work with, and create a logical device instance of the selected device. The logical device represents an instance of the selected Vulkan imple- mentation, which encapsulates the implementation’s state and resources. Each logical device is independent from other logical devices.
In the main() method, you will find two calls: • enumerate_devices()
• select_device().
We will start with the first one, enumerate_devices(). The goal is to query Vulkan for available devices, and
print a short list of these along with some of their properties.
Listing Vulkan devices and properties
Jump to the definition of enumerate_devices(). Right now it does not do much. We will change it such that it first queries Vulkan for a list of physical devices. Second, it should iterate over the devices, querying Vulkan for information about each.
To query available physical devices, Vulkan provides vkEnumeratePhysicalDevice. We will need to call it twice – first to get the number of physical devices, and second to get a list of physical device handles.
In the first of the two calls, we pass a pointer to a std::uint32_t in the second argument of the function vkEnumeratePhysicalDevices and set the third argument to nullptr. Vulkan will then set the std::▽ ◃ uint32_t to the number of devices. In the second call, we set both arguments – the second argument remains the same, but the third one now points at an array of VkPhysicalDevice handles with the length returned by the first call (the example below uses std::vector to allocate this array). The code could look something like the following:
It is possible to create multiple logical device instances, each representing a different GPU. We use the logical device handle (VkDevice) to identify the device that we want to address with our Vulkan calls. It is even possible to create multiple logical device instances from a single physical device, if necessary. Finally, in some cases, multiple physical devices can form a device group, which can then be treated as a single logical device. Throughout these exercises, we will be working with a single logical device, though, and this is by far the most common setting.
This is a somewhat common pattern in Vulkan, and we will see it again later in this exercise. This way, the user is wholly responsible for providing (allocating) the memory in which the list is stored, and Vulkan does not have to concern itself with this.
vstd::uint32_t numDevices = 0; 1 vif( auto const res = vkEnumeratePhysicalDevices( aInstance, &numDevices, nullptr ); ▽2
◃ VK_SUCCESS != res )
v vstd::fprintf( stderr, “Error: unable to get phyiscal device count\n” ); 4 v vstd::fprintf( stderr, “vkEnumeratePhysicalDevices() returned error %s\n”, lut::▽ 5
◃ to_string(res).c_str() );
v vreturn; 6
8 vstd::vector
vif( auto const res = vkEnumeratePhysicalDevices( aInstance, &numDevices, devices.▽ 10 ◃ data() ); VK_SUCCESS != res )
v vstd::fprintf( stderr, “Error: unable to get phyiscal device list\n” ); 12 v vstd::fprintf( stderr, “vkEnumeratePhysicalDevices() returned error %s\n”, lut::▽ 13
◃ to_string(res).c_str() );
v vreturn; 14
COMP5822M – Exercise 1.1 6
Unlike most other Vulkan handles, we are not required/allowed to destroy the VkPhysicalDevice handles returned by vkEnumeratePhysicalDevices.
Next, we iterate over the physical devices:
We can query information about each physical device with the device handle (type VkPhysicalDevice). We shall start with some of the device’s properties, via
• vkGetPhysicalDeviceProperties and • VkPhysicalDeviceProperties.
Take a look at the documentation – in particular, follow links through to VkPhysicalDeviceLimits and briefly scroll through the documentation for it.
For now, we will print the device name (.deviceName), device type (.deviceType), and the two versions. See documentation of VkPhysicalDeviceType for a list of possible device types. The to string.{hpp,cpp} has a few ready-made helper methods to convert some of the values to strings. Check the sources for additional information.
vstd::printf( “Found %zu devices:\n”, devices.size() ); 1 vfor( auto const device : devices ) 2
v v//TODO: more code here, see below. 4
v v// Retrieve basic information (properties) about this device 1 v vVkPhysicalDeviceProperties props; 2 v vvkGetPhysicalDeviceProperties( device, &props ); 3
4 v vauto const versionMajor = VK_API_VERSION_MAJOR(props.apiVersion); 5 v vauto const versionMinor = VK_API_VERSION_MINOR(props.apiVersion); 6 v vauto const versionPatch = VK_API_VERSION_PATCH(props.apiVersion); 7 8 v v// Note: while the driver version is stored in a std::uint32 t like 9
v v// the Vulkan API version, the encoding of the driver version is not 10 v v// standardized. (Some vendors stick to the Vulkan version coding, 11 v v// others do not.) 12 v vstd::printf( “- %s (Vulkan: %d.%d.%d, Driver: %s)\n”, props.deviceName, ▽ 13 ◃ versionMajor, versionMinor, versionPatch, lut::driver_version(props.vendorID,▽
◃ props.driverVersion).c_str() );
v vstd::printf( ” – Type: %s\n”, lut::to_string(props.deviceType).c_str() ); 14
Each physical device provides its own Vulkan API version via the .apiVersion field – this is in addition to the Vulkan loader version that we queried and printed early on. The latter refers to the version of the Vulkan loader, whereas the per-device versions tell us which version of Vulkan the device’s implementation/driver supports. These two versions do not have to match. Different devices can also report different versions.
For example, the loader can report a higher Vulkan version than the device. In this case, we can only use functions from the lower version, as reported by the device’s driver. The reverse can also occur (but is more rare, since drivers typically distribute a matching Vulkan loader) – however, in this case we can use the higher version API under some conditions (but some features, such as Vulkan layers, may not not work). Generally, assume that you can use Vulkan features up to whichever version is lower: the one reported for the device or the one reported by
程序代写 CS代考 加微信: powcoder QQ: 1823890830 Email: powcoder@163.com