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

FrameGraph (4 стр)

Страницы: 13 4 5 610 Следующая »
#45
5:48, 19 янв. 2019

нашёл интересное чтиво по frame graph'ам. судя по всему, всё началось отсюда: https://www.gdcvault.com/play/1024612/FrameGraph-Extensible-Rende… chitecture-in . пока в соседнем треде кто-то стонал от 20 дроколлов, у DICE в 2016 году количество _пассов_ превысило несколько сотен и они изобрели первый frame graph. в их реализации каждый пасс — это класс, который в конструкторе говорит, какие ресурсы ему понадобятся, а в виртуальном методе — что он делает. мне показалось это плохим решением, потому что такой подход не даёт достаточно чёткой структуры графу и из-за большого количества хардкода далеко не является data-driven. более интересную, на мой взгляд, реализацию тех же идей можно найти у http://themaister.net/blog/2017/08/15/render-graphs-and-vulkan-a-deep-dive/

интересный момент, что в их реализации каждый пасс только указывает, какие ресурсы он принимает на вход и какие ресурсы даёт на выходе. все зависимости между пассами генерятся автоматически, я считаю это правильным. неприятный момент — в их реализации синхронизация осуществляется только между пассами. например, доступ к буферам и текстурам никак не синхронизируется. но мне пока не очевидно, нужен ли он в полевых условиях.

#46
15:06, 19 янв. 2019

Suslik
Насколько  помню у Dice была проблема с управлением памятью для временных ресурсов, граф можно оптимизировать для минимального использования памяти, когда все команды выполняются последовательно или наоборот выделить побольше памяти и запустить все что можно параллельно.
Можно еще мержить шейдеры постпроцессов чтоб было меньше пин-понгов между рендертаргетами.

Я смотрел реализацию в Granite еще летом, тогда там все зависимости были через ивенты, потому что

I opted for VkEvent because it can express execution overlap, while pipeline barriers cannot.

Звучит странно, мои тесты на нвидиа показали, что ивенты работают в 2 раза медленее барьеров и никак не влияют на распараллеливание, но тестировать это средствами вулкана не совсем правильно, так как запись времени вызывает какие-то синхронизации и тоже замедляет работу и возможно препятствует распараллеливанию.
Сейчас там многое поменялось и похоже он хорошо протестирован на разных гпу, там есть проверки и на версию драйверов и на вендора, чтоб обходить накоторые проблемы с реализацией и с производительностью

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

#47
15:20, 19 янв. 2019

отстой в том, что лэйауты у имеджей являются состоянием не всего ресурса, а сабресурса. то есть один миплевел может быть в одном лэйауте, а другой миплевел — в другом. и даже для самого простого шейдера построения миплевелов это важно учитывать, так как пока из одного миплевела читаем, в другой пишем. аналогично с layer'ами для текстурных массивов.

я хочу придумать интерфейс для работы с рендерграфом, который бы это всё учитывал.

#48
15:42, 19 янв. 2019

Suslik
Ну так в моей реализации учитываются и диапазоны буфера и слои и мипмэпы в имейджах.
Но есть нюанс: например ты копируешь 2д имейджи в несколько проходов, диапазоны не пересекаются, а барьеры все равно вставляются, потому что не учитываются 2д координаты. То же самое с компьют и рендером.
Я вот только не помню что говорит спеки по таким случаям, то есть что будет если я в разных компьют пассах буду писать в один и тотже имейдж, но координаты не будут пересекаться, аналогично и для рендера, что будет если рисовать в одну текстуру, но в разные виюпорты.

#49
15:52, 19 янв. 2019

Еще в статье по Granite написано что он сортирует граф так, чтобы перед синхронизацией вставлялось побольше операций не требующих синхронизации. Но ведь это само разруливается на ГПУ. Точнее так: рендер обычно никак не распараллеливается, поэтому тут с точки зрения порядка выполнения без разницы есть синхронизация между ними или нет, а вот компьют везде распараллеливается насколько это возможно, если это не глобальный барьер, то он будет влиять только те ресурсы и те диапазоны данных которые указаны в барьере, это никак не помешает выполнять другие команды пока одна команда ожидает завершения предыдущей с которой нужна синхронизация.

Тут есть примеры
https://mynameismjp.wordpress.com/2018/12/09/breaking-down-barrie… d-preemption/

+ Показать
#50
15:52, 19 янв. 2019

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

#51
16:00, 19 янв. 2019

