Vulkan. Developer's guide. Draw a triangle

I am a translator for CG Tribe in Izhevsk, and I continue to upload translations of the Vulkan API manual. Source link - vulkan-tutorial.com .



This publication is dedicated to the translation of the Drawing a triangle section, namely the Setup subsection, the Base code and Instance chapters.



Content
1.



2.



3.



4.





  1. (pipeline)


5.



  1. Staging


6. Uniform-



  1. layout
  2. sets


7.



  1. Image view image sampler
  2. image sampler


8.



9.



10. -



11. Multisampling



FAQ









Base Code







General structure



In the previous chapter, we described how to create a project for Vulkan, configure it correctly and test it using a code snippet. In this chapter, we'll start with the basics.



Consider the following code:



#include <vulkan/vulkan.h>

#include <iostream>
#include <stdexcept>
#include <cstdlib>

class HelloTriangleApplication {
public:
    void run() {
        initVulkan();
        mainLoop();
        cleanup();
    }

private:
    void initVulkan() {

    }

    void mainLoop() {

    }

    void cleanup() {

    }
};

int main() {
    HelloTriangleApplication app;

    try {
        app.run();
    } catch (const std::exception& e) {
        std::cerr << e.what() << std::endl;
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}
      
      





First, we include the Vulkan header file from the LunarG SDK. Header files stdexcepts



and iostream



are used for error handling and distribution. The header file cstdlib



provides macros EXIT_SUCCESS



and EXIT_FAILURE



.



The program itself is wrapped in the HelloTriangleApplication class, in which we will store Vulkan objects as private members of the class. There we will also add functions to initialize each object, called from the function initVulkan



. After that, let's create a main loop for rendering frames. To do this, fill in a function mainLoop



where the loop will be executed until the window is closed. After closing the window and exiting, the mainLoop



resources should be released. To do this, fill in cleanup



.



If a critical error occurs during operation, we will throw an exception std::runtime_error



that will be caught in the function main



, and the description will be displayed in std::cerr



. One of such errors might be, for example, a message that the required extension is not supported. To handle many of the standard exception types, we catch a more general one std::exception



.



Almost every subsequent chapter will add new functions that are called from initVulkan



, and new Vulkan objects that need to be released at cleanup



the end of the program.



Resource management



If Vulkan objects are no longer needed, they must be destroyed. C ++ allows you to automatically deallocate resources using RAII or smart pointers provided by the header file <memory>



. However, in this tutorial we decided to explicitly write when to allocate and deallocate Vulkan objects. After all, this is the peculiarity of Vulkan's work - to describe in detail each operation in order to avoid possible mistakes.



After reading the tutorial, you can implement automatic resource management by writing C ++ classes that receive Vulkan objects in the constructor and free them in the destructor. You can also implement your own deleter for std::unique_ptr



or std::shared_ptr



, depending on your requirements. The RAII concept is recommended for larger programs, but it is helpful to learn more about it.



Vulkan objects are created directly using a function like vkCreateXXX , or allocated through another object using a function like vkAllocateXXX . After making sure that the object is not in use anywhere else, you must destroy it with vkDestroyXXX or vkFreeXXX . The parameters for these features usually vary depending on the type of object, but there is one common parameter: pAllocator



. This is an optional parameter that allows you to use callbacks for custom memory allocation. We will not need it in the manual, we will pass it as an argument nullptr



.



GLFW Integration



Vulkan works fine without creating a window when using offscreen rendering, but much better when the result is visible on the screen.

First, replace the line with the #include <vulkan/vulkan.h>



following:



#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
      
      





Add a function initWindow



and add its call from the method run



before other calls. We will use initWindow



GLFW to initialize and create a window.



void run() {
    initWindow();
    initVulkan();
    mainLoop();
    cleanup();
}

private:
    void initWindow() {

    }
      
      





The very first call to initWindow



must be a function glfwInit()



that initializes the GLFW library. GLFW was originally designed to work with OpenGL. We don't need an OpenGL context, so indicate that we don't need to create it using the following call:



glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
      
      





Temporarily disable the ability to resize the window, since handling this situation requires separate consideration:



glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
      
      





It remains to create a window. To do this, add a private member GLFWwindow* window;



and initialize the window with:



window = glfwCreateWindow(800, 600, "Vulkan", nullptr, nullptr);
      
      





The first three parameters define the width, height and title of the window. The fourth parameter is optional, it allows you to specify the monitor on which the window will be displayed. The last parameter is specific to OpenGL.



It would be nice to use constants for the width and height of the window, since we will need these values ​​elsewhere. Add the following lines before the class definition HelloTriangleApplication



:



const uint32_t WIDTH = 800;
const uint32_t HEIGHT = 600;
      
      





and replace the call to create a window with



window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr);
      
      





You should have the following function initWindow



:



void initWindow() {
    glfwInit();

    glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
    glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);

    window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr);
}


      
      





Let's describe the main loop in the method mainLoop



to keep the application running until the window is closed:



void mainLoop() {
    while (!glfwWindowShouldClose(window)) {
        glfwPollEvents();
    }
}
      
      





This code shouldn't raise any questions. It handles events such as pressing the X button before the user closes the window. Also from this loop we will call a function to render individual frames.



