Первая игра на idTech 6, здесь они впервые добавили поддержку Vulkan, но при этом было допущено не мало мелких ошибок и нарушений best practices, возможно в то время информации о Vulkan было не много и времени на исправления не хватило.
Как рендерится кадр:
- (CB1, CB2, CB3, CB4) Ожидается fence, чтобы переиспользовать командные буферы, используется двойная буферизация, поэтому командные буферы переиспользуются через кадр. Начинается запись в 4-е командных буфера, в первый командный буфер вставляется global execution barrier чтобы дождаться выполнения предыдущего кадра на стороне GPU. Далее создаются таски которые будут заполнять командные буферы в отдельных потоках.
- (CB0) Ожидается fence, чтобы переиспользовать командный буфер. Копирование host -> device: обновляется буфер с particle vector field. Копирование device -> host: в тотже буфер копируются данные для виртуального текстурирования с предыдущего кадра (feedback buffer). Копирование host -> device: обновляется мегатекстура, я насчитал 7 текстур, это обновление происходит не каждый кадр.
- (CB1) Обновляется shadow map atlas, заполняется velocity map и делается depth pre-pass.
- (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 во фрагментном шейдере.
- (CB3) Рисуются светящиеся (emissive) объекты. Много проходов с downscale и blur на фрагментном шейдере, затем рисуются полупрозрачные объекты и для этой текстуры также применяется downscale. Рисуется volumetric fog, вспышки от выстрелов и другие эффекты. Рисуется distortion map в четверть разрешения экрана.
- (CB4) Рисуется UI, интересно что батчинг для UI не используется и за один draw call рисуется один прямоугольник, на OpenGL это было бы очень медленно, но на Vulkan рисование всего UI занимает 70мкс. Применяется motion blur. Далее downscale и blur, рассчитывается средняя яркость кадра, затем применяется tone mapping и другие пост эффекты. В этом же потоке завершается запись всех командных буферов и они отправляются на GPU, CB0 сабмитится раньше всех отдельным вызовом vkQueueSubmit, далее отправляются все остальные командные буферы (CB1, CB2, CB3, CB4), на стороне GPU они будут выполняться именно в таком порядке. И далее вызывается vkQueuePresent.
Особенности:
- Используется расширение NV_dedicated_allocation для всех рендер таргетов, на NVidia это дает небольшой прирост производительности. Сейчас dedicated_allocation добавлено в Vulkan 1.1 и поддерживается всеми вендорами.
- Используют dynamic buffer offsets чтобы уменьшить количество descriptor set'ов.
- Часто обновляемые юниформ буферы находятся в host visible памяти.
- За кадр происходит два вызова vkQueueSubmit и два vkWaitForFences, время блокировки на ожидании фенса всего 10мкс, время кадра на GTX1070 в HD 2-6мс, в 4k - около 15мс.
- Везде используется двойная буферизация, поэтому кадры синхронизируются через один, например 1 и 3, 2 и 4.
- В device features выставлен robustBufferAccess = false, это отключает безопасный доступ к буферу в шейдере и драйвер может упасть при попытке прочитать или записать за границами буфера.
- Все выделения памяти происходит при старте и при загрузке уровня, так как они достаточно медленные. Затем выделенная память переиспользуется.
- Все буферы и текстуры создаются заранее, на старте и при загрузке уровня. В редких случаях создаются новые буферы во время игры. Причем создание ресурсов идет с задержкой в один кадр, то есть параллельно с рендером кадра создается буфер, а на следующем кадре уже после синхронизации потоков этот буфер начинают использовать. Синхронизации между потоками во время рисования кадра не происходят.
- Синхронизация между кадрами на GPU идет за счет global execution barrier.
- Downscale и blur сделаны на фрагментных шейдерах, так как GPU не поддерживают параллельное выполнение команд вместе с рисованием, то в какой-то момент начинают простаиваться вычислительные ядра. Но использование компьют шейдеров не всегда дает прирост производительности.
Недостатки:
- Pipeline barrier'ы расставлены неоптимально, встречается VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, что приводит к ожиданию завершения абсолютно всех предыдущих команд. Ненужные барьеры с VK_ACCESS_HOST_WRITE_BIT, так как используется когерентная память. Вызовы vkCmdPipelineBarrier не сгруппированы в один.
- Некоторые командные буферы содержат минимум полезных команд. Переключение командных буферов очень быстрое, поэтому это никак не повлияет на производительность, но можно было объединить с другими.
- Не используется reverse depth buffer, но для коридорного шутера это не страшно.
- Шейдеры в SPIRV содержат имена переменных и функций, что очень упрощает реверс-инженеринг. Также остался код, результат которого нигде не используется, что может немного повлиять на производительность.
- Очистка текстур перед рисованием сделана через отдельный рендер пасс без вызовов рисования, но с loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR, а затем следующий рендер пасс идет с loadOp = VK_ATTACHMENT_LOAD_OP_LOAD и получает очищенные рендер таргеты. Не очень понятно зачем так сделано, возможно планировалось вообще убрать очистку, потому что в процессе рисования закрашиваются все пиксели, но видимо из-за особенностей драйверов того времени это не получилось сделать.
- Используют loadOp = VK_ATTACHMENT_LOAD_OP_LOAD тогда, когда достаточно и VK_ATTACHMENT_LOAD_OP_DONT_CARE, например при downscale пинпонгом. Возможно на дискретных GPU это не сильно влияет на производительность, но на мобильных это плохо.
- Нет кэширования состояний хотя бы для пайплайнов, индексных и вершинных буферов, из-за этого получаются лишние вызовы Vulkan API, но они достаточно дешевые.
- Не используют push constants.
- Не используют флаг VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, хотя команд буферы используются один раз, а потом перезаписываются.
Комментариев нет:
Отправить комментарий