Главная Статьи Ссылки Скачать Скриншоты Юмор Почитать Tools Проекты Обо мне Гостевая Форум |
Также нам понадобится класс для работы со множествами дескрипторов (descriptor sets).
Обратите внимание, что множества дескрипторов в Vulkan не создаются явно, а выделяются из специальных пулов.
При создании такого пула нужно явно указать максимальное число дескрипторов для каждого типа.
Понятно, что это довольно не удобно. Поэтому мы будем использовать специальный класс DescriptorAllocator
,
задачей которого будет выделение и освобождение множество дескрипторов.
Внутри себя класс будет иметь массив пулов, организованных в два массива - активные пулы (из которых уже делались
выделения) и свободные пулы.
По мере необходимости класс создает новые пулы. Выделение множеств дескрипторов происходит через метод alloc
,
метод reset
освобождает все выделенные ранее множества дескрипторов.
class DescriptorAllocator
{
public:
struct PoolSizes
{
std::vector<std::pair<VkDescriptorType,float>> sizes =
{
{ VK_DESCRIPTOR_TYPE_SAMPLER, 0.5f },
{ VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 4.f },
{ VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 4.f },
{ VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1.f },
{ VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER, 1.f },
{ VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, 1.f },
{ VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 2.f },
{ VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 2.f },
{ VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 1.f },
{ VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC, 1.f },
{ VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, 0.5f }
};
};
void create ( Device& newDevice ); // start allocator
void reset (); // reset all pools and move them to freePools
void clean (); // destroy allocator
// allocate descriptor set
VkDescriptorSet alloc ( VkDescriptorSetLayout layout );
void setMultiplier ( VkDescriptorType type, float factor )
{
for ( auto& p : descriptorSizes.sizes )
if ( p.first == type )
p.second = factor;
}
private:
VkDescriptorPool pickPool (); // pick appropriate pool for allocation
VkDevice device = VK_NULL_HANDLE;
VkDescriptorPool currentPool = VK_NULL_HANDLE;
PoolSizes descriptorSizes;
std::vector<VkDescriptorPool> usedPools; // active pools with allocated items
std::vector<VkDescriptorPool> freePools;
};
Для инкапсуляции самих множеств дескрипторов мы будем использовать класс DescriptorSet
, содержащий в себе ссылку
на устройство, на DescriptorAllocator
, само множество дескрипторов и его layout.
С помощью метода setLayout
мы задаем layout (структуру) множества дескрипторов.
class DescriptorSet
{
Device * device = nullptr;
DescriptorAllocator * allocator = nullptr;
VkDescriptorSet set = VK_NULL_HANDLE;
VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE;
std::vector<VkWriteDescriptorSet> writes;
public:
DescriptorSet () = default;
DescriptorSet ( DescriptorSet&& )
{
fatal () << "DescriptorSet move c-tor called" << Log::endl;
}
DescriptorSet ( const DescriptorSet& ) = delete;
~DescriptorSet ()
{
clean ();
}
VkDescriptorSet getHandle () const
{
return set;
}
void clean ()
{
for ( auto& d : writes )
{
delete d.pBufferInfo;
delete d.pImageInfo;
}
writes.clear ();
}
DescriptorSet& setLayout ( Device& dev, DescriptorAllocator& descAllocator, const DescSetLayout& descSetLayout );
Метод create
подготавливает множество дескрипторов к использованию (создавая соответствующий объект Vulkan).
void create ()
{
if ( set == VK_NULL_HANDLE )
alloc ();
vkUpdateDescriptorSets ( device->getDevice (), static_cast<uint32_t>(writes.size()), writes.data(), 0, nullptr );
}
Также у этого класса есть целая группа методов add*
, добавляющих дескриптор определенного типа, ниже мы
приведем некоторые из них.
DescriptorSet& addUniformBuffer ( uint32_t binding, Buffer& buffer, VkDeviceSize offset = 0, VkDeviceSize size = VK_WHOLE_SIZE );
DescriptorSet& addStorageBuffer ( uint32_t binding, Buffer& buffer, VkDeviceSize offset = 0, VkDeviceSize size = VK_WHOLE_SIZE );
DescriptorSet& addImage ( uint32_t binding, Texture& texture, Sampler& sampler );
DescriptorSet& addSampler ( uint32_t binding, Sampler& sampler );
Еще одним объектом Vulkan, который мы завернем в класс С++, будет объект-конвейер (pipeline). При этом на самом деле у нас будет два
разных конвейера - графический (GraphicsPipeline
) и вычислительный (ComputePipeline
).
Сам класс GraphicsPipeline
инкапсулирует весь графический конвейер (со всеми его частями).
Обычно для задания графического конвейера необходимо задать большое число вспомогательных структур, в которых
задаются все свойства конвейера (в Vulkan для конвейера нет значений по умолчанию).
Ясно что это довольно неудобно. Поэтому мы инкапсулируем все свойства графического конвейера (GraphicsPipeline
)
и для большинства из них мы зададим значения по умолчанию - таким образом нужно будет явно задавать только
небольшую часть этих свойств.
Вместо приведения здесь полного кода класса GraphicsPipeline
(который на самом деле очень велик и включает в себя ряд
вспомогательных классов) мы приведем ниже просто пример его использования.
pipeline
.setDevice ( device )
.setVertexShader ( "shaders/pbr.vert.spv" )
.setFragmentShader ( "shaders/pbr.frag.spv" )
.setSize ( swapChain.getExtent () )
.addVertexBinding ( sizeof ( BasicVertex ) )
.addVertexAttributes <BasicVertex> ()
.addDescLayout ( 0, DescSetLayout ()
.add ( 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT )
.add ( 1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT )
.add ( 2, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT )
.add ( 3, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT )
.add ( 4, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT ) )
.setCullMode ( VK_CULL_MODE_NONE )
.setFrontFace ( VK_FRONT_FACE_COUNTER_CLOCKWISE )
.create ( renderPass );
Как видно из приведенного кода мы смогли сильно упростить работу по созданию и настройке графического конвейера и получить при этом гораздо более удобно-читаемый код.
Одним из существенных отличий Vulkan от OpenGL является так называемый swap chain. В OpenGL мы обычно считаем что у нас имеется двойная буферизация, т.е. у нас есть два буфера, в один из которых осуществляется рендеринг, а другой (точнее его цветовая часть) в это время показывается на экране. В действительности сейчас все немного не так. У нас есть набор изображений, в которые мы можем осуществлять рендеринг, причем в любой из них. Также любой из них может быть показан на экране (present).
Объект swap chain это и есть такой набор изображений.
В любой момент времени мы можем запросить у него изображение для рендеринга (acquireNextImage
).
Также в любой момент времени мы можем сказать что изображение с заданным номером готово к показу на экране
в заданной очереди (presentQueue).
Для наилучшего качества показа анимации можно использовать несколько различных режимов показа (present mode).
Мы завернем все это в класс SwapChain
.
Кроме собственно массива изображений он будет содержать в себе набор примитивов синхронизации, служащих для
синхронизации рендеринга и показа готовых изображений.
Мы позволим задавать через параметр конструктора класса хотим ли мы использовать для изображений обычный формат RGB
или же формат sRGB. В результате SwapChain
запросит у устройства список поддерживаемых форматов и выберет из
лучшей поддерживающий RGB/sRGB и способ показа.
Теперь из базовых классов, необходимых для создания простого приложения на Vulkan, у нас остается класс VulkanWindow
.
Этот класс инкапсулирует окно для рендеринга (аналогично классу GlutWindow
, использованному во многих примерах на OpenGL)
Поведения класса будет конфигурироваться двумя способами - через переопределение виртуальных методов и передачу в
конструктор указатель на объект-политику (policy).
Необходимость использования последнего связана с тем, что есть ряд операций, которые было бы правильно выполнить непосредственно
в конструкторе класса. Но в этом случае мы лишаемся возможности управлять поведением через переопределение методов для
унаследованных классов.
Именно поэтому эти действия вынесены в отдельный класс, указатель на который и передается в конструктор VulkanWindow
.
В этот класс вынесены методы pickDevice
для выбора подходящего устройства (если их несколько),
метод isDeviceSuitable
проверяющий подходит ли данное устройства,
validateLayers
и deviceExtensions
для получения списка слоев валидации и
списка расширения для устройства.
struct DevicePolicy
{
DevicePolicy () = default;
virtual ~DevicePolicy () = default;
// pick appropriate physical device
virtual VkPhysicalDevice pickDevice ( VkInstance instance, std::vector<VkPhysicalDevice>& devices, VkSurfaceKHR surface ) const
{
for ( const auto& dev : devices ) // check every device
if ( isDeviceSuitable ( dev, surface ) ) // use surface and checks for queue families
return dev;
return VK_NULL_HANDLE;
}
// check whether this device is ok
virtual bool isDeviceSuitable ( VkPhysicalDevice device, VkSurfaceKHR surface ) const
{
QueueFamilyIndices indices = QueueFamilyIndices::findQueueFamilies ( device, surface );
return indices.isComplete ();
}
virtual std::vector<const char*> validationLayers () const
{
return { "VK_LAYER_KHRONOS_validation" };
};
virtual std::vector<const char*> instanceExtensions () const
{
return {};
};
virtual std::vector<const char*> deviceExtensions () const
{
return { VK_KHR_SWAPCHAIN_EXTENSION_NAME };
};
};
Задачей класса VulkanWindow
является создание окна (при помощи библиотеки GLFW, которая на самом деле поддерживает не
только OpenGL, но и Vulkan), установка обработчиков сообщений, инициализация устройства (физического и логического),
получение очередей для устройства и т.п.
Также он отвечает за рендеринг анимаций, изменение размеров окна и расчет FPS.
Внутри себя он содержит свойства, необходимые для создания экземпляра (instance), выделения дескрипторов,
рендеринга и показа готовых изображений и многое другое.
Конструктор класса сразу создает окно с заданными свойствами и инициализирует необходимые объекты и ресурсы.
Как и в классе GlutWindow
данный класс содержит методы getCaption
, setCaption
,
getWidth
, getHeight
, getAspect
, getTime
, setFullscreen
и многие другие.
class VulkanWindow
{
protected:
enum
{
MAX_FPS_FRAMES = 5 // maximum number of frames we use for FPS computing
};
std::string appName = "Vulkan application";
std::string engineName = "No engine";
DevicePolicy * policy = nullptr;
GLFWwindow * window = nullptr;
bool hasDepth = true; // do we should attach depth buffer to FB
bool srgb = false;
uint32_t currentImage = 0;
Controller * controller = nullptr;
bool showFps = false;
bool fullScreen = false;
int frame = 0; // current frame number
float frameTime [MAX_FPS_FRAMES]; // time at last 5 frames for FPS calculations
float fps;
int savePosX, savePosY; // save pos & size when going fullscreen
int saveWidth, saveHeight;
int width, height;
std::string title;
std::string assetPath; // path to various assets
Device device;
VkDebugUtilsMessengerEXT debugMessenger = VK_NULL_HANDLE;
VkDebugReportCallbackEXT msgCallback = VK_NULL_HANDLE;
VkSurfaceKHR surface = VK_NULL_HANDLE;
VkInstance instance = VK_NULL_HANDLE;
SwapChain swapChain;
Texture depthTexture; // may be empty
DescriptorAllocator descAllocator;
public:
VulkanWindow ( int w, int h, const std::string& t, bool depth = true, DevicePolicy * p = nullptr ) : hasDepth ( depth )
{
if ( p == nullptr )
p = new DevicePolicy ();
initWindow ( w, h, t );
initVulkan ( policy = p );
}
~VulkanWindow ()
{
descAllocator.clean ();
depthTexture.clean ();
clean ();
delete policy;
}
void setCaption ( const std::string& t )
{
title = t;
glfwSetWindowTitle ( window, title.c_str () );
}
std::string getCaption () const
{
return title;
}
double getTime () const // return time in seconds
{
return glfwGetTime ();
}
GLFWwindow * getWindow () const
{
return window;
}
uint32_t getWidth () const
{
return swapChain.getExtent ().width;
}
uint32_t getHeight () const
{
return swapChain.getExtent ().height;
}
VkExtent2D getExtent () const
{
return swapChain.getExtent ();
}
SwapChain& getSwapChain ()
{
return swapChain;
}
DescriptorAllocator& getDescriptorAllocator ()
{
return descAllocator;
}
Texture& getDepthTexture ()
{
return depthTexture;
}
void setSize ( uint32_t w, uint32_t h )
{
glfwSetWindowSize ( window, width = w, height = h );
}
float getAspect () const
{
return static_cast<float> ( getWidth () ) / static_cast<float> ( getHeight () );
}
void setShowFps ( bool flag )
{
showFps = flag;
}
bool isFullScreen () const
{
return fullScreen;
}
const std::string getAssetsPath () const
{
return assetPath;
}
void setAssetPath ( const std::string& path )
{
assetPath = path;
}
Метод run
отвечает за запуск рендеринга и анимации.
Метод drawFrame
отвечает за рендеринг очередного кадра, а метод submit
отвечает за показ заданного кадра.
Задача методов createPipelines
и freePipelines
является создание и уничтожение pipeline-объектов и объектов, связанных с ними.
Методы reshape
, keyTyped
, mouse*
и idle
это обработчики событий
(аналогичные имеющимся в GlutWindow
, обратите внимание, что есть
небольшая разница в обработке событий от мыши и клавиатуры в связана с отличиями в API GLFW и freeglut).
Сейчас мы рассмотрим написание простейшего приложения с использованием описанных ранее классов. В этом приложении мы будем выводить всего один треугольник, без каких-либо текстур и матриц преобразования, для каждой вершины мы зададим координаты и цвет.
-- скриншот
#include "VulkanWindow.h"
struct Vertex
{
glm::vec2 pos;
glm::vec3 color;
};
template <>
GraphicsPipeline& registerVertexAttrs<Vertex> ( GraphicsPipeline& pipeline )
{
return pipeline
.addVertexAttr ( 0, 0, VK_FORMAT_R32G32B32_SFLOAT, offsetof(Vertex, pos) ) // binding, location, format, offset
.addVertexAttr ( 0, 1, VK_FORMAT_R32G32B32_SFLOAT, offsetof(Vertex, color) );
}
class ExampleWindow final : public VulkanWindow
{
std::vector<CommandBuffer> commandBuffers;
std::vector<DescriptorSet> descriptorSets;
GraphicsPipeline pipeline;
Renderpass renderPass;
Buffer vertexBuffer;
public:
ExampleWindow ( int w, int h, const std::string& t ) : VulkanWindow ( w, h, t )
{
const std::vector<Vertex> vertices =
{
{ {0.0f, -0.5f}, {1.0f, 0.0f, 0.0f}},
{ {0.5f, 0.5f}, {0.0f, 1.0f, 0.0f}},
{{-0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}}
};
vertexBuffer.create ( device, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, vertices, Buffer::hostWrite );
createPipelines ();
}
virtual void createPipelines () override
{
// default renderPass
createDefaultRenderPass ( renderPass );
pipeline
.setDevice ( device )
.setVertexShader ( "shaders/shader.2.vert.spv" )
.setFragmentShader ( "shaders/shader.2.frag.spv" )
.setSize ( swapChain.getExtent () )
.addVertexBinding ( sizeof ( Vertex ) )
.addVertexAttributes <Vertex> ()
.addDescLayout ( 0, DescSetLayout () )
.create ( renderPass );
// create before command buffers
swapChain.createFramebuffers ( renderPass, depthTexture.getImageView () ); // m.b. depthTexture instead of getImageView ???
createDescriptorSets ();
createCommandBuffers ( renderPass );
}
virtual void freePipelines () override
{
commandBuffers.clear ();
pipeline.clean ();
renderPass.clean ();
descriptorSets.clear ();
descAllocator.clean ();
}
virtual void submit ( uint32_t imageIndex ) override
{
defaultSubmit ( commandBuffers [imageIndex] );
}
void createDescriptorSets ()
{
descriptorSets.resize ( swapChain.imageCount () );
for ( auto& desc : descriptorSets )
desc.setLayout ( device, descAllocator, pipeline.getDescLayout () ).create ();
}
void createCommandBuffers ( Renderpass& renderPass )
{
auto& framebuffers = swapChain.getFramebuffers ();
auto size = framebuffers.size ();
commandBuffers = device.allocCommandBuffers ( (uint32_t)size );
for ( size_t i = 0; i < size; i++ )
commandBuffers [i].begin ().beginRenderPass ( RenderPassInfo ( renderPass ).framebuffer ( framebuffers [i] ).extent ( swapChain.getExtent () ).clearColor ( 0, 0, 0, 1 ).clearDepthStencil () )
.pipeline ( pipeline )
.bindVertexBuffers ( { {vertexBuffer, 0} } )
.addDescriptorSets ( {descriptorSets[i]} )
.draw ( 3 )
.end ();
}
};
int main ( int argc, char * argv )
{
return ExampleWindow ( 800, 600, "Vulkan example" ).run ();
}
Структура Vertex
описывает формат используемой нами для рендеринга вершина, а шаблонная функция registerVertexAttrs
регистрирует все
атрибуты вершины в заданном графическом конвейере.
Для каждого вершинного атрибута мы задаем его binding (к какому вершинному буферу он относится), его номер (в директиве
layout в шейдере), формат и смещение внутри вершины.
Мы создадим новый класс ExampleWindow
, унаследовав его от VulkanWindow
.
В нем нам понадобится буфер с вершинами (объекта класса Buffer
), графический конвейер и проход рендеринга (объект класса RenderPass
).
Как уже отмечалось в Vulkan в отличии от OpenGL у нас есть целый набор буферов, в которые осуществляется рендеринг.
При этом возможна ситуация, когда мы еще не закончили рендеринг в один буфер, а уже приступаем к рендерингу в следующий.
Это значит, что мы не можем изменять какие-то данные, используемые при рендеринге предыдущего кадра.
Это значит, что у нас должен быть массив uniform/storage-буферов, массивов дескрипторов (поскольку они ссылаются на них),
командных буферов (т.к. в них явно задается куда осуществляется рендеринг).
Поэтому ряд объектов у нас представлены в нескольких экземплярах и хранятся в std::vector
.
При создании окна или изменении его размеров мы вызываем метод createPipelines
для подготовки объектов-конвейеров
(и связанных с ними объектов) к работе.
virtual void createPipelines () override
{
// default renderPass
createDefaultRenderPass ( renderPass );
pipeline
.setDevice ( device )
.setVertexShader ( "shaders/shader.2.vert.spv" )
.setFragmentShader ( "shaders/shader.2.frag.spv" )
.setSize ( swapChain.getExtent () )
.addVertexBinding ( sizeof ( Vertex ) )
.addVertexAttributes <Vertex> ()
.addDescLayout ( 0, DescSetLayout () )
.create ( renderPass );
// create before command buffers
swapChain.createFramebuffers ( renderPass, depthTexture.getImageView () );
createDescriptorSets ();
createCommandBuffers ( renderPass );
}
virtual void freePipelines () override
{
commandBuffers.clear ();
pipeline.clean ();
renderPass.clean ();
descriptorSets.clear ();
descAllocator.clean ();
}
Нам потребуется объект прохода рендеринга, но мы можем использовать код его инициализации по умолчанию (это справедливо
для большинства примеров) createDefaultRenderPass
.
Далее мы создаем и настраиваем объект типа GraphicsPipeline
, задавая для него все необходимые параметры и используя
параметры по умолчанию.
После этого нам необходимо подготовить swap chain к рендерингу, создав необходимые изображения и виды для них,
фреймбуферы и объекты синхронизации при помощи вызова createFramebuffers
.
Далее мы создаем множество дескрипторов (через вызов createDescriptorSets
) и командные буфера
(через вызов createCommandBuffers
).
Для создания множества дескрипторов мы просто задаем нужный размер массиву множеств дескрипторов (по числу изображений в swap chain) и для каждого из них задаем layout из графического конвейера.
void createDescriptorSets ()
{
descriptorSets.resize ( swapChain.imageCount () );
for ( auto& desc : descriptorSets )
desc.setLayout ( device, descAllocator, pipeline.getDescLayout () ).create ();
}
Более сложным является создание массива командных буферов - мы также начинаем с задания размера массива.
После этого для каждого буфера мы начинаем запись в него (begin
), начинаем проход рендеринга (beginRenderPass
),
задаем фреймбуфер, куда будет осуществляться рендеринг (метод framebuffer
), задаем размер
области рендеринга (extent
), задаем цвет для очистки буфера цвета (clearColor
) и значения для очистки
буферов глубины и трафарета (clearDepthStencil
), задаем используемый конвейер (pipeline
),
массив используемых вершинных буферов (bindVertexBuffers
) и множества дескрипторов (addDescriptorSet
)
После этого мы осуществляем рендеринг треугольника (draw
) и завершаем запись в буфер (end
).
void createCommandBuffers ( Renderpass& renderPass )
{
auto& framebuffers = swapChain.getFramebuffers ();
auto size = framebuffers.size ();
commandBuffers = device.allocCommandBuffers ( (uint32_t)size );
for ( size_t i = 0; i < size; i++ )
commandBuffers [i].begin ().beginRenderPass ( RenderPassInfo ( renderPass ).framebuffer ( framebuffers [i] )
.extent ( swapChain.getExtent () ).clearColor ( 0, 0, 0, 1 ).clearDepthStencil () )
.pipeline ( pipeline )
.bindVertexBuffers ( { {vertexBuffer, 0} } )
.addDescriptorSets ( {descriptorSets[i]} )
.draw ( 3 )
.end ();
}
Для показа кадра с заданным номеров мы переопределяем метод submit
(просто вызывая defaultSubmit
)
Также нам нужно переопределить метод freePipelines
для освобождения конвейера и связанных с ним объектов.
Обратите внимание, что при задании ряда объектов мы явно указываем размер области рендеринга.
Это значит при при изменении размеров окна все эти объекты нужно пересоздать.
Именно для этого и используется пара методов freePipelines
/createPipelines
.
virtual void submit ( uint32_t imageIndex ) override
{
defaultSubmit ( commandBuffers [imageIndex] );
}
Все файлы для этого примера доступны на github - https://github.com/steps3d/vulkan-with-classes.git.