steps3D - Tutorials - Vulkan with classes

Vulkan with classes

Из предыдущий статей вы уже наверное поняли, что написание кода под Vulkan требует довольно много кода (хотя вы пока еще даже не представляет6 насколько много :)).

С другой стороны современный OpenGL тоже не слишком прост - если все делать руками без использования различных библиотек и wrapper-ов, то тоже нужно будет писать совсем не мало кода. В своих примерах я не пишу на "сыром" OpenGL, а использую библиотеки и обертки, что позволило заметно сократить размер примеров, сведя большинство примеров к 1-2 страницам кода.

Давайте применим этот же подход для написания кода на Vulkan. Мы завернем все основные сущности Vulkan в удобные для использования классы. При этом в отличии от распространенной библиотеки Vulkan.hpp мы будем заворачивать не базовые структуры Vk*, а гораздо более высокоуровневые сущности, чтобы сделать получающийся код гораздо компактнее и нагляднее. Обратите внимание, что для всех сущностей, являющихся обертками объектов Vulkan, удалены copy-constructor и оператор присваивания. Но для того, чтобы их можно было складывать в контейнеры, добавлены move-версии данные операций. Кроме того обычно конструктор не создает сущностей Vulkan, для этого будет использоваться специальный метод create.

При этом, как и ранее, мы будем использовать библиотеку VMA для выделения памяти под буфера и изображения. Также мы будем использовать библиотеку GLFW для работы с окнами и обработки событий (она поддерживает не только OpenGL, но и Vulkan).

Мы будем использовать библиотеку GLM для работы с векторами и матрицами, только нужно будет определить символ GLM_FORCE_DEPTH_ZERO_TO_ONE, чтобы матрица проектирования строилась именно для Vulkan (в нем zeye изменяется от 0 до 1, а не от -1 до 1 как в OpenGL).

Первым и самым главным нашим классом будет класс Device. Он будет отвечать сразу на несколько базовых сущностей Vulkan - инстанс (экземпляр). физическое устройство и логическое устройство. Также в нем будут храниться очереди и различная информация об устройстве (properties, features).

bool    create ( VkInstance _instance, VkPhysicalDevice _physicalDebice, 
                VkSurfaceKHR surface, const std::vector<const char*>& deviceExtensions, 
                const std::vector<const char*>& validationLayers );

Самыми важными методами этого класса являются create, отвечающий за создание всех содержащихся сущностей, и clean, обеспечивающий их освобождение (например для последующего пересоздания).

void    clean ()

Метод allocCommandBuffers создает заданное количество командных буферов (точнее оберток, их содержащих внутри себя). Метод freeCommandBuffer освобождает созданный командный буфер и все связанные с ним ресурсы.

void                        freeCommandBuffer   ( CommandBuffer& buffer );
std::vector<CommandBuffer>  allocCommandBuffers ( uint32_t count );

Для выделения памяти используется библиотека VMA. Однако при этом поддерживается также вариант не использовать библиотеку VMA, в этом случае используется класс GpuMemory как абстракция выделенного блока памяти GPU. Главными методами этого класса являются alloc и clean служащие для выделения и освобождения блока памяти GPU.

void    clean ()
bool    alloc ( Device& _device, VkMemoryRequirements memRequirements, VkMemoryPropertyFlags properties )

Методы copy, map и unmap служат для копирования памяти GPU и управления отображением отображения памяти GPU в память CPU (если тип выделенной памяти это поддерживает).

Для инкапсуляции буферов будет использоваться класс Buffer. Самый главный метод этого класса это create, создающий буфер в памяти GPU с заданным типом памяти. Метод clean освобождает все ресурсы GPU, связанные с этим буфером.

void    clean ()
bool    create ( Device& dev, VkDeviceSize size, VkBufferUsageFlags usage, int mappable )

Также есть несколько вариантов перегруженного метода copy, служащего для копирования данных в буфер/из буфера.

bool    copy ( const void * ptr, VkDeviceSize size, size_t offs = 0 )

