Главная Статьи Ссылки Скачать Скриншоты Юмор Почитать Tools Проекты Обо мне Гостевая Форум |
Из предыдущий статей вы уже наверное поняли, что написание кода под 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.
От класса
Для доступа к отдельным элементам на стороне CPU служат перегруженные операторы
Также мы сделаем обертки для классов синхронизации (semaphore, fence и event).
Класс
Класс
Класс
В этой статье мы рассмотрели самые базовые классы, во следующей мы продолжим рассмотрение используемых классов.
Рассматриваемые классы можно найти в репозитории https://github.com/steps3d/vulkan-with-classes.
PersistentBuffer
наследуется шаблонный класс UniformT
.
Обратите внимание на метод create
- он получает ссылку на устройство, число элементов в массиве и выравнивание
для этих элементов в массиве (в ряде случаев выравнивание отдельных элементов играет очень важную роль).
bool create ( Device& device, VkBufferUsageFlags usage, int n = 1, int al = 1 )
->
и []
.
T * operator -> () const
return getPtr ();
}
T& operator [] ( int i )
{
return *(T *)(i*itemSize + (char *)getPtr ());
}
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 );
}
};