понедельник, 4 марта 2019 г.

Разбор движка в Doom (2016)

  Первая игра на idTech 6, здесь они впервые добавили поддержку Vulkan, но при этом было допущено не мало мелких ошибок и нарушений best practices, возможно в то время информации о Vulkan было не много и времени на исправления не хватило.



Предварительно желательно ознакомпится с разбором рендера (doom 2016 graphics study).



Как рендерится кадр:
  1. (CB1, CB2, CB3, CB4) Ожидается fence, чтобы переиспользовать командные буферы, используется двойная буферизация, поэтому командные буферы переиспользуются через кадр. Начинается запись в 4-е командных буфера, в первый командный буфер вставляется global execution barrier чтобы дождаться выполнения предыдущего кадра на стороне GPU. Далее создаются таски которые будут заполнять командные буферы в отдельных потоках.
  2. (CB0) Ожидается fence, чтобы переиспользовать командный буфер. Копирование host -> device: обновляется буфер с particle vector field. Копирование device -> host: в тотже буфер копируются данные для виртуального текстурирования с предыдущего кадра (feedback buffer). Копирование host -> device: обновляется мегатекстура, я насчитал 7 текстур, это обновление происходит не каждый кадр.
  3. (CB1) Обновляется shadow map atlas, заполняется velocity map и делается depth pre-pass.
  4. (CB2) Заполняется G-buffer, заполняется feedback buffer для виртуального текстурирования. Тут используется clustered forward renderer, информация об источниках освещения хранится в двух буферах clusternumlights и clusterlightsid у них host visible память, для них используется двойная буферизация и данные обновляются каждый кадр. Далее идет симуляция частиц, где используется particle vector field буфер. Затем рассчитывается screen-space directional occlusion (SSDO) во фрагментном шейдере и в половинном разрешении, также там есть опциональный temporal anti-aliasing (TAA). Следующий проход - screen-space reflections (SSR) также во фрагментном шейдере. Далее static cubemap reflections (SCR) во фрагментном шейдере. В компьют шейдере объединяются SSDO, SSR, SCR и накладывается туман. Последним идет particle lighting во фрагментном шейдере.
  5. (CB3) Рисуются светящиеся (emissive) объекты. Много проходов с downscale и blur на фрагментном шейдере, затем рисуются полупрозрачные объекты и для этой текстуры также применяется downscale. Рисуется volumetric fog, вспышки от выстрелов и другие эффекты. Рисуется distortion map в четверть разрешения экрана.
  6. (CB4) Рисуется UI, интересно что батчинг для UI не используется и за один draw call рисуется один прямоугольник, на OpenGL это было бы очень медленно, но на Vulkan рисование всего UI занимает 70мкс. Применяется motion blur. Далее downscale и blur, рассчитывается средняя яркость кадра, затем применяется tone mapping и другие пост эффекты. В этом же потоке завершается запись всех командных буферов и они отправляются на GPU, CB0 сабмитится раньше всех отдельным вызовом vkQueueSubmit, далее отправляются все остальные командные буферы (CB1, CB2, CB3, CB4), на стороне GPU они будут выполняться именно в таком порядке. И далее вызывается vkQueuePresent.

Особенности:
  1. Используется расширение NV_dedicated_allocation для всех рендер таргетов, на NVidia это дает небольшой прирост производительности. Сейчас dedicated_allocation добавлено в Vulkan 1.1 и поддерживается всеми вендорами.
  2. Используют dynamic buffer offsets чтобы уменьшить количество descriptor set'ов.
  3. Часто обновляемые юниформ буферы находятся в host visible памяти.
  4. За кадр происходит два вызова vkQueueSubmit и два vkWaitForFences, время блокировки на ожидании фенса всего 10мкс, время кадра на GTX1070 в HD 2-6мс, в 4k - около 15мс.
  5. Везде используется двойная буферизация, поэтому кадры синхронизируются через один, например 1 и 3, 2 и 4.
  6. В device features выставлен robustBufferAccess = false, это отключает безопасный доступ к буферу в шейдере и драйвер может упасть при попытке прочитать или записать за границами буфера.
  7. Все выделения памяти происходит при старте и при загрузке уровня, так как они достаточно медленные. Затем выделенная память переиспользуется.
  8. Все буферы и текстуры создаются заранее, на старте и при загрузке уровня. В редких случаях создаются новые буферы во время игры. Причем создание ресурсов идет с задержкой в один кадр, то есть параллельно с рендером кадра создается буфер, а на следующем кадре уже после синхронизации потоков этот буфер начинают использовать. Синхронизации между потоками во время рисования кадра не происходят.
  9. Синхронизация между кадрами на GPU идет за счет global execution barrier.
  10. Downscale и blur сделаны на фрагментных шейдерах, так как GPU не поддерживают параллельное выполнение команд вместе с рисованием, то в какой-то момент начинают простаиваться вычислительные ядра. Но использование компьют шейдеров не всегда дает прирост производительности.
Недостатки:
  1. Pipeline barrier'ы расставлены неоптимально, встречается VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, что приводит к ожиданию завершения абсолютно всех предыдущих команд. Ненужные барьеры с VK_ACCESS_HOST_WRITE_BIT, так как используется когерентная память. Вызовы vkCmdPipelineBarrier не сгруппированы в один.
  2. Некоторые командные буферы содержат минимум полезных команд. Переключение командных буферов очень быстрое, поэтому это никак не повлияет на производительность, но можно было объединить с другими.
  3. Не используется reverse depth buffer, но для коридорного шутера это не страшно.
  4. Шейдеры в SPIRV содержат имена переменных и функций, что очень упрощает реверс-инженеринг. Также остался код, результат которого нигде не используется, что может немного повлиять на производительность.
  5. Очистка текстур перед рисованием сделана через отдельный рендер пасс без вызовов рисования, но с loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR, а затем следующий рендер пасс идет с loadOp = VK_ATTACHMENT_LOAD_OP_LOAD и получает очищенные рендер таргеты. Не очень понятно зачем так сделано, возможно планировалось вообще убрать очистку, потому что в процессе рисования закрашиваются все пиксели, но видимо из-за особенностей драйверов того времени это не получилось сделать.
  6. Используют loadOp = VK_ATTACHMENT_LOAD_OP_LOAD тогда, когда достаточно и VK_ATTACHMENT_LOAD_OP_DONT_CARE, например при downscale пинпонгом. Возможно на дискретных GPU это не сильно влияет на производительность, но на мобильных это плохо.
  7. Нет кэширования состояний хотя бы для пайплайнов, индексных и вершинных буферов, из-за этого получаются лишние вызовы Vulkan API, но они достаточно дешевые.
  8. Не используют push constants.
  9. Не используют флаг VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, хотя команд буферы используются один раз, а потом перезаписываются.
  

Комментариев нет:

Отправить комментарий