Suslik
> это разрешается
Там есть нюансы типа image granularity для копирования, render area granularity для рендера.
Еще при optimal tiling пиксели в имейдже расположены в другом порядке и получается что диапазоны в 2д координатах не пересекаются, а в памяти пересекаются. Так что тут надо почитать что пишут.

#52
12:56, 2 фев. 2019

Посмотрел как сделано в вольфенштейн. Так там 9 сабмитов и 6 ожиданий фенса, получается часть кадра отправляется на отрисовку пока остальная часть еще может записываться в команд буфер. Это немного снижает frame latency. На ожидание одного фенса в дум тратилось до 10мкс, в вольфенштейне на все 6 фенсов уходит 10-20мкс, так что недостатков в таком подходе я не вижу.
На картинке сверху пример как сделано в вольфенштейне, снизу как я могу сделать в своем фреймграфе. Из-за этапа синхронизации всех потоков могут быть чуть бОльшие потери чем у вольфенштейна, но когда я это разрабатывал вариант без синхронизаций занял бы намного больше времени.

cpu-gpu-sync | FrameGraph

#53
3:08, 12 фев. 2019

Надо было подебажить вершинный шейдер, включил запись трейса для gl_VertexIndex == 0 и на всю сцену только одно срабатывание. Получается на нвидиа хардварный кулинг отсекает все лишнее до запуска шейдера. По этой же причине в idTech для нвидиа не включено по дефолту occlusion culling.

#54
11:35, 16 фев. 2019

наконец-то я закончил свою реализацию render graph'а. вот так у меня выглядит основной цикл с примером рендеринга:

auto frameInfo = inFlightQueue->BeginFrame();
{
  auto &frameResources = frameResourcesDatum[frameInfo.renderGraph];
  if (!frameResources)
  {
    glm::uvec2 size = { inFlightQueue->GetImageSize().width, inFlightQueue->GetImageSize().height };
    frameResources.reset(new FrameResources(frameInfo.renderGraph, size));
  }

  const legit::DescriptorSetLayoutKey *frameSetInfo = frameDescriptorLayoutShader.GetSetInfo(FrameSetIndex); //should be the same for all shaders
  uint32_t frameConstantOffset = frameInfo.memoryPool->BeginSet(frameSetInfo);
  {
    auto frameConstantBuffer = frameInfo.memoryPool->GetBufferData<FrameConstantBuffer>("FrameData");
    frameConstantBuffer->time = std::chrono::duration<float>(std::chrono::system_clock::now() - startTime).count();
  }
  frameInfo.memoryPool->EndSet();
  auto frameSet = descriptorSetCache.GetDescriptorSet(*frameSetInfo, frameInfo.memoryPool->GetBuffer()->GetHandle(), {});


  frameInfo.renderGraph->AddRenderPass({ frameInfo.swapchainImageViewProxy }, frameResources->depthStencilImageViewProxy, {}, inFlightQueue->GetImageSize(), [&](legit::RenderGraph::RenderPassContext passContext)
  {
    const legit::DescriptorSetLayoutKey *passSetInfo = shaderProgram.vertexShader->GetSetInfo(PassSetIndex); //should be the same for all objects with the same shader
    uint32_t passConstantOffset = frameInfo.memoryPool->BeginSet(passSetInfo);
    {
      auto passConstantBuffer = frameInfo.memoryPool->GetBufferData<PassConstantBuffer>("PassData");
      float aspect = float(inFlightQueue->GetImageSize().width) / float(inFlightQueue->GetImageSize().height);
      passConstantBuffer->projMatrix = glm::perspective(1.0f, aspect, 0.01f, 1000.0f) * glm::scale(glm::vec3(1.0f, -1.0f, -1.0f));
      passConstantBuffer->viewMatrix = glm::inverse(camera.GetTransformMatrix());
    }
    frameInfo.memoryPool->EndSet();
    auto passSet = descriptorSetCache.GetDescriptorSet(*passSetInfo, frameInfo.memoryPool->GetBuffer()->GetHandle(), {});


    auto pipeineInfo = pipelineCache.BindPipeline(passContext.GetCommandBuffer(), passContext.GetRenderPass()->GetHandle(), legit::DepthSettings::DepthTest(), legit::BlendSettings::Opaque(), vertexDecl, &shaderProgram);
    {
      //if(!frameSetIsBound)
      //passContext.commandBuffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipeineInfo.pipelineLayout, FrameSetIndex, { frameSet }, { frameConstantOffset });
      //if(!passSetIsBound)
      //passContext.commandBuffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipeineInfo.pipelineLayout, PassSetIndex, { passSet }, { passConstantOffset });

      const legit::DescriptorSetLayoutKey *drawCallSetInfo = shaderProgram.vertexShader->GetSetInfo(DrawCallSetIndex); //should be unique for all objects

      for (auto &object : objects)
      {
        uint32_t drawCallConstantOffset = frameInfo.memoryPool->BeginSet(drawCallSetInfo);
        {
          auto drawCallData = frameInfo.memoryPool->GetBufferData<DrawCallConstantBuffer>("DrawCallData");
          drawCallData->objectMatrix = object.transform;
        }
        frameInfo.memoryPool->EndSet();
        auto drawCallSet = descriptorSetCache.GetDescriptorSet(*drawCallSetInfo, frameInfo.memoryPool->GetBuffer()->GetHandle(), {});
        //passContext.commandBuffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipeineInfo.pipelineLayout, DrawCallSetIndex, { drawCallSet }, { drawCallConstantOffset });
        passContext.GetCommandBuffer().bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipeineInfo.pipelineLayout, FrameSetIndex,
          { frameSet, passSet, drawCallSet },
          { frameConstantOffset, passConstantOffset, drawCallConstantOffset });

        passContext.GetCommandBuffer().bindVertexBuffers(0, { object.mesh->vertexBuffer->GetBuffer() }, { 0 });
        passContext.GetCommandBuffer().bindIndexBuffer(object.mesh->indexBuffer->GetBuffer(), 0, vk::IndexType::eUint32);
        passContext.GetCommandBuffer().drawIndexed(uint32_t(object.mesh->indicesCount), 1, 0, 0, 0);
      }
    }
  });
}
inFlightQueue->EndFrame();

