Решил создать тему для всех моих текущих проектов.
1. FrameGraph
Первоначально ради эксперимента написал фреймграф на своем недодвижке, потом решил вынести в отдельную либу, чтоб можно было использовать отдельно от движка и заодно переписать все с учетом предыдущего опыта.
Идея в том, чтобы сделать абстракцию от Vulkan (и других апи), которая максимально упрощает работу и при этом дает минимальный оверхэд, а плюс ко всему еще и пытается оптимизировать.
Начало разработки: 26 июля 2018
Статус: вроде работает
Дополнительно идут вспомогательные проекты:
VulkanLoader - моя версия загрузчика функций вулкана, из плюсов:
- добавлен аттрибут [[nodiscard]] - теперь компилятор напомнит проверить результат функции.
- если адрес функции не удалось взять, то подставится заглушка, которая будет ругаться в лог, но прога не упадет, хотя может упасть в другом месте, так как появляются неинициализированные или некорректные значения.
Framework - инициализация девайса и свопчена, обертка над SDL2 и glfw + пример с двумя окнами на разных девайсах (разных гпу если есть).
PipelineCompiler - встраивается в фреймграф и позволяет использовать glsl шейдеры.
GLSL_Trace - патчит AST от glslang для записи трейса шейдера, это нужно для отладки шейдеров, может использоваться отдельно, либо встраивается в PipelineCompiler и включается флагами.
UI - imgui на фреймграфе.
Scene - рендер и менеджер сцен, есть поддержка рейтрейса на RTX.
2. VkTraceConverter
Мне понравился слой vktrace, который записывает все вызовы апи и позволяет их проигрывать заново. И я решил написать конвертер трейса в с++ код при этом с возможностью добавления оптимизаций/деоптимизаций, чтоб проверить как это влияет на производительность.
Все это нужно для оптимизации фреймграфа. В планах конвертировать в:
- вулкан, как в оригинале
- оптимизированный вулкан
- vulkan-ez как один из конкурентов моему фреймграфу
- собственно в вызовы моего фреймграфа
- возможно будет порт на огл 4 со всеми оптимизациями
А дальше тесты покажут кто круче))
Начало разработки: 30 августа 2018
Статус: работает конвертация в C++ код, в вызовы вулкана и в вызовы фреймграфа
Наконец сконвертировал трейс и с++ и все работает, надо будет еще некоторые моменты пофиксить.
Но есть проблема: маленький трейс превращается в 500мб кода, который компилируется 50мин и бинарник весит 230мб.
Звучить совсем непрактично, так что буду думать как сохранить читаемый и удобный для редактирования код, но не такой огромный)
Немного отвлекся и сделал визуализацию графа синхронизаций
оранжевым отмечены сигналы - fence или semaphore
красные ноды и линии - ожидание сигнала
наконец запустил трейс записаный на nvidia под intel
теперь можно писать тесты на производительность и гонять на разных девайсах)
Сделал такую визуализацию графа и расставленых барьеров.
Барьеры делал как тут, только намного меньше, иначе мешают читать все остальное.
/A\
> Сделал такую визуализацию графа и расставленых барьеров.
А можно все это в код превратить?
Ну типа блюпринта для пайплайна.
Great V.
> А можно все это в код превратить?
Ну какбы оно визуализирует граф собраный кодом.
Вот пример: https://github.com/azhirnov/FrameGraph/blob/dev/tests/framegraph/… mage4.cpp#L76
Есть метод AddTask и у тасков есть зависимость DependsOn, к этому можно прикрутить уже любой более удобный интерфейс.
/A\
> Ну какбы оно визуализирует граф собраный кодом.
Это я шарю.
Я имею ввиду сложно ли из этого запилить графический редактор.
Чтобы натыкать квадратиков, соединить стрелочками и получить на выходе код.
Great V.
> Я имею ввиду сложно ли из этого запилить графический редактор.
При желании можно сделать все)
Тут все же низкоуровневые функции, обычно такие редакторы делают для более высокоуровневого кода.
Suslik
> блин, сделай что-нибудь с переносом строк — там половина просто не отображается или уезжает куда-то.
А у меня идеально в экран вписывается, проблема в том что гитхаб так и не умеет отображать табы как 4 пробела, а не как 8, но не вижу никакого удобного для меня варианта)
> я вижу, что ты разработал удобную систему работы с синхронизацией для своих
> тестовых задач, но не могу представить, как её транслировать на более широкий круг
Ну вообще-то я уже успешно сконвертировал часть примеров отсюда https://github.com/SaschaWillems/Vulkan и эту демку https://github.com/WindyDarian/Vulkan-Forward-Plus-Renderer, все работало без видимой просадки в производительности, даже смог запустить трейс doom4, но потом он переполнил буфер с дескрипторами, оказалось у меня баг в управлении закэшированными ресурсами, как пофикшу может и дальше пойдет)
В общем как оно все работает:
В граф добавляются таски, за исключением некоторых тасков тут все быстро отрабатывает.
Граф сортируется и начинается заполнение командного буфера. Тут работает класс VTaskProcessor, пример:
void VTaskProcessor::Visit (const VFgTask<CopyBufferToImage> &task) { // эти объекты хранят локальное состояние ресурсов, об этом позже VLocalBuffer const * src_buffer = task.srcBuffer; VLocalImage const * dst_image = task.dstImage; BufferImageCopyRegions_t regions; regions.resize( task.regions.size( ) ); for ( size_t i = 0, count = regions.size( ); i < count; ++i) { ... // добавляются используемые диапазоны данных для каждого из ресурсов _AddBuffer( src_buffer, EResourceState::TransferSrc, dst, dst_image ); _AddImage( dst_image, EResourceState::TransferDst, task.dstLayout, dst.imageSubresource ); } // все барьеры устанавливаются за 1 вызов vkCmdPipelineBarrier _CommitBarriers( ); // теперь можно выполнить копирование _dev.vkCmdCopyBufferToImage( _cmdBuffer, src_buffer->Handle( ), dst_image->Handle( ), task.dstLayout, uint( regions.size( )), regions.data( ) ); }
У ресурсов есть глобальные immutable объекты VBuffer, VImage и есть локальные состояние внутри потока одного кадра VLocalBuffer, VLocalImage они создаются только если ресурс используется в этом потоке и удаляются в конце кадра, они нужны чтоб хранить барьеры.
Реализация гарантирует, что в начале коммандного буфера объектам не нужна синхронизация, а в конце объект вернется в дефолтное состояние и все операции записи будут синхронизированны. Например image в дефолтном состоянии имеет лейаут General, тогда в начале будет перевод лейаута general -> color_attachment, рендеринг, а потом обратно color_attachment -> general, это немного неоптимально, но зато позволяет не задумываться о расстановке барьеров когда заполняешь команд буферы из разных потоков.
У локальних объектов есть методы:
AddPendingState - принимает диапазон данных который должен быть синхронизирован, внутри произойдет мержинг с другими ожидаемыми синхронизациями.
CommitBarrier - мержит ожидаемые синхронизации с предыдущими, если есть чтение после записи или запись после записи, то будет вставлен барьер.
ResetState - переводит ресурс в дефолтное состояние и вставляет синхронизации с операциями записи.
Правильно определенные диапазоны данных должны помочь расспараллеливанию команд над разными диапазонами, но насколько это влияет на производительность я еще не проверял, пока не было подходящего тестового примера с множеством барьеров.
Расставление семафоров и фенсов автоматизируется через SubmissionGraph (тут наглядная схема есть), идея в том, что каждый поток пишет команд буфер для какой-то части батча, как только батч заполнен, то он сразу сабмитится. Если у батча нет последующих батчей, то на него вешается фенс.
Suslik
> как у тебя осуществляется сихронизация между записями в рендертаргет и байндингом этого таргета как семплер в следующем пассе?
В начале рендер пасса всем рендер таргетам передается состояние что они color или depth-stencil attachment, при бинде descriptor set идет проход по всем записям и для них выставляются барьеры в перед началом рендер пасса.
> как осуществляется рендеринг большого количества объектов с потенциально уникальными шейдерами и/или constant buffer'ами?
У каждого drawtask'а есть свой набор дескрипторов и пуш констант. Заметного оверхэда от этого я не заметил.
> как устанавливаются blend states и, например, depth buffer?
RenderPassDesc().AddTarget( RenderTargetID("depth"), depth_image ).AddTarget( RenderTargetID("color"), color_image ).AddColorBuffer( RenderTargetID("color"), <тут настраивается blend state> )
Для каждого drawtask можно заоверайдить состояния из рендер пасса
DrawVertices().AddColorBuffer(...)
> если ты пишешь в буфер, а потом из него читаешь, то нужно ли вообще создавать для этого отдельные два таска с зависимостями и не проще ли хранить
> синхронизационные примитивы внутри самого класса буфера?
Таски это просто набор команд, если есть необходимость часто вызывать несколько тасков последовательно, то есть смысл добавить новый тип таска который их заменит и будет более оптимизированным.
Информация по синхронизациям и так хранятся внутри буфера.
/A\
> У ресурсов есть глобальные immutable объекты VBuffer, VImage и есть локальные
> состояние внутри потока одного кадра VLocalBuffer, VLocalImage они создаются
> только если ресурс используется в этом потоке и удаляются в конце кадра, они
> нужны чтоб хранить барьеры.
я тоже пришёл к чему-то такому. только я для себя разделил понятие ресурса (в основном immutable) и его состояние внутри кадра (что-то вроде state, который меняется при каждой операции с ресурсом внутри кадра). интересный момент заключается в том, что состояние ресурса — это не то, в каком состоянии этот ресурс находится в данный момент, а то, в каком состоянии он будет находиться в момент выполнения этой стадии рендера будущего кадра. грубо говоря, если ты говоришь, что будешь рендерить в этот таргет, то он перейдёт в состояние color attachment, тогда потом, чтобы из него читать, он должен будет перейти в состояние для чтение. вот эти будущие состояния его layout'а и хранятся в state.
/A\
> Реализация гарантирует, что в начале коммандного буфера объектам не нужна
> синхронизация, а в конце объект вернется в дефолтное состояние и все операции
> записи будут синхронизированны.
у меня в такой парадигме возникают проблемы с ресурсами, которые загружаются асинхронно между кадрами. например, ресурсы, которые загружаются при старте приложения или на фоне. например, лоды текстуры могут загружаться с высоких к низким и использоваться по мере готовности.
/A\
> У каждого drawtask'а есть свой набор дескрипторов и пуш констант
представь, у нас есть несколько тысяч объектов и на них на всех — несколько десятков шейдеров (материалов). у каждого материала — свой набор параметров, причём каждый параметр помечен как "общий для всей сцены", "общий для всех объектов с этим шейдером", "уникальный для каждого материала", то есть, грубо говоря, разбиты на дескриптор сеты по частоте использования. разумеется, шейдеры и параметры — data-driven, то есть хранятся в ресурсах или генерятся в рантайме. как бы ты организовал rendering loop для такой системы?
/A\
> Например image в дефолтном состоянии имеет лейаут General, тогда в начале будет
> перевод лейаута general -> color_attachment, рендеринг, а потом обратно
> color_attachment -> general, это немного неоптимально, но зато позволяет не
> задумываться о расстановке барьеров когда заполняешь команд буферы из разных
> потоков.
я тож думал о том, чтобы хранить при создании все ресурсы в некотором дефолтном состоянии и переводить их в нужный лейаут, грубо говоря, on demand. к сожалению, многие команды требуют как начального лейаута, так и конечного, поэтмоу чтобы их автоматически проставить, нужно знать текущую операцию над ресурсом и следующую. это, пожалуй, основной плюс frame graph'а, который я вижу.
/A\
> RenderPassDesc().AddTarget( RenderTargetID("depth"), depth_image ).AddTarget( RenderTargetID("color"), color_image )
не вижу контроля, как определяется, в какой мип и леер происходит рендеринг. для этого нужно либо вытаскивать в API понятие ImageView, либо указывать опциональные доп.аргументы при AddTarget.
ещё интересный момент. в твоей реализации ты практически полностью запрещаешь пользовательский доступ к vk::CommandBuffer. то есть если в будущем появляется новая вулканная фишка или если юзеру взбрело в голову сделать что-то экзотическое, то он этого сделать в принципе не сможет, если ты не добавишь для этого соответствующий таск в своём API. это не плохо и не хорошо, просто важный момент, я считаю.
/A\
> Ну вообще-то я уже успешно сконвертировал часть примеров отсюда
> https://github.com/SaschaWillems/Vulkan
оффтоп немного, но я уже высказывал своё мнение по поводу его примеров. если коротко, то считаю очень не показательными, так как пусть они и показывают, грубо говоря, какие вызовы GAPI за что отвечают, они никак не показывают самое сложное — как вокруг этих вызовов предполагается строить хоть какой-то более высокоуровневый рендер. например, ты пошёл в одном направлении, но ту же самую функциональность можно было реализовать ещё 10 разными способами и вообще не очевидно, какой из них предпочтительнее. и статей по организации вот такого middleware найти гораздо сложнее.
/A\
> https://github.com/WindyDarian/Vulkan-Forward-Plus-Renderer
аналогично:
void initialize() { createSwapChain( ); createSwapChainImageViews( ); createRenderPasses( ); createDescriptorSetLayouts( ); createGraphicsPipelines( ); createComputePipeline( ); createDepthResources( ); createFrameBuffers( ); createTextureSampler( ); createUniformBuffers( ); createLights( ); createDescriptorPool( ); model = VModel::loadModelFromFile( vulkan_context, getGlobalTestSceneConfiguration( ).model_file, texture_sampler.get( ), descriptor_pool.get( ), material_descriptor_set_layout.get( )); createSceneObjectDescriptorSet( ); createCameraDescriptorSet( ); createIntermediateDescriptorSet( ); updateIntermediateDescriptorSet( ); createLigutCullingDescriptorSet( ); createLightVisibilityBuffer( ); // create a light visiblity buffer and update descriptor sets, need to rerun after changing size createGraphicsCommandBuffers( ); createLightCullingCommandBuffer( ); createDepthPrePassCommandBuffer( ); createSemaphores( ); }
прибитый гвоздями к полу монолит, в котором всё примотано ко всему изолентой. такой код нереально ни поддерживать, ни расширять.
ещё вопрос. ты говоришь, что ковырял трейсы дума 4. у тебя есть картинки с их frame graph? интересно посмотреть, как он выглядит.
lookid
я это уже читал. там нет ничего ни про синхронизацию, ни про организацию пассов, ни про менеджмент памяти.
Тема в архиве.