|
Главная
Статьи
Ссылки
Скачать
Скриншоты
Юмор
Почитать
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.