целью моей реализации является написать как можно более тонкую обёртку, по минимуму ограничивающую возможности API, но позволяющую автоматизировать менеджмент всех layout'ов, barrier'ов, pipeline'ов, framebuffer'ов, renderpass'ов, шейдерной памяти и зависимостей между рендерпассами.

с функциональной точки зрения пока сыро и не все типы зависимостей реализованы, но меня в первую очередь интересует архитектурная организация, которая это всё позволяет. это, разумеется, не является игровым движком, но задумывается как промежуточная прослойка между игровым рендером и api.

#55
11:50, 16 фев. 2019

Suslik
Коментариев нехватает или кода целиком.
А ты уже думал про многопоточность, про использование разных очередей и синхронизации между ними?
Я тут понял, что понятие "кадр" не очень удобное, надо все переделывать на отдельные батчи, каждый батч это один вызов vkQueueSubmit, а внутри уже обычный рендер граф, который расставляет зависимости. Батчи также организуются в граф, но уже не разделяются на кадры, тогда можно делать зависимости между кадрами и между разными очередями.

#56
(Правка: 12:05) 11:58, 16 фев. 2019

/A\
> Я тут понял, что понятие "кадр" не очень удобное
у меня нет понятия кадра в графе. в графе есть таск ImagePresent, вот и всё

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

> А ты уже думал про многопоточность, про использование разных очередей и синхронизации между ними?
зависимостей между разными очередями у меня пока нет. однако, многопоточность у меня поддерживается следующим образом: исходим из предположения, что количество рендерпассов на кадр относительно невелико (во фростбайте их около 400, у нас — около 50). все пассы записываются последовательно. однако, дроколлов внутри каждого пасса может быть много (тысячи), поэтому они сабмитятся параллельно в несколько потоков. это можно реализовать без взаимных блокировок потоков, если каждый поток использует внутри пасса свой command buffer, а когда все потоки готовы, то все полученные command buffer'ы объединяются в один.

зависимость между очередями в общем-то сделать можно, но придётся автоматически расставлять семафоры и сабмиты, чего мне пока делать не хочется.

#57
(Правка: 12:36) 12:17, 16 фев. 2019

сделал попроще пример с минимумом функций:

//frameInfo хранит в себе контекст кадра -- пул памяти для шейдерных констант, рендерграф и тому подобное
auto frameInfo = inFlightQueue->BeginFrame();
{
  //рендерпасс -- это одна установка render target'ов
  //вызов ниже добавляет один рендерпасс, который рендерит на свопчейн
  //в общем случае один рендерпасс делается на рендеринг gbuffer'а, один -- на shadow map, один -- на постпроцесс и так далее
  frameInfo.renderGraph->AddRenderPass({ frameInfo.swapchainImageViewProxy }, nullptr, {}, inFlightQueue->GetImageSize(), [&](legit::RenderGraph::RenderPassContext passContext)
  {

    //один байндинг пайплайна осуществляется каждый раз, когда меняется шейдер внутри рендерпасса
    //в общем случае пайплайн выставляется для каждого уникального типа материала
    auto pipeineInfo = pipelineCache.BindPipeline(passContext.GetCommandBuffer(), passContext.GetRenderPass()->GetHandle(), legit::DepthSettings::DepthTest(), legit::BlendSettings::Opaque(), vertexDecl, &shaderProgram);
    {
      //DescriptorSetLayoutKey хранит в себе расположение ресурсов внутри шейдера
      //и позволяет из текста шейдера через reflection узнавать, по какому offset'у находится каждый константный буффер
      const legit::DescriptorSetLayoutKey *drawCallSetInfo = shaderProgram.vertexShader->GetSetInfo(DrawCallSetIndex);

      for (auto &object : objects)
      {
        //memory pool хранит в себе всю шейдерную память, используемую в текущем кадре
        //в этом простейшем случае каждый объект выставляет только уникальную для него матрицу, однако, 
        //можно выставлять констанные буферы с меньшей частотой обновления -- например, матрица камеры типично меняется один раз за рендерпасс и её нет смысла загружать для каждого объекта отдельно
        //BeginSet() означает выделение памяти в пуле для очередного сета со структурой drawCallSetInfo. этот сет отображается в тип DrawCallConstantBuffer и в шейдере называется "DrawCallData"
        uint32_t drawCallConstantOffset = frameInfo.memoryPool->BeginSet(drawCallSetInfo);
        {
          auto drawCallData = frameInfo.memoryPool->GetBufferData<DrawCallConstantBuffer>("DrawCallData");
          drawCallData->objectMatrix = object.transform;
        }
        frameInfo.memoryPool->EndSet();
        auto drawCallSet = descriptorSetCache.GetDescriptorSet(*drawCallSetInfo, frameInfo.memoryPool->GetBuffer()->GetHandle(), {});

        passContext.GetCommandBuffer().bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipeineInfo.pipelineLayout, DrawCallSetIndex, { drawCallSet }, { drawCallConstantOffset });

        //непосредственно рендеринг меша объекта
        passContext.GetCommandBuffer().bindVertexBuffers(0, { object.mesh->vertexBuffer->GetBuffer() }, { 0 });
        passContext.GetCommandBuffer().bindIndexBuffer(object.mesh->indexBuffer->GetBuffer(), 0, vk::IndexType::eUint32);
        passContext.GetCommandBuffer().drawIndexed(uint32_t(object.mesh->indicesCount), 1, 0, 0, 0);
      }
    }
  });
}
//EndFrame автоматически вызывает исполнение frame graph'а и выводит изображенеи на экран
inFlightQueue->EndFrame();

на вход каждому рендерпассу передаётся массив рендртаргетов и массив текстур, которые будут читаться в шейдере. эти данные используются для автоматического расставляния барьеров и смены лэйаутов этих изображений. также интересный момент заключается в том, что рендерграф по умолчанию работает не с самими изображениями, а с их proxy. под proxy понимается описание изображения — размер, формат, а рендерграф сам решает, как и когда для них выделять память. чтобы получить из proxy (который по сути — просто числовой id) готовое изображение, в каждый рендерпасс передаётся passContext, который позволяет разыменовать proxy непосредственно в vk::Image или vk::ImageView. это позволяет автоматически переиспользовать временные рендертаргеты. например, в deferred rendering'е после расчёта освещения, память текстур gbuffer'а может использоваться для текстур постпроцесса.

#58
12:46, 16 фев. 2019

Suslik
> с точки зрения рендера, разбиение на кадры необходимо, чтобы разделять доступ к ресурсам.
Ну так ты вызываешь vkQueueSubmit и передаешь туда фенс, а чтоб переиспользовать ожидаешь этот фенс.
То что у тебя на кадр будет не один сабмит, а 10 никак не сломает переиспользование ресурсов.
Чем раньше ты отправишь команды на гпу, тем раньше начнется рисование и это уменьшит задерку между инпутом и выводом на экран.

> если каждый поток использует внутри пасса свой command buffer
И свой command pool тоже, так как доступ к пулу должен быть потокобезопасен.

> сделал попроще пример с минимумом функций:
Так понятней. А что с зависимостями между ресурсами?

#59
12:50, 16 фев. 2019

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

Страницы: 13 4 5 610 Следующая »
ФлеймФорумПроЭкты