Vulkan API — урок 3. Базовый код

Основная структура

Итак. Ранее мы запускали проект с неким кодом. Теперь же будем использовать этот код:

#include <vulkan/vulkan.h>

#include <iostream>
#include <stdexcept>
#include <functional>

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

private:
    void initVulkan() {
    }

void mainLoop() {
    }
};

int main() {
    HelloTriangleApplication app;

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

    return EXIT_SUCCESS;
}

В нем для начала мы включаем заголовочный Vulkan’a предоставленный LunarG SDK, который обеспечивает работу функций, структур и перечислений.

stdexcept и iostream добавлены для вывода отчетов и обработки ошибок.

functional будет использован для лямбда функций в управлении ресурсами.

Сама программа свернута в класс, где мы сохраним объекты Vulkan’a как private члены класса и добавим функции для инициализации каждого из них, которые будут вызываться из  initVulkan. Когда все будет подготовлено, мы запустим основной цикл для рендера кадров. Мы будем выполнять mainLoop функцию пока окно не будет закрыто.

Если же во время выполнения программы вылезет какаялибонибудь фатальная ошибка, то мы вызовем std::runtime_error исключение с сообщение описывающим ошибку, которое мы передадим в main функцию для последующего вывода в командную строку.

В последующих главах планирую добавлять функции и объекты к тому, что написано здесь.

Управление ресурсами

Каждый Vulkan объект должен быть уничтожен с помощью вызова функции, когда он больше не нужен, так же, как каждый кусок памяти, выделенной при помощи  malloc, требует вызова free. Делать это вручную — слишком много работы, и вероятность допустить ошибку крайне высока, но мы можем избежать всех проблем используя C++ RAII принцип. Для этого всего нам необходимо создать класс-обертку для объектов Vulkan’a и соответственно будет удалять их при выходе из области видимости.

Для начала рассмотрим интерфейс класса-обертки – VDeleter. Допустим мы хотим хранить объекты VkInstance которые будут уничтожены при помощи vkDestroyInstance в определенный момент. Тогда нам необходимо добавить следующий член класса:

VDeleter<VkInstance> instance{vkDestroyInstance};

 Шаблонный аргумент указывает тип объекта Vulkan, который мы хотим обернуть и аргумент конструктора задает функцию удаления объекта при его выходе из области видимости (своеобразный деструктор, так сказать).

Для того, что бы привязать объект к обертке, мы бы хотели просто передать указатель в функцию создания, как если бы это была обычная переменная  VkInstance :

vkCreateInstance(&instanceCreateInfo, nullptr, &instance);

 К сожалению, получение адреса дескриптора в обертке не обязательно означает, что мы хотим перезаписать это значение. В общем просто используем &instance как сокращение массива экземпляров с 1 элементом. Если мы хотим описать новый дескриптор, то обертка должна очищать каждый предыдущий объект, дабы избежать утечек памяти. Поэтому было бы лучше иметь & оператор, возвращающий константный указатель и  имеющий явную функцию замены дескриптора. Функция replace вызывает очистку для любого существующего дескриптора и затем возвращает не константный указатель на перезаписанный дескриптора:

vkCreateInstance(&instanceCreateInfo, nullptr, instance.replace());

Таким образом теперь мы можем использовать instance переменную везде, где обычно принимается  VkInstance. Нам более не придется беспокоиться об очистке, т.к. это будет происходить автоматически после того как переменная instance станет недоступной. Это довольно просто, не так ли?

Реализация такого класса-обертки довольно проста. Она просто потребует чутка лямбда-магии (С++11, если вам еще не знакомы лямбда-выражения, то почитать о них можно тут и тут, сам не сразу привык к такому стилю написания, но все же он действительно удобен), что бы сократить синтаксис для определения функции очистки:

template <typename T>
class VDeleter {
public:
    VDeleter() : VDeleter([](T, VkAllocationCallbacks*) {}) {}

