Войти
ФлеймФорумПроЭкты

FrameGraph

Страницы: 1 2 310 11 Следующая »
#0
13:28, 13 сен 2018

Решил создать тему для всех моих текущих проектов.

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++ код, в вызовы вулкана и в вызовы фреймграфа

#1
14:39, 14 сен 2018

Наконец сконвертировал трейс и с++ и все работает, надо будет еще некоторые моменты пофиксить.
Но есть проблема: маленький трейс превращается в 500мб кода, который компилируется 50мин и бинарник весит 230мб.
Звучить совсем непрактично, так что буду думать как сохранить читаемый и удобный для редактирования код, но не такой огромный)

#2
22:38, 26 сен 2018

Немного отвлекся и сделал визуализацию графа синхронизаций
Изображение
оранжевым отмечены сигналы - fence или semaphore
красные ноды и линии - ожидание сигнала

#3
14:29, 2 окт 2018

наконец запустил трейс записаный на nvidia под intel

vktrace player | FrameGraph


теперь можно писать тесты на производительность и гонять на разных девайсах)

#4
1:32, 14 дек 2018

Сделал такую визуализацию графа и расставленых барьеров.
fg | FrameGraph

Барьеры делал как тут, только намного меньше, иначе мешают читать все остальное.

+ Показать
#5
1:41, 14 дек 2018

/A\
> Сделал такую визуализацию графа и расставленых барьеров.
А можно все это в код превратить?
Ну типа блюпринта для пайплайна.

#6
2:47, 14 дек 2018

Great V.
> А можно все это в код превратить?
Ну какбы оно визуализирует граф собраный кодом.
Вот пример: https://github.com/azhirnov/FrameGraph/blob/dev/tests/framegraph/… mage4.cpp#L76
Есть метод AddTask и у тасков есть зависимость DependsOn, к этому можно прикрутить уже любой более удобный интерфейс.

#7
12:00, 14 дек 2018

/A\
> Ну какбы оно визуализирует граф собраный кодом.
Это я шарю.
Я имею ввиду сложно ли из этого запилить графический редактор.
Чтобы натыкать квадратиков, соединить стрелочками и получить на выходе код.

#8
12:58, 14 дек 2018

Great V.
> Я имею ввиду сложно ли из этого запилить графический редактор.
При желании можно сделать все)
Тут все же низкоуровневые функции, обычно такие редакторы делают для более высокоуровневого кода.

#9
12:40, 20 дек 2018

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 (тут наглядная схема есть), идея в том, что каждый поток пишет команд буфер для какой-то части батча, как только батч заполнен, то он сразу сабмитится. Если у батча нет последующих батчей, то на него вешается фенс.

#10
12:51, 20 дек 2018

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(...)

> если ты пишешь в буфер, а потом из него читаешь, то нужно ли вообще создавать для этого отдельные два таска с зависимостями и не проще ли хранить
> синхронизационные примитивы внутри самого класса буфера?
Таски это просто набор команд, если есть необходимость часто вызывать несколько тасков последовательно, то есть смысл добавить новый тип таска который их заменит и будет более оптимизированным.
Информация по синхронизациям и так хранятся внутри буфера.

#11
13:26, 20 дек 2018

/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();
  }

прибитый гвоздями к полу монолит, в котором всё примотано ко всему изолентой. такой код нереально ни поддерживать, ни расширять.

#12
13:38, 20 дек 2018

ещё вопрос. ты говоришь, что ковырял трейсы дума 4. у тебя есть картинки с их frame graph? интересно посмотреть, как он выглядит.

#13
13:44, 20 дек 2018

Suslik
http://www.adriancourreges.com/blog/2016/09/09/doom-2016-graphics-study/

#14
13:47, 20 дек 2018

lookid
я это уже читал. там нет ничего ни про синхронизацию, ни про организацию пассов, ни про менеджмент памяти.

Страницы: 1 2 310 11 Следующая »
ФлеймФорумПроЭкты

Тема в архиве.