template <typename T>
bool    copy ( const std::vector<T>& data )
{
    return copy ( data.data (), data.size () * sizeof ( T ) );
}

template <typename T>
bool    copy ( const T& data )
{
    return copy ( &data, sizeof ( T ) );
}

С помощью метода copyBuffer можно скопировать в текущий буфер содержимое другого буфера.

void    copyBuffer ( SingleTimeCommand& cmd, Buffer& fromBuffer, VkDeviceSize size )

От класса Buffer наследуется класс PersistentBuffer, соответствующий случаю, когда для буфера все время поддерживается отображение в память CPU (без необходимости постоянно создавать и освобождать такое отображение). Метод getPtr служит для получения указателя на отображенную память CPU.

От класса PersistentBuffer наследуется шаблонный класс Uniform, инкапсулирующий persistent uniform буфер, хранящий массив значений класса T. Обратите внимание на метод create - он получает ссылку на устройство, число элементов в массиве и выравнивание для этих элементов в массиве (в ряде случаев выравнивание отдельных элементов играет очень важную роль).

bool    create ( Device& device, VkBufferUsageFlags usage, int n = 1, int al = 1 )

Для доступа к отдельным элементам на стороне CPU служат перегруженные операторы -> и [].

T * operator -> () const
    return getPtr ();
}

T&  operator [] ( int i )
{
    return *(T *)(i*itemSize + (char *)getPtr ());
}

Также мы сделаем обертки для классов синхронизации (semaphore, fence и event). Класс Semaphore является оберткой семафора в Vulkan, как и для остальных классов, его конструктор не создает семафора в Vulkan (но его деструктор уничтожает уже созданный). Для создания семафора служит метод create, получающий на вход ссылку на объект Device. Метод clean уничтожает созданный семафор, освобождая выделенные ресурсы. Метод signal переводит семафор во signalled-состояние, помещая соответствующий запрос в переданную очередь.

class   Semaphore 
{
    VkSemaphore handle = VK_NULL_HANDLE;
    Device   * device  = nullptr;

public:
    Semaphore  () = default;
    Semaphore  ( Semaphore&& s )
    {
        std::swap ( handle, s.handle );
    }
    Semaphore ( const Semaphore& ) = delete;
    ~Semaphore ()
    {
        clean ();
    }

    Semaphore& operator = ( const Semaphore& ) = delete;    

    bool    isOk () const
    {
        return device != nullptr && handle != VK_NULL_HANDLE;
    }

    VkSemaphore getHandle () const
    {
        return handle;
    }

    void    clean ()
    {
        if ( handle != VK_NULL_HANDLE )
            vkDestroySemaphore ( device->getDevice (), handle, nullptr );

        handle = VK_NULL_HANDLE;
    }

    void    create ( Device& dev )
    {
        device = &dev;

        VkSemaphoreCreateInfo semaphoreCreateInfo = {};

        semaphoreCreateInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;

        if ( vkCreateSemaphore ( device->getDevice (), &semaphoreCreateInfo, nullptr, &handle ) != VK_SUCCESS )
            fatal () << "Semaphore: error creating" << Log::endl;
    }


    void    signal ( VkQueue queue )
    {
        VkSubmitInfo    submitInfo = {};
        
        submitInfo.sType                = VK_STRUCTURE_TYPE_SUBMIT_INFO;
        submitInfo.signalSemaphoreCount = 1;
        submitInfo.pSignalSemaphores    = &handle;
        
        vkQueueSubmit   ( queue, 1, &submitInfo, VK_NULL_HANDLE );
        vkQueueWaitIdle ( queue );
    }
};

Класс Fence служит оберткой для объекта fence. Метод create как обычно служит для создания соответствующего объекта (параметр signalled позволяет создавать объект сразу во взведенном состоянии). Метод clean уничтожает созданный объект. При помощи метода reset можно сбросить состояние fence, метод status возвращает является ли текущее состояние взведенным. При помощи метода wait можно подождать пока объект не будет переведен во взведенное состояние. Для этого метода задается параметр timeout, задающий максимальное время ожидания в наносекундах.