After closing the window, we need to free resources and exit GLFW. First, let's add to the cleanup



following code:



void cleanup() {
    glfwDestroyWindow(window);

    glfwTerminate();
}
      
      





As a result, after starting the program, you will see a window with a name Vulkan



that will be displayed until the program is closed. Now that we have a skeleton for working with Vulkan, let's move on to creating our first Vulkan object!



C ++ code











Instance







Instantiation



The first thing you need to do is create an instance to initialize the library. An instance is the link between your program and the Vulkan library, and in order to create it, you will need to provide the driver with some information about your program.



Add a method createInstance



and call it from a function initVulkan



.



void initVulkan() {
    createInstance();
}
      
      





Add an instance member to our class to hold an instance handle:



private:
VkInstance instance;
      
      





Now we need to fill in a special structure with information about the program. Technically, the data is optional, however, this will allow the driver to obtain useful information to optimize the work with your program. This structure is called VkApplicationInfo



:



void createInstance() {
    VkApplicationInfo appInfo{};
    appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
    appInfo.pApplicationName = "Hello Triangle";
    appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
    appInfo.pEngineName = "No Engine";
    appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
    appInfo.apiVersion = VK_API_VERSION_1_0;
}
      
      





As mentioned, many structures in Vulkan require an explicit type definition in the sType member . Also, this structure, like many others, contains an element pNext



that allows you to provide information for extensions. We use value initialization to fill the structure with zeros.



Most of the information in Vulkan is passed through structures, so you need to fill in one more structure to provide enough information to create an instance. The following structure is required, it tells the driver which global extensions and validation layers we want to use. "Global" means that the extensions apply to the entire program and not to a specific device.



VkInstanceCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
createInfo.pApplicationInfo = &appInfo;
      
      





The first two parameters raise no questions. The next two members define the required global extensions. As you already know, the Vulkan API is completely platform independent. This means that you need an extension to interact with the windowing system. GLFW has a handy built-in function that returns a list of required extensions.



uint32_t glfwExtensionCount = 0;
const char** glfwExtensions;

glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);

createInfo.enabledExtensionCount = glfwExtensionCount;
createInfo.ppEnabledExtensionNames = glfwExtensions;
      
      





The last two structure members define which global validation layers to include. We'll talk about them in more detail in the next chapter, so leave these values ​​blank for now.



createInfo.enabledLayerCount = 0;
      
      





Now you have done everything necessary to create an instance. Make a call vkCreateInstance



:



VkResult result = vkCreateInstance(&createInfo, nullptr, &instance);
      
      





As a rule, the parameters of functions for creating objects are in this order:



  • Pointer to a structure with the required information
  • Pointer to custom allocator
  • Pointer to a variable where the descriptor of the new object will be written


If everything is done correctly, the instance descriptor will be stored in instance . Almost all Vulkan functions return a VkResult value , which can be either an VK_SUCCESS



error code. We don't need to store the result to make sure the instance has been created. Let's use a simple check:



if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) {
    throw std::runtime_error("failed to create instance!");
}
      
      





Now run the program to verify that the instance was created successfully.



Checking Supported Extensions



If we look at the Vulkan documentation , we can find that one of the possible error codes is VK_ERROR_EXTENSION_NOT_PRESENT



. We can simply specify the required extensions and stop working if they are not supported. This makes sense for major extensions like the windowing system interface, but what if we want to test the optional capabilities?



To get a list of supported extensions before instantiating, use the vkEnumerateInstanceExtensionProperties function... The first parameter of the function is optional, it allows you to filter extensions by a specific validation layer, so we'll leave it blank for now. The function also requires a pointer to a variable, where the number of extensions will be written and a pointer to a memory area where information about them should be written.



To allocate memory for storing extension information, you first need to know the number of extensions. Leave the last parameter blank to request the number of extensions:



uint32_t extensionCount = 0;
vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr);
      
      





Allocate an array to store extension information (don't forget about include <vector>



):



std::vector<VkExtensionProperties> extensions(extensionCount);
      
      





You can now request information about extensions.



vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, extensions.data());
      
      





Each VkExtensionProperties structure contains the name and version of the extension. They can be listed with a simple for loop ( \t



here is the indentation tab):



std::cout << "available extensions:\n";

for (const auto& extension : extensions) {
    std::cout << '\t' << extension.extensionName << '\n';
}
      
      





You can add this code to a function createInstance



for more information on Vulkan support. You can also try creating a function that will check if all extensions returned by the function glfwGetRequiredInstanceExtensions



are included in the list of supported extensions.





Cleaning



VkInstance must be destroyed before closing the program. This can be donecleanup



using the VkDestroyInstance function:



void cleanup() {
    vkDestroyInstance(instance, nullptr);

    glfwDestroyWindow(window);

    glfwTerminate();
}
      
      





The parameters for the vkDestroyInstance function are self- explanatory. As mentioned in the previous chapter, the allocation and deallocation functions in Vulkan accept optional pointers to custom allocators that we do not use and we pass nullptr



. All other Vulkan resources must be cleaned up before the instance is destroyed.



Before moving on to more complex steps, we need to set up the validation layers for ease of debugging.



C ++ code



All Articles