Instance

Code: main.rs

The very first thing you will want to do is initialize the Vulkan library by creating an instance. The instance is the connection between your application and the Vulkan library and creating it involves specifying some details about your application to the driver. To get started, add the following imports:

use anyhow::{anyhow, Result};
use log::*;
use vulkanalia::loader::{LibloadingLoader, LIBRARY};
use vulkanalia::window as vk_window;
use vulkanalia::prelude::v1_0::*;

Here we first add the anyhow! macro to our imports from anyhow. This macro will be used to easily construct instances of anyhow errors. Then, we import log::* so we can use the logging macros from the log crate. Next, we import LibloadingLoader which serves as vulkanalia's libloading integration which we will use to load the initial Vulkan commands from the Vulkan shared library. The standard name of the Vulkan shared library on your operating system (e.g., vulkan-1.dll on Windows) is then imported as LIBRARY.

Next we import vulkanalia's window integration as vk_window which in this chapter we will use to enumerate the global Vulkan extensions required to render to a window. In a future chapter we will also use vk_window to link our Vulkan instance with our winit window.

Lastly we import the Vulkan 1.0 prelude from vulkanalia which will provide all of the other Vulkan-related imports we will need for this and future chapters.

Now, to create an instance we'll next have to fill in a struct with some information about our application. This data is technically optional, but it may provide some useful information to the driver in order to optimize our specific application (e.g., because it uses a well-known graphics engine with certain special behavior). This struct is called vk::ApplicationInfo and we'll create it in a new function called create_instance that takes our window and a Vulkan entry point (which we will create later) and returns a Vulkan instance:

unsafe fn create_instance(window: &Window, entry: &Entry) -> Result<Instance> {
    let application_info = vk::ApplicationInfo::builder()
        .application_name(b"Vulkan Tutorial\0")
        .application_version(vk::make_version(1, 0, 0))
        .engine_name(b"No Engine\0")
        .engine_version(vk::make_version(1, 0, 0))
        .api_version(vk::make_version(1, 0, 0));
}

A lot of information in Vulkan is passed through structs instead of function parameters and we'll have to fill in one more struct to provide sufficient information for creating an instance. This next struct is not optional and tells the Vulkan driver which global extensions and validation layers we want to use. Global here means that they apply to the entire program and not a specific device, which will become clear in the next few chapters. First we'll need to use vulkanalia's window integration to enumerate the required global extensions and convert them into null-terminated C strings (*const c_char):

let extensions = vk_window::get_required_instance_extensions(window)
    .iter()
    .map(|e| e.as_ptr())
    .collect::<Vec<_>>();

With our list of required global extensions in hand we can create and return a Vulkan instance using the Vulkan entry point passed into this function:

let info = vk::InstanceCreateInfo::builder()
    .application_info(&application_info)
    .enabled_extension_names(&extensions);

Ok(entry.create_instance(&info, None)?)

As you'll see, the general pattern that object creation function parameters in Vulkan follow is:

  • Reference to struct with creation info
  • Optional reference to custom allocator callbacks, always None in this tutorial

Now that we have a function to create Vulkan instances from entry points, we next need to create a Vulkan entry point. This entry point will load the Vulkan commands used to query instance support and create instances. But before we do that, let's add some fields to our App struct to store the Vulkan entry point and instance we will be creating:

struct App {
    entry: Entry,
    instance: Instance,
}

To populate these fields, update the App::create method to the following:

unsafe fn create(window: &Window) -> Result<Self> {
    let loader = LibloadingLoader::new(LIBRARY)?;
    let entry = Entry::new(loader).map_err(|b| anyhow!("{}", b))?;
    let instance = create_instance(window, &entry)?;
    Ok(Self { entry, instance })
}

Here we first create a Vulkan function loader which will be used to load the initial Vulkan commands from the Vulkan shared library. Next we create the Vulkan entry point using the function loader which will load all of the commands we need to manage Vulkan instances. Lastly we are now able to call our create_instance function with the Vulkan entry point.

Cleaning up

The Instance should only be destroyed right before the program exits. It can be destroyed in the App::destroy method using destroy_instance:

unsafe fn destroy(&mut self) {
    self.instance.destroy_instance(None);
}

Like the Vulkan commands used to create objects, the commands used to destroy objects also take an optional reference to custom allocator callbacks. So like before, we pass None to indicate we are content with the default allocation behavior.

Non-conformant Vulkan implementations

Not every platform is so fortunate to have an implementation of the Vulkan API that fully conforms to the Vulkan specification. On such a platform, there may be standard Vulkan features that are not available and/or there may be significant differences between the actual behavior of a Vulkan application using that non-conformant implementation and what the Vulkan specification says that application should behave.