    VDeleter(std::function<void(T, VkAllocationCallbacks*)> deletef) {
        this->deleter = [=](T obj) { deletef(obj, nullptr); };
    }

    VDeleter(const VDeleter<VkInstance>& instance, std::function<void(VkInstance, T, VkAllocationCallbacks*)> deletef) {
        this->deleter = [&instance, deletef](T obj) { deletef(instance, obj, nullptr); };
    }

    VDeleter(const VDeleter<VkDevice>& device, std::function<void(VkDevice, T, VkAllocationCallbacks*)> deletef) {
        this->deleter = [&device, deletef](T obj) { deletef(device, obj, nullptr); };
    }

    ~VDeleter() {
        cleanup();
    }

    const T* operator &() const {
        return &object;
    }

    T* replace() {
        cleanup();
        return &object;
    }

    operator T() const {
        return object;
    }

    void operator=(T rhs) {
        cleanup();
        object = rhs;
    }

    template<typename V>
    bool operator==(V rhs) {
        return object == T(rhs);
    }

private:
    T object{ VK_NULL_HANDLE };
    std::function<void(T)> deleter;

    void cleanup() {
        if (object != VK_NULL_HANDLE) {
            deleter(object);
        }
    object = VK_NULL_HANDLE;
    }
};

Три доработанных конструктора позволяют указать все три типа удаления функции, используемые в Vulkan:

  • vkDestroyXXX(object, callbacks): в функцию очистки передается только сам объект, поэтому мы просто можем построить VDeleter с функцией в качестве аргумента.
  • vkDestroyXXX(instance, object, callbacks): также  необходимо передать в функцию очистки VkInstance, так что мы используем VDeleter конструктор, который принимает ссылку VkInstance и функцию очистки в качестве параметра.
  • vkDestroyXXX(device, object, callbacks): аналогично предыдущему варианту, но VkInstance будет замещен на  VkDevice.

Параметр callbacks опционален и мы передаем ему nullpt, что вы можете наблюдать в коде.

Все конструкторы инициализируют дескриптор объекта с эквивалентом nullptr в Vulkan’e: VK_NULL_HANDLE. Любой дополнительный аргумент, необходимый для функции deleter, также должен быть передан, обычно родительский объект. Перегрузка операторов получения ссылки, присваивания, сравнения и приведения типов (address-of, assignment, comparison and casting operators) позволяет сделать обертку наиболее прозрачной. Когда обернуты объект выходит из области видимости, это автоматически вызывает описанную нами функцию.

Оператор получения ссылки возвращает константный указатель, что позволяет убедиться в неизменности объекта. Если вы хотите заменить дескриптор внутри обертки через указатель, то следует использовать функцию replace().  Это позволит вызывать функцию очистки для существующего дескриптора.

Итак, что-то получилось выложить урок только заполночь, но тут ничего не поделать, постараюсь конечно все делать пораньше, но обещать этого не могу. Завтра будет выложен короткий урок по интеграции GLFW далее возможен однодневное затишье, но все равно заглядывайте, может все таки наберусь сил и буду выкладывать по уроку в день!

Main Admin

3 Comments

    • С Vulkan-HPP практически не знаком, в основном по причине «Vulkan-Hpp requires a C++11 capable compiler to compile. The following compilers are known to work: … Visual Studio >=2013 …», являюсь сторонником кросплатформенной разработки, особенно если все изначально под неё запилено, пока жду широкий спектр проверенных программ под разные платформы.
      Ну а непосредственно об аналоге VDeleter пока еще не слышал, документацию не читал по указанным выше причинами, но Вы можете и сами его быстренько добавить, все исходники ведь открыты.

      • Исходники-то открыты, но быстренько написать всё таки не смогу =)
        С лямбда-выражениями не знаком, да и там надо обращаться к функциям-членам, что несколько усложняет задачу.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *