You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Vulkan is a low-overhead, cross-platform 3D graphics and compute API.
Vulkan targets
Vulkan targets high-performance realtime 3D graphics applications such as
games and interactive media across multiple platforms providing higher
performance and lower CPU usage.
Tutorial Structure
These tutorials assume you have the Vulkan SDK installed and a working
Vulkan driver.
There is no global state in Vulkan; all application state is stored in
a vkInstance object. Creating a vkInstance object initializes the
Vulkan library and allows application to pass information about itself
to the implementation.
To create an instance we also need a vkInstanceCreateInfo object
controlling the creation of the instance and a vkAllocationCallback to
control host memory allocation for the instance. For now we will ignore
vkAllocationCallback and use NULL which will use the system-wide allocator.
More on vkAllocationCallback later.
vkApplication applicationInfo;
vkInstanceCreateInfo instanceInfo;
vkInstance instance;
// Filling out application description:// sType is mandatory
applicationInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
// pNext is mandatory
applicationInfo.pNext = NULL;
// The name of our application
applicationInfo.pApplicationName = "Tutorial 1";
// The name of the engine (e.g: Game engine name)
applicationInfo.pEngineName = NULL;
// The version of the engine
applicationInfo.engineVersion = 1;
// The version of Vulkan we're using for this application
applicationInfo.apiVersion = VK_API_VERSION;
// Filling out instance description:// sType is mandatory
instanceInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
// pNext is mandatory
instanceInfo.pNext = NULL;
// flags is mandatory
instanceInfo.flags = 0;
// The application info structure is then passed through the instance
instanceInfo.pApplicationInfo = &applicationInfo;
// Don't enable and layer
instanceInfo.enabledLayerCount = 0;
instanceInfo.ppEnabledLayerNames = NULL;
// Don't enable any extensions
instanceInfo.enabledExtensionCount = 0;
instanceInfo.ppEnabledExtensionNames = NULL;
// Now create the desired instance
vkResult result = vkCreateInstance(&instanceInfo, NULL, &instance);
if (result != VK_SUCCESS) {
fprintf(stderr, "Failed to create instance: %d\n", result);
abort();
}
// To Come Later// ...// ...// Never forget to free resourcesvkDestroyInstance(instance, NULL);
sType is used to describe the type of the structure. It must be filled
out in every structure. pNext must be filled out too. The idea behind
pNext is to store pointers to extension-specific structures. Valid usage
currently is to assign it a value of NULL. The same goes for flags.
In the first chunk of code we setup an application description structure
which will be a required component for our instance info structure. In Vulkan
you're expected to describe what your application is, which engine it uses
(or NULL.) This is useful information for Vulkan to have as driver vendors
may want to apply engine or game specific features/fixes to the application
code. Traditionally this sort of technique was supported in much more
complicated and unsafe manners. Vulkan addresses the problem by requiring
upfront information.
The second part creates an instance description structure which will
be used to actually initialize an instance. This is where you'd request
extensions or layers. Extensions work in the same way as GL did extensions,
nothing has changed here and it should be familiar. A layer is a new
concept Vulkan has introduced. Layers are techniques you can enable that
insert themselves into the call chain for Vulkan commands the layer is
inserted in. They can be used to validate application behavior during
development. Think of them as decorators to commands. In our example
we don't bother with extensions or layers, but they must be filled out.
From here it's as trivial as calling vkCreateInstance to create an
instance. On success this function will return VK_SUCCESS. When we're
done with our instance we destroy it with vkDestroyInstance.
Now that we have an instance we need to a way to associate the instance
with the hardware. In Vulkan there is no notion of a singular GPU, instead
you enumerate physical devices and choose. This allows you to use multiple
physical devices at the same time for rendering or compute.
The vkEnumeratePhysicalDevices function allows you to both query the
count of physical devices present on the system and fill out an array of
vkPhysicalDevice structures representing the physical devices.
// Query how many devices are present in the systemuint32_t deviceCount = 0;
VkResult result = vkEnumeratePhysicalDevices(instance, &deviceCount, NULL);
if (result != VK_SUCCESS) {
fprintf(stderr, "Failed to query the number of physical devices present: %d\n", result);
abort();
}
// There has to be at least one device presentif (deviceCount == 0) {
fprintf(stderr, "Couldn't detect any device present with Vulkan support: %d\n", result);
abort();
}
// Get the physical devices
vector<VkPhysicalDevice> physicalDevices(deviceCount);
result = vkEnumeratePhysicalDevices(instance, &deviceCount, &physicalDevices[0]);
if (result != VK_SUCCESS) {
fprintf(stderr, "Faied to enumerate physical devices present: %d\n", result);
abort();
}
Once we have a physical device; we can fetch the properties of that
physical device using vkGetPhysicalDeviceProperties which will
fill out a vkPhysicalDeviceProperties structure.
In Vulkan the API version is encoded as a 32-bit integer with the major
and minor version being encoded into bits 31-22 and 21-12 respectively
(for 10 bits each.); the final 12-bits encode the patch version number.
These handy macros should help with fetching some human readable digits
from the encoded API integer.
Queues in Vulkan provide an interface to the execution engine of a device.
Commands are recorded into command buffers ahead of execution time. These
same buffers are then submitted to queues for execution. Each physical
devices provides a family of queues to choose from. The choice of the queue
depends on the task at hand.
A Vulkan queue can support one or more of the following operations
(in order of most common):
graphic VK_QUEUE_GRAPHICS_BIT
compute VK_QUEUE_COMPUTE_BIT
transfer VK_QUEUE_TRANSFER_BIT
sparse memory VK_QUEUE_SPARSE_BINDING_BIT
This is encoded in the queueFlags field of the VkQueueFamilyProperties
structures filled out by vkGetPhysicalDeviceQueueFamilyProperties. Which,
like vkEnumeratePhysicalDevices can also be used to query the count of
available queue families.
While the queue support bits are pretty straight forward; something must
be said about VK_QUEUE_SPARSE_BINDING_BIT. If this bit is set it indicates
that the queue family supports sparse memory management operations. Which
means you can submit operations that operate on sparse resources. If this
bit is not present, submitting operations with sparse resource is undefined.
Sparse resources will be covered in later tutorials as they are an
advanced topic.
uint32_t queueFamilyCount = 0;
vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyCount, NULL);
vector<VkQueueFamilyProperties> familyProperties(queueFamilyCount);
vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyCount &damilyProperties[0]);
// Print the familiesfor (uint32_t i = 0; i < deviceCount; i++) {
for (uint32_t j = 0; j < queueFamilyCount; j++) {
printf("Count of Queues: %d\n", familyProperties[j].queueCount);
printf("Supported operationg on this queue:\n");
if (familyProperties[j].queueFlags & VK_QUEUE_GRAPHICS_BIT)
printf("\t\t Graphics\n");
if (familyProperties[j].queueFlags & VK_QUEUE_COMPUTE_BIT)
printf("\t\t Compute\n");
if (familyProperties[j].queueFlags & VK_QUEUE_TRANSFER_BIT)
printf("\t\t Transfer\n");
if (familyProperties[j].queueFlags & VK_QUEUE_SPARSE_BINDING_BIT)
printf("\t\t Sparse Binding\n");
}
}
So far we have the ability to get the physical devices present on the
system, create an instance and query the queue families supported by the
physical devices. Vulkan does not operate directly on a VkPhysicalDevice.
Instead it operates on views of a VkPhysicalDevice which it represents
as a VkDevice and calls a logical device. This additional layer of
abstraction is what allows us to tie together everything into an abstract,
usable context.
Like the other structures we filled out previously, sType, pNext and
flags are mandatory here.
VkDeviceCreateInfo deviceInfo;
// Mandatory fields
deviceInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
deviceInfo.pNext = NULL;
deviceInfo.flags = 0;
// We won't bother with extensions or layers
deviceInfo.enabledLayerCount = 0;
deviceInfo.ppEnabledLayerNames = NULL;
deviceInfo.enabledExtensionCount = 0;
deviceInfo.ppEnabledExtensionNames = NULL;
// Here's where we initialize our queues
VkDeviceQueueCreateInfo deviceQueueInfo;
deviceQueueInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
deviceQueueInfo.pNext = NULL;
deviceQueueInfo.flags = 0;
// Use the first queue family in the family list
deviceQueueInfo.queueFamilyIndex = 0;
// Create only one queuefloat queuePriorities[] = { 1.0f };
deviceQueueInfo.queueCount = 1;
deviceQueueInfo.pQueuePriorities = queuePriorities;
// Set queue(s) into the device
deviceInfo.queueCreateInfoCount = 1;
deviceInfo.pQueueCreateInfos = &deviceQueueInfo;
result = vkCreateDevice(physicalDevice, &deviceInfo, NULL, device);
if (result != VK_SUCCESS) {
fprintf(stderr, "Failed creating logical device: %d\n", result);
abort();
}
You can create many instances of the same queue family and set multiple
queues into a VkDeviceCreateInfo structure. Just be sure to set the
queueCount correctly. In Vulkan you can control the priority of each queue
with an array of normalized floats. A value of 1 has highest priority.
With that you should have a logical device setup from a physical device
with your associated queues containing your application-provided information.
From here we may now create the appropriate swap chains and begin rendering.
What we have now is sufficient enough to create a swap chain with and
begin rendering, for Vulkan does not require a surface be present
to render into. Chances are you want to get something on screen; so to
do that we need to make a surface. If you are interested in doing window-less
rendering this tutorial may be skipped.
Creating a surface is platform-specific: think WGL, AGL, GLX, etc. However
the amount of platform-specific code has gone down tremendously in Vulkan;
which now provides some easy to use extensions.
We must now go back to when we created our application info structure
and add the appropriate extensions:
Now we can begin to use the extensions to get a surface to render into.
VkSurfaceKHR surface;
#if defined(_WIN32)
VkWin32SurfaceCreateInfoKHR surfaceCreateInfo;
surfaceCreateInfo.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR;
surfaceCreateInfo.hinstance = (HINSTANCE)platformHandle; // provided by the platform code
surfaceCreateInfo.hwnd = (HWND)platformWindow; // provided by the platform code
VkResult result = vkCreateWin32SurfaceKHR(instance, &surfaceCreateInfo, NULL, &surface);
#elif defined(__ANDROID__)
VkAndroidSurfaceCreateInfoKHR surfaceCreateInfo;
surfaceCreateInfo.sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR;
surfaceCreateInfo.window = window; // provided by the platform code
VkResult result = vkCreateAndroidSurfaceKHR(instance, &surfaceCreateInfo, NULL, &surface);
#else
VkXcbSurfaceCreateInfoKHR surfaceCreateInfo;
surfaceCreateInfo.sType = VK_STRUCTURE_TYPE_XCB_SURFACE_CREATE_INFO_KHR;
surfaceCreateInfo.connection = connection; // provided by the platform code
surfaceCreateInfo.window = window; // provided by the platform code
VkResult result = vkCreateXcbSurfaceKHR(instance, &surfaceCreateInfo, NULL, &surface);
#endifif (result != VK_SUCCESS) {
fprintf(stderr, "Failed to create Vulkan surface: %d\n", result);
abort();
}
Once we have the surface obtained the next step is to get the physical
device surface properties and formats
VkFormat colorFormat;
uint32 formatCount = 0;
VkGetPhysicalDeviceSurfaceCapabilitiesKHR(physicalDevice, surface, &formatCount, NULL);
vector<VkSurfaceFormatKHR> surfaceFormats(formatCount);
VkGetPhysicalDeviceSurfaceCapabilitiesKHR(physicalDevice, surface, &formatCount, &surfaceFormats[0]);
// If the format list includes just one entry of VK_FORMAT_UNDEFINED,// the surface has no preferred format. Otherwise, at least one// supported format will be returnedif (formatCount == 1 && surfaceFormats[0].format == VK_FORMAT_UNDEFINED)
colorFormat = VK_FORMAT_B8G8R8A8_UNORM;
else {
assert(formatCount >= 1);
colorFormat = surfaceFormats[0].format;
}
colorSpace = surfaceFormats[0].colorSpace;
You can iterate this list to find better formats for your application but
the first entry is usually acceptable for most uses. Take note that Vulkan
differentiates between a color format and color space. The format
can be thought of like the data format while the latter is the way that
data is to be interpreted by the implementation. We record this information
for it will be useful for later rendering.