Since version 1.3.216 of the Vulkan SDK, applications that use a non-conformant Vulkan implementation must enable some additional Vulkan extensions. These compatibility extensions have the primary purpose of forcing the developer to acknowledge that their application is using a non-conformant implementation of Vulkan and that they should not expect everything to be as the Vulkan specification says it should be.

This tutorial will be utilizing these compatibility Vulkan extensions so that your application can run even on platforms that lack a fully conforming Vulkan implementation.

However, you might ask "Why are we doing this? Do we really need to worry about supporting niche platforms in an introductory Vulkan tutorial?" As it turns out, the not-so-niche macOS is among those platforms that lack a fully-conformant Vulkan implementation.

As was mentioned in the introduction, Apple has their own low-level graphics API, Metal. The Vulkan implementation that is provided as part of the Vulkan SDK for macOS (MoltenVK) is a layer that sits in-between your application and Metal and translates the Vulkan API calls your application makes into Metal API calls. Because MoltenVK is not fully conformant with the Vulkan specification, you will need to enable the compatibility Vulkan extensions we've been talking about to support macOS.

As an aside, while MoltenVK is not fully-conformant, you shouldn't encounter any issues caused by deviations from the Vulkan specification while following this tutorial on macOS.

Enabling compatibility extensions

Note: Even if you are not following this tutorial on a macOS, some of the code added in this section is referenced in the remainder of this tutorial so you can't just skip it!

We'll want to check if the version of Vulkan we are using is equal to or greather than the version of Vulkan that introduced the compatibility extension requirement. With this goal in mind, we'll first add an additional import:

use vulkanalia::Version;

With this new import in place, we'll define a constant for the minimum version:

const PORTABILITY_MACOS_VERSION: Version = Version::new(1, 3, 216);

Replace the extension enumeration and instance creation code with the following:

let mut extensions = vk_window::get_required_instance_extensions(window)
    .iter()
    .map(|e| e.as_ptr())
    .collect::<Vec<_>>();

// Required by Vulkan SDK on macOS since 1.3.216.
let flags = if 
    cfg!(target_os = "macos") && 
    entry.version()? >= PORTABILITY_MACOS_VERSION
{
    info!("Enabling extensions for macOS portability.");
    extensions.push(vk::KHR_GET_PHYSICAL_DEVICE_PROPERTIES2_EXTENSION.name.as_ptr());
    extensions.push(vk::KHR_PORTABILITY_ENUMERATION_EXTENSION.name.as_ptr());
    vk::InstanceCreateFlags::ENUMERATE_PORTABILITY_KHR
} else {
    vk::InstanceCreateFlags::empty()
};

let info = vk::InstanceCreateInfo::builder()
    .application_info(&application_info)
    .enabled_extension_names(&extensions)
    .flags(flags);

This code enables KHR_PORTABILITY_ENUMERATION_EXTENSION if your application is being compiled for a platform that lacks a conformant Vulkan implementation (just checking for macOS here) and the Vulkan version meets or exceeds the minimum version we just defined.

This code also enables KHR_GET_PHYSICAL_DEVICE_PROPERTIES2_EXTENSION under the same conditions. This extension is needed to enable the KHR_PORTABILITY_SUBSET_EXTENSION device extension which will be added later in the tutorial when we set up a logical device.

Instance vs vk::Instance

When we call our create_instance function, what we get back is not a raw Vulkan instance as would be returned by the Vulkan command vkCreateInstance (vk::Instance). Instead what we got back is a custom type defined by vulkanalia which combines both a raw Vulkan instance and the commands loaded for that specific instance.

This is the Instance type we have been using (imported from the vulkanalia prelude) which should not be confused with the vk::Instance type which represents a raw Vulkan instance. In future chapters we will also use the Device type which, like Instance, is a pairing of a raw Vulkan device (vk::Device) and the commands loaded for that specific device. Fortunately we will not be using vk::Instance or vk::Device directly in this tutorial so you don't need to worry about getting them mixed up.

Because an Instance contains both a Vulkan instance and the associated commands, the command wrappers implemented for an Instance are able to provide the Vulkan instance when it is required by the underlying Vulkan command.

If you look at the documentation for the vkDestroyInstance command, you will see that it takes two parameters: the instance to destroy and the optional custom allocator callbacks. However, destroy_instance only takes the optional custom allocator callbacks because it is able to provide the raw Vulkan handle as the first parameter itself as described above.

Before continuing with the more complex steps after instance creation, it's time to evaluate our debugging options by checking out validation layers.