class   Fence
{
    VkFence     fence               = VK_NULL_HANDLE;
    Device    * device  = nullptr;
    
public:
    Fence () = default;
    Fence ( Fence&& f )
    {
        std::swap ( fence, f.fence );
    }
    Fence ( const Fence& ) = delete;
    ~Fence ()
    {
        clean ();
    }
    
    Fence& operator = ( const Fence& ) = delete;

    bool    isOk () const
    {
        return device != nullptr && fence != VK_NULL_HANDLE;
    }

    VkFence getHandle () const
    {
        return fence;
    }
    
    void    clean ()
    {
        if ( fence != VK_NULL_HANDLE )
            vkDestroyFence  ( device->getDevice (), fence, nullptr );

        fence = VK_NULL_HANDLE;
    }
    
    void    create ( Device& dev, bool signaled = false )
    {
        VkFenceCreateInfo fenceInfo = {};

        fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
        fenceInfo.flags = signaled ? VK_FENCE_CREATE_SIGNALED_BIT : 0;
        device          = &dev;
        
        if ( vkCreateFence ( device->getDevice (), &fenceInfo, nullptr, &fence ) != VK_SUCCESS )
            fatal () << "Fence: error creating" << Log::endl;
    }

        // set state to unsignalled from host
    void    reset ()
    {
        vkResetFences ( device->getDevice (), 1, &fence );
    }

        // VK_SUCCESS - signaled
        // VK_NOT_READY unsignaled
    VkResult    status () const
    {
        return vkGetFenceStatus ( device->getDevice (), fence );
    }

        // wait for fence, timeout in nanoseconds
    bool    wait ( uint64_t timeout )
    {
        return vkWaitForFences ( device->getDevice (), 1, &fence, VK_TRUE, timeout ) == VK_SUCCESS;
    }
};

Класс Event служит оберткой для события (event). Как и со многими другим объектами для создания мы используем метод create, для уничтожения - метод clean. Метод status возвращает текущее состояние события - наступили ли оно на данный момент времени или нет. Метод reset сбрасывает событие (в ненаступившее состояние), метод signal переводит его в наступившее состояние.

class   Event
{
    VkEvent     event               = VK_NULL_HANDLE;
    Device    * device  = nullptr;
    
public:
    Event () = default;
    Event( Event&& f )
    {
        std::swap ( event, f.event );
    }
    Event ( const Event& ) = delete;
    ~Event ()
    {
        clean ();
    }
    
    Event& operator = ( const Event& ) = delete;

    bool    isOk () const
    {
        return device != nullptr && event != VK_NULL_HANDLE;
    }

    VkEvent getHandle () const
    {
        return event;
    }
    
    void    clean ()
    {
        if ( event != VK_NULL_HANDLE )
            vkDestroyEvent  ( device->getDevice (), event, nullptr );

        event = VK_NULL_HANDLE;
    }
    
    void    create ( Device& dev )
    {
        VkEventCreateInfo eventInfo = {};

        eventInfo.sType = VK_STRUCTURE_TYPE_EVENT_CREATE_INFO;
        device          = &dev;
        
        if ( vkCreateEvent ( device->getDevice (), &eventInfo, nullptr, &event ) != VK_SUCCESS )
            fatal () << "Event: error creating" << Log::endl;
    }

    bool    status () const
    {
        VkResult res = vkGetEventStatus ( device->getDevice (), event );

        return res == VK_EVENT_SET;
    }
        // set state to unsignalled from host
    void    reset ()
    {
        vkResetEvent ( device->getDevice (), event );
    }

    void    signal ()       // set
    {
        vkSetEvent ( device->getDevice (), event );
    }
};

В этой статье мы рассмотрели самые базовые классы, во следующей мы продолжим рассмотрение используемых классов. Рассматриваемые классы можно найти в репозитории https://github.com/steps3d/vulkan-with-classes.